Claude
Skills
Sign in
Back

gatekeeper-nginx-setup

Included with Lifetime
$97 forever

Use this skill when the user asks to set up nginx in front of a ByteForge Gatekeeper deployment, configure the gatekeeper vhost, fix Cloudflare Error 1000 / `:3000` redirect leaks for a Gatekeeper site, or wire `auth_request` CORS for a service protected by Gatekeeper's `/authz`. Generates a drop-in nginx vhost (HTTP→HTTPS, `/api/*` → Flask backend, `/` → Next.js frontend) with the four traps documented inline so they don't get "simplified" away, plus optional `/authz` + `/health` + `/metrics` and an optional CORS-via-auth_request configuration for protected upstream services.

Web Dev

What this skill does


# Gatekeeper nginx vhost setup

This skill produces a complete nginx vhost for a ByteForge Gatekeeper deployment running behind Cloudflare:

- Flask backend container (gunicorn on port 7843)
- Next.js console frontend container (port 3000)
- Cloudflare Origin certificate already installed on the host

It also covers the optional bits that come up in real deployments — root-level `/authz`/`/health`/`/metrics` exposure, and CORS wiring when **another** vhost on this host (or another host) protects its upstream with `auth_request` against Gatekeeper's `/authz`.

The vhost shape, header set, and resolver trick are not arbitrary. Each load-bearing decision is documented in **Traps** below — read that section before editing anything the skill emits.

## When to Use This Skill

- Bringing up a new Cloudflare-fronted Gatekeeper deployment on a fresh host
- Migrating a Gatekeeper deployment to a new domain
- Debugging `307 → https://<DOMAIN>:3000/<locale>` redirect leaks (the i18n trap)
- Debugging Cloudflare Error 1000 (`DNS points to prohibited IP`) on `/api/*` calls (the catch-all trap)
- Adding `auth_request` CORS to a protected upstream service that delegates authz to a Gatekeeper instance
- Auditing an existing Gatekeeper vhost against the documented traps

## What This Skill Creates

1. **An nginx server block** — HTTP→HTTPS redirect, TLS 443 listener with Cloudflare Origin cert, `/api/*` proxy to backend, `/` proxy to frontend with the i18n-safe forwarded headers
2. **A `resolver` check + directive snippet** — needed because the vhost uses `set $var <container>; proxy_pass http://$var:port;` to defer DNS to request time
3. **(Optional) Root-level endpoints** — `/authz`, `/health`, `/metrics` if external systems need them through this vhost
4. **(Optional) `auth_request` + CORS configuration** — for a separate vhost that delegates authz to this Gatekeeper deployment
5. **Smoke-test commands** — four `curl`s that prove every code path works
6. **A failure-decoder table** — symptom → likely cause → fix, mapped to the trap numbers

## Step 1: Gather Information

Ask the user these questions before generating any config. Substitute the answers consistently throughout.

1. **"What is the public domain for this Gatekeeper deployment?"** (e.g., `gatekeeper.example.com`)
   - Stored as `<DOMAIN>`

2. **"What is the Docker service name of the Flask/gunicorn backend container?"** (e.g., `gatekeeper-backend`)
   - Stored as `<BACKEND_CONTAINER>`
   - Must be reachable by hostname on the docker network nginx is attached to

3. **"What is the Docker service name of the Next.js frontend container?"** (e.g., `gatekeeper-frontend`)
   - Stored as `<FRONTEND_CONTAINER>`
   - Must be reachable by hostname on the docker network nginx is attached to

4. **"Where do the Cloudflare Origin certificate files live for this host?"** (default: `/etc/nginx/ssl/<DOMAIN>.crt` and `.key`)
   - Adjust to the host's nginx convention if different

