feat: add header key auth as alternative to IP allowlist
Build Admin / Build image (push) Successful in 43s
Build Admin / Build image (push) Successful in 43s
This commit is contained in:
4 files changed
+48
-7
No files matched your search
@@ -12,3 +12,9 @@ PROXY_SECRET=
|
||||
# Allowed source CIDRs (comma-separated), leave empty to allow all (dev mode)
|
||||
# Example: "10.0.0.0/8,172.16.0.0/12"
|
||||
ALLOWED_CIDR=
|
||||
|
||||
# API key header: a request carrying this header bypasses the IP allowlist (IP OR key).
|
||||
# Set both or neither. Name: [A-Za-z0-9-], value: [A-Za-z0-9._~+/=-]. Only matters if ALLOWED_CIDR is set.
|
||||
# Generate value with: openssl rand -hex 32
|
||||
AUTH_HEADER_NAME=
|
||||
AUTH_HEADER_VALUE=
|
||||
@@ -10,6 +10,7 @@
|
||||
# --- Access control (generated by docker-entrypoint.sh from env) ---
|
||||
# matchers.caddy: @denied_ip / @bad_token definitions (empty when disabled)
|
||||
# guard.caddy: matching respond directives (empty when disabled)
|
||||
# scrub_extra.caddy: strips the optional auth key header from upstream requests
|
||||
|
||||
# --- Shared upstream request hygiene ---
|
||||
(scrub) {
|
||||
@@ -24,6 +25,7 @@
|
||||
header_up -Forwarded
|
||||
header_up -Via
|
||||
header_up -X-Proxy-Token
|
||||
import /etc/caddy/scrub_extra.caddy
|
||||
}
|
||||
|
||||
# (api) <prefix> <upstream-host> <upstream-path-prefix>
|
||||
|
||||
@@ -37,6 +37,8 @@ DOMAIN=proxy.bildme.ru
|
||||
ACME_EMAIL=you@example.com
|
||||
PROXY_SECRET=<openssl rand -hex 32>
|
||||
ALLOWED_CIDR=10.0.0.0/8,172.16.0.0/12
|
||||
AUTH_HEADER_NAME=X-Api-Key # optional: access from any IP by header
|
||||
AUTH_HEADER_VALUE=<openssl rand -hex 32>
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -207,7 +209,7 @@ option.WithHeader("X-Proxy-Token", os.Getenv("PROXY_SECRET")),
|
||||
|
||||
### `403 {"error":"ip_not_allowed"}`
|
||||
|
||||
The caller's source IP is not in `ALLOWED_CIDR`. Check the effective list:
|
||||
The caller's source IP is not in `ALLOWED_CIDR` and the request has no valid `AUTH_HEADER_NAME: AUTH_HEADER_VALUE` header. Check the effective list:
|
||||
|
||||
```bash
|
||||
docker exec api-proxy cat /etc/caddy/matchers.caddy
|
||||
@@ -240,8 +242,8 @@ docker inspect --format='{{.State.Health.Status}}' api-proxy
|
||||
## How It Works
|
||||
|
||||
1. A k8s pod sends a request to `https://proxy.bildme.ru/elevenlabs/v1/text-to-speech/...` (or `http://<vm-ip>:8082/...`) with the `X-Proxy-Token` header and the original API key.
|
||||
2. Caddy checks the source IP against `ALLOWED_CIDR` and validates `X-Proxy-Token` — rejecting with 403 if either fails. `/health` is answered before both guards.
|
||||
2. Caddy lets the request through if the source IP is in `ALLOWED_CIDR` **or** it carries the `AUTH_HEADER_NAME: AUTH_HEADER_VALUE` header, then validates `X-Proxy-Token` (if `PROXY_SECRET` is set) — rejecting with 403 on failure. `/health` is answered before both guards.
|
||||
3. `handle_path` strips the prefix (`/elevenlabs/`, `/openai/`, `/telegram/`, `/openrouter/`, `/deepseek/`) and the request is forwarded over HTTPS with the upstream host as both `Host` header and TLS SNI.
|
||||
4. `X-Proxy-Token` and all IP-leaking headers (`X-Forwarded-*`, `X-Real-IP`, `True-Client-IP`, `CF-Connecting-IP`, `X-Client-IP`, `Forwarded`, `Via`) are removed before forwarding; API keys pass through unchanged.
|
||||
4. `X-Proxy-Token`, the `AUTH_HEADER_NAME` header and all IP-leaking headers (`X-Forwarded-*`, `X-Real-IP`, `True-Client-IP`, `CF-Connecting-IP`, `X-Client-IP`, `Forwarded`, `Via`) are removed before forwarding; API keys pass through unchanged.
|
||||
5. Responses stream back unbuffered (`flush_interval -1`) — critical for binary audio, SSE token streams and Telegram long-polling.
|
||||
6. Access control fragments (`matchers.caddy`, `guard.caddy`) are generated from environment variables at container start by `docker-entrypoint.sh`.
|
||||
+35
-4
@@ -6,9 +6,11 @@ set -e
|
||||
CONF_DIR="/etc/caddy"
|
||||
MATCHERS="${CONF_DIR}/matchers.caddy"
|
||||
GUARD="${CONF_DIR}/guard.caddy"
|
||||
SCRUB_EXTRA="${CONF_DIR}/scrub_extra.caddy"
|
||||
|
||||
: > "${MATCHERS}"
|
||||
: > "${GUARD}"
|
||||
: > "${SCRUB_EXTRA}"
|
||||
|
||||
# --- 0. Required settings ---
|
||||
if [ -z "${DOMAIN}" ]; then
|
||||
@@ -20,13 +22,42 @@ if [ -z "${ACME_EMAIL}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. IP allowlist ---
|
||||
# --- 1. API key header (optional bypass of the IP allowlist) ---
|
||||
KEY_MATCH=""
|
||||
if [ -n "${AUTH_HEADER_NAME}" ] || [ -n "${AUTH_HEADER_VALUE}" ]; then
|
||||
if [ -z "${AUTH_HEADER_NAME}" ] || [ -z "${AUTH_HEADER_VALUE}" ]; then
|
||||
echo "[entrypoint] FATAL: AUTH_HEADER_NAME and AUTH_HEADER_VALUE must be set together" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "${AUTH_HEADER_NAME}" in
|
||||
*[!A-Za-z0-9-]*)
|
||||
echo "[entrypoint] FATAL: AUTH_HEADER_NAME may contain only [A-Za-z0-9-]" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
case "${AUTH_HEADER_VALUE}" in
|
||||
*[!A-Za-z0-9._~+/=-]*)
|
||||
echo "[entrypoint] FATAL: AUTH_HEADER_VALUE may contain only [A-Za-z0-9._~+/=-]" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
KEY_MATCH="not header ${AUTH_HEADER_NAME} \"${AUTH_HEADER_VALUE}\""
|
||||
echo "# strip the key header before it reaches upstreams" > "${SCRUB_EXTRA}"
|
||||
echo "header_up -${AUTH_HEADER_NAME}" >> "${SCRUB_EXTRA}"
|
||||
echo "[entrypoint] Header key auth configured (header: ${AUTH_HEADER_NAME})"
|
||||
fi
|
||||
|
||||
# --- 2. IP allowlist (a valid key header also grants access) ---
|
||||
if [ -z "${ALLOWED_CIDR}" ]; then
|
||||
echo "[entrypoint] ALLOWED_CIDR is empty — allowing all IPs (dev mode)"
|
||||
if [ -n "${KEY_MATCH}" ]; then
|
||||
echo "[entrypoint] WARNING: header key has no effect while ALLOWED_CIDR is empty" >&2
|
||||
fi
|
||||
else
|
||||
CIDR_LIST=$(echo "${ALLOWED_CIDR}" | tr ',' ' ' | tr -s ' ')
|
||||
cat >> "${MATCHERS}" <<MATCH
|
||||
@denied_ip not remote_ip ${CIDR_LIST}
|
||||
@denied_ip {
|
||||
not remote_ip ${CIDR_LIST}
|
||||
${KEY_MATCH}
|
||||
}
|
||||
MATCH
|
||||
cat >> "${GUARD}" <<GUARDRULE
|
||||
respond @denied_ip \`{"error":"ip_not_allowed"}\` 403 {
|
||||
@@ -36,7 +67,7 @@ GUARDRULE
|
||||
echo "[entrypoint] IP allowlist configured: ${CIDR_LIST}"
|
||||
fi
|
||||
|
||||
# --- 2. Token auth ---
|
||||
# --- 3. Token auth (legacy, applied in addition to the checks above) ---
|
||||
if [ -z "${PROXY_SECRET}" ]; then
|
||||
echo "[entrypoint] PROXY_SECRET is not set — token auth disabled (open mode)"
|
||||
else
|
||||
@@ -51,7 +82,7 @@ GUARDRULE
|
||||
echo "[entrypoint] Token auth configured"
|
||||
fi
|
||||
|
||||
# --- 3. Validate and start ---
|
||||
# --- 4. Validate and start ---
|
||||
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
echo "[entrypoint] Starting caddy (domain: ${DOMAIN})..."
|
||||
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
Reference in new issue
Block a user