AnthropicallyHuggingFaces commited on
Commit
20fbc8d
·
verified ·
1 Parent(s): fa1044b

Add secure nginx installer with API key auto-discovery

Browse files
Files changed (1) hide show
  1. install-nginx.sh +341 -0
install-nginx.sh ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # ===========================================================================
5
+ # install-nginx.sh
6
+ # Builds nginx from source, wires up a systemd unit, and configures a
7
+ # reverse proxy that injects an upstream API key server-side.
8
+ #
9
+ # The API key is resolved automatically (Vault / AWS Secrets Manager /
10
+ # local files / systemd units), stored ONLY in a root-only runtime env
11
+ # file, and is never written into nginx.conf.
12
+ # ===========================================================================
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Variables
16
+ # ---------------------------------------------------------------------------
17
+ NGINX_VERSION=1.25.3
18
+ INSTALL_PATH=/usr/local/nginx
19
+ SERVICE_FILE=/etc/systemd/system/nginx.service
20
+ TEMPLATE_FILE=/tmp/nginx.conf.template
21
+
22
+ # Runtime env file consumed by systemd -> nginx master process.
23
+ # This is where the ONLY copy of the key lives at rest (chmod 600, root).
24
+ RUNTIME_ENV=/etc/nginx-upstream.env
25
+ SECRET_FILE=/etc/nginx-secrets.env # legacy/cache; read for back-compat
26
+
27
+ DEFAULT_API_KEY="__REPLACE_ME__"
28
+
29
+ # --- HashiCorp Vault -------------------------------------------------------
30
+ VAULT_ADDR="${VAULT_ADDR:-}"
31
+ VAULT_SECRET_PATH="${VAULT_SECRET_PATH:-}"
32
+ VAULT_SECRET_FIELD="${VAULT_SECRET_FIELD:-api_key}"
33
+ VAULT_TOKEN_FILE="${VAULT_TOKEN_FILE:-/etc/vault-token}"
34
+ # AppRole (used only if no token above is available)
35
+ VAULT_ROLE_ID="${VAULT_ROLE_ID:-}"
36
+ VAULT_SECRET_ID_FILE="${VAULT_SECRET_ID_FILE:-/etc/vault-approle-secret-id}"
37
+
38
+ # --- AWS Secrets Manager ---------------------------------------------------
39
+ AWS_SECRET_ID="${AWS_SECRET_ID:-}"
40
+ AWS_SECRET_FIELD="${AWS_SECRET_FIELD:-api_key}"
41
+ AWS_REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}"
42
+
43
+ # --- Heuristic local sources ----------------------------------------------
44
+ LOCAL_KEY_SOURCES=(
45
+ "/etc/myapp/api.key"
46
+ "/etc/myapp/secrets.env"
47
+ "/opt/app/.env"
48
+ "/etc/environment"
49
+ )
50
+ BACKEND_UNITS=( "myapp.service" "model-api.service" )
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Helpers
54
+ # ---------------------------------------------------------------------------
55
+ log() { printf '[setup] %s\n' "$*"; }
56
+ warn() { printf '[setup][warn] %s\n' "$*" >&2; }
57
+
58
+ ensure_secret_tools() {
59
+ if [[ -n "$VAULT_SECRET_PATH" || -n "$AWS_SECRET_ID" ]]; then
60
+ apt-get update -qq || true
61
+ apt-get install -y jq >/dev/null 2>&1 || warn "jq unavailable; using sed fallback"
62
+ fi
63
+ if [[ -n "$AWS_SECRET_ID" ]] && ! command -v aws >/dev/null 2>&1; then
64
+ apt-get install -y awscli >/dev/null 2>&1 || warn "awscli not installable"
65
+ fi
66
+ }
67
+
68
+ # Extract the first API key-ish value from a KEY=VALUE style file.
69
+ key_from_envfile() {
70
+ local file="$1" name val
71
+ [[ -r "$file" ]] || return 1
72
+ while IFS='=' read -r name val; do
73
+ name="${name//[[:space:]]/}"; name="${name#export}"
74
+ case "$name" in
75
+ UPSTREAM_API_KEY|API_KEY|OPENAI_API_KEY|MODEL_API_KEY|LLM_API_KEY)
76
+ val="${val%%#*}"; val="${val//\"/}"; val="${val//\'/}"
77
+ val="${val//[[:space:]]/}"
78
+ [[ -n "$val" ]] && { printf '%s' "$val"; return 0; }
79
+ ;;
80
+ esac
81
+ done < "$file"
82
+ return 1
83
+ }
84
+
85
+ # Whole file (trimmed) is the key.
86
+ key_from_keyfile() {
87
+ local file="$1"
88
+ [[ -r "$file" ]] || return 1
89
+ tr -d '[:space:]' < "$file" | head -c 512
90
+ }
91
+
92
+ # Mine a systemd unit for EnvironmentFile= / Environment= keys.
93
+ key_from_systemd_unit() {
94
+ local unit="$1" unitfile ef found inline
95
+ for dir in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do
96
+ unitfile="$dir/$unit"
97
+ [[ -f "$unitfile" ]] || continue
98
+ while read -r ef; do
99
+ ef="${ef#-}"
100
+ found="$(key_from_envfile "$ef" 2>/dev/null || true)"
101
+ [[ -n "$found" ]] && { printf '%s' "$found"; return 0; }
102
+ done < <(sed -nE 's/^[[:space:]]*EnvironmentFile=//p' "$unitfile")
103
+ inline="$(sed -nE \
104
+ 's/.*(UPSTREAM_API_KEY|API_KEY|OPENAI_API_KEY|MODEL_API_KEY|LLM_API_KEY)=([^[:space:]]+).*/\2/p' \
105
+ "$unitfile" | head -n1)"
106
+ [[ -n "$inline" ]] && { printf '%s' "$inline"; return 0; }
107
+ done
108
+ return 1
109
+ }
110
+
111
+ # --- Vault: token file, then AppRole --------------------------------------
112
+ vault_login() {
113
+ # Prefer an existing VAULT_TOKEN; else token file; else AppRole.
114
+ if [[ -z "${VAULT_TOKEN:-}" && -r "$VAULT_TOKEN_FILE" ]]; then
115
+ VAULT_TOKEN="$(<"$VAULT_TOKEN_FILE")"
116
+ fi
117
+ if [[ -n "${VAULT_TOKEN:-}" ]]; then
118
+ printf '%s' "$VAULT_TOKEN"; return 0
119
+ fi
120
+
121
+ # AppRole login: role_id + secret_id (from file), token captured in-var only.
122
+ [[ -n "$VAULT_ROLE_ID" && -r "$VAULT_SECRET_ID_FILE" ]] || return 1
123
+ local secret_id resp tok
124
+ secret_id="$(<"$VAULT_SECRET_ID_FILE")"
125
+ resp="$(vault write -format=json auth/approle/login \
126
+ role_id="$VAULT_ROLE_ID" secret_id="$secret_id" 2>/dev/null)" || return 1
127
+ if command -v jq >/dev/null 2>&1; then
128
+ tok="$(printf '%s' "$resp" | jq -r '.auth.client_token // empty')"
129
+ else
130
+ tok="$(printf '%s' "$resp" | sed -nE 's/.*"client_token"[: ]*"([^"]+)".*/\1/p' | head -n1)"
131
+ fi
132
+ [[ -n "$tok" ]] || return 1
133
+ printf '%s' "$tok"
134
+ }
135
+
136
+ key_from_vault() {
137
+ command -v vault >/dev/null 2>&1 || return 1
138
+ [[ -n "$VAULT_ADDR" && -n "$VAULT_SECRET_PATH" ]] || return 1
139
+ export VAULT_ADDR
140
+ VAULT_TOKEN="$(vault_login)" || return 1
141
+ export VAULT_TOKEN
142
+ vault kv get -field="$VAULT_SECRET_FIELD" "$VAULT_SECRET_PATH" 2>/dev/null
143
+ }
144
+
145
+ # --- AWS Secrets Manager ---------------------------------------------------
146
+ key_from_aws() {
147
+ command -v aws >/dev/null 2>&1 || return 1
148
+ [[ -n "$AWS_SECRET_ID" ]] || return 1
149
+ export AWS_REGION
150
+ local secret
151
+ secret="$(aws secretsmanager get-secret-value \
152
+ --secret-id "$AWS_SECRET_ID" \
153
+ --query SecretString --output text 2>/dev/null)" || return 1
154
+ [[ -n "$secret" ]] || return 1
155
+ if command -v jq >/dev/null 2>&1; then
156
+ printf '%s' "$secret" | jq -r --arg k "$AWS_SECRET_FIELD" '.[$k] // empty'
157
+ else
158
+ printf '%s' "$secret" \
159
+ | sed -nE "s/.*\"${AWS_SECRET_FIELD}\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/p" | head -n1
160
+ fi
161
+ }
162
+
163
+ # ===========================================================================
164
+ # 0a. Resolve the key (env / cache / Vault / AWS / local / placeholder)
165
+ # ===========================================================================
166
+ [[ -f "$SECRET_FILE" ]] && { source "$SECRET_FILE"; }
167
+ [[ -f "$RUNTIME_ENV" ]] && { source "$RUNTIME_ENV"; }
168
+
169
+ : "${UPSTREAM_URL:=http://127.0.0.1:5000/}"
170
+ : "${LISTEN_PORT:=80}"
171
+
172
+ if [[ -z "${UPSTREAM_API_KEY:-}" ]]; then
173
+ ensure_secret_tools
174
+ log "No UPSTREAM_API_KEY set; resolving from configured sources..."
175
+ candidate=""
176
+
177
+ if [[ -n "$VAULT_SECRET_PATH" ]]; then
178
+ candidate="$(key_from_vault || true)"
179
+ [[ -n "$candidate" ]] && log "Fetched key from Vault ($VAULT_SECRET_PATH)"
180
+ fi
181
+ if [[ -z "$candidate" && -n "$AWS_SECRET_ID" ]]; then
182
+ candidate="$(key_from_aws || true)"
183
+ [[ -n "$candidate" ]] && log "Fetched key from AWS Secrets Manager ($AWS_SECRET_ID)"
184
+ fi
185
+ if [[ -z "$candidate" ]]; then
186
+ for src in "${LOCAL_KEY_SOURCES[@]}"; do
187
+ if [[ "$src" == *.key ]]; then
188
+ candidate="$(key_from_keyfile "$src" 2>/dev/null || true)"
189
+ else
190
+ candidate="$(key_from_envfile "$src" 2>/dev/null || true)"
191
+ fi
192
+ [[ -n "$candidate" ]] && { log "Found key in $src"; break; }
193
+ done
194
+ fi
195
+ if [[ -z "$candidate" ]]; then
196
+ for unit in "${BACKEND_UNITS[@]}"; do
197
+ candidate="$(key_from_systemd_unit "$unit" 2>/dev/null || true)"
198
+ [[ -n "$candidate" ]] && { log "Found key via systemd unit: $unit"; break; }
199
+ done
200
+ fi
201
+ [[ -n "$candidate" ]] && UPSTREAM_API_KEY="$candidate"
202
+ fi
203
+
204
+ if [[ -z "${UPSTREAM_API_KEY:-}" ]]; then
205
+ UPSTREAM_API_KEY="$DEFAULT_API_KEY"
206
+ warn "No API key resolved; using placeholder ($DEFAULT_API_KEY)."
207
+ fi
208
+ export UPSTREAM_API_KEY UPSTREAM_URL LISTEN_PORT
209
+
210
+ # ===========================================================================
211
+ # 0b. Write the RUNTIME env file (systemd -> nginx). Key stored ONLY here.
212
+ # ===========================================================================
213
+ log "Writing runtime env to $RUNTIME_ENV (chmod 600)"
214
+ ( umask 077 && cat > "$RUNTIME_ENV" <<EOF
215
+ UPSTREAM_API_KEY=$UPSTREAM_API_KEY
216
+ EOF
217
+ )
218
+ chmod 600 "$RUNTIME_ENV"
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # 1. Build dependencies (gettext-base => envsubst)
222
+ # ---------------------------------------------------------------------------
223
+ apt-get update
224
+ apt-get install -y \
225
+ build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev \
226
+ libssl-dev wget curl gettext-base
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # 2. Download and extract nginx
230
+ # ---------------------------------------------------------------------------
231
+ cd /tmp
232
+ wget -q "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
233
+ tar -xzf "nginx-${NGINX_VERSION}.tar.gz"
234
+ cd "nginx-${NGINX_VERSION}"
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # 3. Configure
238
+ # ---------------------------------------------------------------------------
239
+ ./configure \
240
+ --prefix="$INSTALL_PATH" \
241
+ --sbin-path="$INSTALL_PATH/sbin/nginx" \
242
+ --conf-path="$INSTALL_PATH/nginx.conf" \
243
+ --pid-path="$INSTALL_PATH/nginx.pid" \
244
+ --lock-path="$INSTALL_PATH/nginx.lock" \
245
+ --with-http_ssl_module \
246
+ --with-stream \
247
+ --with-http_realip_module \
248
+ --with-http_v2_module \
249
+ --with-threads \
250
+ --with-file-aio \
251
+ --with-http_stub_status_module
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # 4. Build and install
255
+ # ---------------------------------------------------------------------------
256
+ make -j"$(nproc)"
257
+ make install
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # 5. systemd service — EnvironmentFile supplies the key at runtime
261
+ # ---------------------------------------------------------------------------
262
+ cat > "$SERVICE_FILE" <<EOL
263
+ [Unit]
264
+ Description=The NGINX HTTP and reverse proxy server
265
+ After=network.target
266
+
267
+ [Service]
268
+ Type=forking
269
+ PIDFile=${INSTALL_PATH}/nginx.pid
270
+ EnvironmentFile=${RUNTIME_ENV}
271
+ ExecStartPre=${INSTALL_PATH}/sbin/nginx -t
272
+ ExecStart=${INSTALL_PATH}/sbin/nginx
273
+ ExecReload=${INSTALL_PATH}/sbin/nginx -s reload
274
+ ExecStop=${INSTALL_PATH}/sbin/nginx -s quit
275
+ PrivateTmp=true
276
+
277
+ [Install]
278
+ WantedBy=multi-user.target
279
+ EOL
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # 6. nginx.conf TEMPLATE. Note: the key is NOT interpolated here.
283
+ # `env upstream_api_key;` exposes the var; $upstream_api_key is resolved
284
+ # at request time from the master process environment.
285
+ # ---------------------------------------------------------------------------
286
+ cat > "$TEMPLATE_FILE" <<'EOL'
287
+ worker_processes auto;
288
+
289
+ # Expose the systemd-provided variable to nginx config.
290
+ env upstream_api_key;
291
+
292
+ events { worker_connections 1024; }
293
+
294
+ http {
295
+ # Map the environment variable into a normal nginx variable.
296
+ map $upstream_api_key $api_key_value {
297
+ default $upstream_api_key;
298
+ }
299
+
300
+ server {
301
+ listen ${LISTEN_PORT};
302
+ server_name _;
303
+
304
+ location /model-api/ {
305
+ # Key injected at runtime; never present in this file.
306
+ proxy_set_header Authorization "Bearer $api_key_value";
307
+
308
+ proxy_pass ${UPSTREAM_URL};
309
+ proxy_http_version 1.1;
310
+ proxy_set_header Host $host;
311
+ proxy_set_header X-Real-IP $remote_addr;
312
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
313
+ proxy_set_header X-Forwarded-Proto $scheme;
314
+ proxy_set_header Connection "";
315
+ }
316
+ }
317
+ }
318
+ EOL
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # 7. Render. UPSTREAM_API_KEY is DELIBERATELY excluded from the whitelist.
322
+ # ---------------------------------------------------------------------------
323
+ ( umask 077 && \
324
+ envsubst '$LISTEN_PORT $UPSTREAM_URL' < "$TEMPLATE_FILE" > /tmp/nginx.conf )
325
+ install -m 644 -o root -g root /tmp/nginx.conf "$INSTALL_PATH/nginx.conf"
326
+
327
+ rm -f "$TEMPLATE_FILE" /tmp/nginx.conf
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # 8. Validate and start
331
+ # ---------------------------------------------------------------------------
332
+ "$INSTALL_PATH/sbin/nginx" -t
333
+
334
+ if [[ "$UPSTREAM_API_KEY" == "$DEFAULT_API_KEY" ]]; then
335
+ warn "Still using placeholder key; configure Vault, AWS, or a local source."
336
+ else
337
+ log "Key resolved and stored only in $RUNTIME_ENV (chmod 600)."
338
+ log "nginx.conf contains no secret and is world-readable (644) on purpose."
339
+ fi
340
+
341
+ log "NGINX install complete (key kept off disk in nginx.conf)."