#!/bin/bash set -euo pipefail # =========================================================================== # install-nginx.sh # Builds nginx from source, wires up a systemd unit, and configures a # reverse proxy that injects an upstream API key server-side. # # The API key is resolved automatically (Vault / AWS Secrets Manager / # local files / systemd units), stored ONLY in a root-only runtime env # file, and is never written into nginx.conf. # =========================================================================== # --------------------------------------------------------------------------- # Variables # --------------------------------------------------------------------------- NGINX_VERSION=1.25.3 INSTALL_PATH=/usr/local/nginx SERVICE_FILE=/etc/systemd/system/nginx.service TEMPLATE_FILE=/tmp/nginx.conf.template # Runtime env file consumed by systemd -> nginx master process. # This is where the ONLY copy of the key lives at rest (chmod 600, root). RUNTIME_ENV=/etc/nginx-upstream.env SECRET_FILE=/etc/nginx-secrets.env # legacy/cache; read for back-compat DEFAULT_API_KEY="__REPLACE_ME__" # --- HashiCorp Vault ------------------------------------------------------- VAULT_ADDR="${VAULT_ADDR:-}" VAULT_SECRET_PATH="${VAULT_SECRET_PATH:-}" VAULT_SECRET_FIELD="${VAULT_SECRET_FIELD:-api_key}" VAULT_TOKEN_FILE="${VAULT_TOKEN_FILE:-/etc/vault-token}" # AppRole (used only if no token above is available) VAULT_ROLE_ID="${VAULT_ROLE_ID:-}" VAULT_SECRET_ID_FILE="${VAULT_SECRET_ID_FILE:-/etc/vault-approle-secret-id}" # --- AWS Secrets Manager --------------------------------------------------- AWS_SECRET_ID="${AWS_SECRET_ID:-}" AWS_SECRET_FIELD="${AWS_SECRET_FIELD:-api_key}" AWS_REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" # --- Heuristic local sources ---------------------------------------------- LOCAL_KEY_SOURCES=( "/etc/myapp/api.key" "/etc/myapp/secrets.env" "/opt/app/.env" "/etc/environment" ) BACKEND_UNITS=( "myapp.service" "model-api.service" ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { printf '[setup] %s\n' "$*"; } warn() { printf '[setup][warn] %s\n' "$*" >&2; } ensure_secret_tools() { if [[ -n "$VAULT_SECRET_PATH" || -n "$AWS_SECRET_ID" ]]; then apt-get update -qq || true apt-get install -y jq >/dev/null 2>&1 || warn "jq unavailable; using sed fallback" fi if [[ -n "$AWS_SECRET_ID" ]] && ! command -v aws >/dev/null 2>&1; then apt-get install -y awscli >/dev/null 2>&1 || warn "awscli not installable" fi } # Extract the first API key-ish value from a KEY=VALUE style file. key_from_envfile() { local file="$1" name val [[ -r "$file" ]] || return 1 while IFS='=' read -r name val; do name="${name//[[:space:]]/}"; name="${name#export}" case "$name" in UPSTREAM_API_KEY|API_KEY|OPENAI_API_KEY|MODEL_API_KEY|LLM_API_KEY) val="${val%%#*}"; val="${val//\"/}"; val="${val//\'/}" val="${val//[[:space:]]/}" [[ -n "$val" ]] && { printf '%s' "$val"; return 0; } ;; esac done < "$file" return 1 } # Whole file (trimmed) is the key. key_from_keyfile() { local file="$1" [[ -r "$file" ]] || return 1 tr -d '[:space:]' < "$file" | head -c 512 } # Mine a systemd unit for EnvironmentFile= / Environment= keys. key_from_systemd_unit() { local unit="$1" unitfile ef found inline for dir in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do unitfile="$dir/$unit" [[ -f "$unitfile" ]] || continue while read -r ef; do ef="${ef#-}" found="$(key_from_envfile "$ef" 2>/dev/null || true)" [[ -n "$found" ]] && { printf '%s' "$found"; return 0; } done < <(sed -nE 's/^[[:space:]]*EnvironmentFile=//p' "$unitfile") inline="$(sed -nE \ 's/.*(UPSTREAM_API_KEY|API_KEY|OPENAI_API_KEY|MODEL_API_KEY|LLM_API_KEY)=([^[:space:]]+).*/\2/p' \ "$unitfile" | head -n1)" [[ -n "$inline" ]] && { printf '%s' "$inline"; return 0; } done return 1 } # --- Vault: token file, then AppRole -------------------------------------- vault_login() { # Prefer an existing VAULT_TOKEN; else token file; else AppRole. if [[ -z "${VAULT_TOKEN:-}" && -r "$VAULT_TOKEN_FILE" ]]; then VAULT_TOKEN="$(<"$VAULT_TOKEN_FILE")" fi if [[ -n "${VAULT_TOKEN:-}" ]]; then printf '%s' "$VAULT_TOKEN"; return 0 fi # AppRole login: role_id + secret_id (from file), token captured in-var only. [[ -n "$VAULT_ROLE_ID" && -r "$VAULT_SECRET_ID_FILE" ]] || return 1 local secret_id resp tok secret_id="$(<"$VAULT_SECRET_ID_FILE")" resp="$(vault write -format=json auth/approle/login \ role_id="$VAULT_ROLE_ID" secret_id="$secret_id" 2>/dev/null)" || return 1 if command -v jq >/dev/null 2>&1; then tok="$(printf '%s' "$resp" | jq -r '.auth.client_token // empty')" else tok="$(printf '%s' "$resp" | sed -nE 's/.*"client_token"[: ]*"([^"]+)".*/\1/p' | head -n1)" fi [[ -n "$tok" ]] || return 1 printf '%s' "$tok" } key_from_vault() { command -v vault >/dev/null 2>&1 || return 1 [[ -n "$VAULT_ADDR" && -n "$VAULT_SECRET_PATH" ]] || return 1 export VAULT_ADDR VAULT_TOKEN="$(vault_login)" || return 1 export VAULT_TOKEN vault kv get -field="$VAULT_SECRET_FIELD" "$VAULT_SECRET_PATH" 2>/dev/null } # --- AWS Secrets Manager --------------------------------------------------- key_from_aws() { command -v aws >/dev/null 2>&1 || return 1 [[ -n "$AWS_SECRET_ID" ]] || return 1 export AWS_REGION local secret secret="$(aws secretsmanager get-secret-value \ --secret-id "$AWS_SECRET_ID" \ --query SecretString --output text 2>/dev/null)" || return 1 [[ -n "$secret" ]] || return 1 if command -v jq >/dev/null 2>&1; then printf '%s' "$secret" | jq -r --arg k "$AWS_SECRET_FIELD" '.[$k] // empty' else printf '%s' "$secret" \ | sed -nE "s/.*\"${AWS_SECRET_FIELD}\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/p" | head -n1 fi } # =========================================================================== # 0a. Resolve the key (env / cache / Vault / AWS / local / placeholder) # =========================================================================== [[ -f "$SECRET_FILE" ]] && { source "$SECRET_FILE"; } [[ -f "$RUNTIME_ENV" ]] && { source "$RUNTIME_ENV"; } : "${UPSTREAM_URL:=http://127.0.0.1:5000/}" : "${LISTEN_PORT:=80}" if [[ -z "${UPSTREAM_API_KEY:-}" ]]; then ensure_secret_tools log "No UPSTREAM_API_KEY set; resolving from configured sources..." candidate="" if [[ -n "$VAULT_SECRET_PATH" ]]; then candidate="$(key_from_vault || true)" [[ -n "$candidate" ]] && log "Fetched key from Vault ($VAULT_SECRET_PATH)" fi if [[ -z "$candidate" && -n "$AWS_SECRET_ID" ]]; then candidate="$(key_from_aws || true)" [[ -n "$candidate" ]] && log "Fetched key from AWS Secrets Manager ($AWS_SECRET_ID)" fi if [[ -z "$candidate" ]]; then for src in "${LOCAL_KEY_SOURCES[@]}"; do if [[ "$src" == *.key ]]; then candidate="$(key_from_keyfile "$src" 2>/dev/null || true)" else candidate="$(key_from_envfile "$src" 2>/dev/null || true)" fi [[ -n "$candidate" ]] && { log "Found key in $src"; break; } done fi if [[ -z "$candidate" ]]; then for unit in "${BACKEND_UNITS[@]}"; do candidate="$(key_from_systemd_unit "$unit" 2>/dev/null || true)" [[ -n "$candidate" ]] && { log "Found key via systemd unit: $unit"; break; } done fi [[ -n "$candidate" ]] && UPSTREAM_API_KEY="$candidate" fi if [[ -z "${UPSTREAM_API_KEY:-}" ]]; then UPSTREAM_API_KEY="$DEFAULT_API_KEY" warn "No API key resolved; using placeholder ($DEFAULT_API_KEY)." fi export UPSTREAM_API_KEY UPSTREAM_URL LISTEN_PORT # =========================================================================== # 0b. Write the RUNTIME env file (systemd -> nginx). Key stored ONLY here. # =========================================================================== log "Writing runtime env to $RUNTIME_ENV (chmod 600)" ( umask 077 && cat > "$RUNTIME_ENV" < envsubst) # --------------------------------------------------------------------------- apt-get update apt-get install -y \ build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev \ libssl-dev wget curl gettext-base # --------------------------------------------------------------------------- # 2. Download and extract nginx # --------------------------------------------------------------------------- cd /tmp wget -q "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" tar -xzf "nginx-${NGINX_VERSION}.tar.gz" cd "nginx-${NGINX_VERSION}" # --------------------------------------------------------------------------- # 3. Configure # --------------------------------------------------------------------------- ./configure \ --prefix="$INSTALL_PATH" \ --sbin-path="$INSTALL_PATH/sbin/nginx" \ --conf-path="$INSTALL_PATH/nginx.conf" \ --pid-path="$INSTALL_PATH/nginx.pid" \ --lock-path="$INSTALL_PATH/nginx.lock" \ --with-http_ssl_module \ --with-stream \ --with-http_realip_module \ --with-http_v2_module \ --with-threads \ --with-file-aio \ --with-http_stub_status_module # --------------------------------------------------------------------------- # 4. Build and install # --------------------------------------------------------------------------- make -j"$(nproc)" make install # --------------------------------------------------------------------------- # 5. systemd service — EnvironmentFile supplies the key at runtime # --------------------------------------------------------------------------- cat > "$SERVICE_FILE" < "$TEMPLATE_FILE" <<'EOL' worker_processes auto; # Expose the systemd-provided variable to nginx config. env upstream_api_key; events { worker_connections 1024; } http { # Map the environment variable into a normal nginx variable. map $upstream_api_key $api_key_value { default $upstream_api_key; } server { listen ${LISTEN_PORT}; server_name _; location /model-api/ { # Key injected at runtime; never present in this file. proxy_set_header Authorization "Bearer $api_key_value"; proxy_pass ${UPSTREAM_URL}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; } } } EOL # --------------------------------------------------------------------------- # 7. Render. UPSTREAM_API_KEY is DELIBERATELY excluded from the whitelist. # --------------------------------------------------------------------------- ( umask 077 && \ envsubst '$LISTEN_PORT $UPSTREAM_URL' < "$TEMPLATE_FILE" > /tmp/nginx.conf ) install -m 644 -o root -g root /tmp/nginx.conf "$INSTALL_PATH/nginx.conf" rm -f "$TEMPLATE_FILE" /tmp/nginx.conf # --------------------------------------------------------------------------- # 8. Validate and start # --------------------------------------------------------------------------- "$INSTALL_PATH/sbin/nginx" -t if [[ "$UPSTREAM_API_KEY" == "$DEFAULT_API_KEY" ]]; then warn "Still using placeholder key; configure Vault, AWS, or a local source." else log "Key resolved and stored only in $RUNTIME_ENV (chmod 600)." log "nginx.conf contains no secret and is world-readable (644) on purpose." fi log "NGINX install complete (key kept off disk in nginx.conf)."