5. **"Is nginx running on the host directly, or as a container on the same docker network as the Gatekeeper containers?"**
   - Determines which `resolver` IP to use:
     - **Container nginx on a docker user network** → `127.0.0.11` (docker's embedded resolver)
     - **Host nginx using systemd-resolved** → `127.0.0.53`
     - **Host nginx using upstream DNS** → whatever the host actually uses

6. **"Do any external systems (load balancers, monitoring, sibling vhosts) need to reach `/authz`, `/health`, or `/metrics` on this domain?"**
   - If **no**, skip Step 4
   - If **yes**, ask which paths and why — `/authz` in particular often wants `internal;`

7. **"Are there any other services on this host (or another host) that delegate authorization to this Gatekeeper instance via `auth_request /authz`?"**
   - If **yes**, walk Step 5 (CORS via auth_request)
   - If **no** or unsure, skip Step 5

## Step 2: Verify a `resolver` directive exists

The vhost emitted in Step 3 uses `set $var <container>; proxy_pass http://$var:port;` deliberately, so nginx defers DNS resolution to **request time** instead of config-load time. That makes the vhost survive container restarts — but it requires a `resolver` directive somewhere included into the http block.

Run this on the host where nginx config lives:

```bash
grep -r "^[[:space:]]*resolver " /etc/nginx/ 2>/dev/null
```

(Adjust the path if nginx config lives in a docker volume or a non-standard location.)

- **A `resolver` line is present** → done, proceed to Step 3.
- **No `resolver` line** → add this once at the top of `nginx.conf`'s `http {}` block (or include a small file that does):

```nginx
# Container nginx on a docker user network — use docker's embedded resolver.
# valid= sets the cache TTL.
resolver 127.0.0.11 valid=10s ipv6=off;

# Host nginx with systemd-resolved:
# resolver 127.0.0.53 valid=10s ipv6=off;
```

Without this, the variable-form `proxy_pass` fails silently with "no resolver defined" and `502 Bad Gateway` at request time. This is **Trap 4** — do not skip it.

## Step 3: Generate the Drop-in vhost

Substitute `<DOMAIN>`, `<BACKEND_CONTAINER>`, `<FRONTEND_CONTAINER>` with the values from Step 1.

```nginx
# HTTP -> HTTPS redirect
server {
    listen 80;
    server_name <DOMAIN>;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name <DOMAIN>;

    ssl_certificate     /etc/nginx/ssl/<DOMAIN>.crt;
    ssl_certificate_key /etc/nginx/ssl/<DOMAIN>.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    access_log /var/log/nginx/<DOMAIN>_access.log;
    error_log  /var/log/nginx/<DOMAIN>_error.log;

    # All /api/* (admin, auth, config, webhooks/aegis, ...) goes to the
    # gatekeeper backend. The frontend defines no /api/* routes — routing
    # an unmapped /api/* to Next.js will server-side-fetch the same origin
    # and loop through Cloudflare, producing Error 1000. See Trap 1.
    location /api/ {
        set $gk_backend <BACKEND_CONTAINER>;
        proxy_pass http://$gk_backend:7843;
        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;
        # 30s, not 10s: the /api/auth/* proxy makes synchronous outbound
        # calls to Aegis, which can occasionally take longer under cold
        # start or upstream DB pressure. The gatekeeper itself responds
        # in single-digit ms; the headroom is for Aegis.
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }

    location / {
        set $gk_frontend <FRONTEND_CONTAINER>;
        proxy_pass http://$gk_frontend:3000;
        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;
        # X-Forwarded-Host + X-Forwarded-Port are required: without them,
        # Next.js 16's i18n middleware bakes the UPSTREAM port (3000) into
        # the Location header on its locale redirect, and browsers then
        # try to follow https://<DOMAIN>:3000/en, which fails. See Trap 2.
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

Drop this into the host's nginx layout — typically one of:

- `/etc/nginx/conf.d/<DOMAIN>.conf`
- `/etc/nginx/sites-available/<DOMAIN>` (then `ln -

Related in Web Dev