nginx-api-key-proxy / install-nginx.sh
AnthropicallyHuggingFaces's picture
Add secure nginx installer with API key auto-discovery
20fbc8d verified
Raw History Blame Contribute Delete
12.6 kB
#!/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" <<EOF
UPSTREAM_API_KEY=$UPSTREAM_API_KEY
EOF
)
chmod 600 "$RUNTIME_ENV"
# ---------------------------------------------------------------------------
# 1. Build dependencies (gettext-base => 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" <<EOL
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=network.target
[Service]
Type=forking
PIDFile=${INSTALL_PATH}/nginx.pid
EnvironmentFile=${RUNTIME_ENV}
ExecStartPre=${INSTALL_PATH}/sbin/nginx -t
ExecStart=${INSTALL_PATH}/sbin/nginx
ExecReload=${INSTALL_PATH}/sbin/nginx -s reload
ExecStop=${INSTALL_PATH}/sbin/nginx -s quit
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOL
# ---------------------------------------------------------------------------
# 6. nginx.conf TEMPLATE. Note: the key is NOT interpolated here.
# `env upstream_api_key;` exposes the var; $upstream_api_key is resolved
# at request time from the master process environment.
# ---------------------------------------------------------------------------
cat > "$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)."