---
sidebar_position: 4
title: Auth proxy
---

# Auth proxy

Most self-hosted apps either ship with weak default credentials (`admin/admin` Grafana) or no app-level auth at all (n8n's free tier, raw webhook receivers). The auth proxy is a per-instance opt-in **Caddy sidecar** that fronts the ALB and validates a credential before letting the request reach the app.

It's not a replacement for the app's own login system — it's a pre-filter. The app keeps its own session model; the proxy just makes sure the request never gets there without a valid credential.

## Architecture

```
without auth:
  client → ALB → task:<appPort>

with --auth-type basic:
  client → ALB → task:8888 (caddy)
                       │  basic-auth check (bcrypt hash)
                       ↓
                  localhost:<appPort> (app)

with --auth-type token:
  client → ALB → task:8888 (caddy)
                       │  Authorization: Bearer <token>
                       ↓
                  localhost:<appPort> (app)
```

The Caddy image is `caddy:2-alpine` direct (no custom ECR build). The entrypoint is an inline `sh -c` script that templates the Caddyfile from env at boot. The app keeps listening on its own internal port (unchanged); ALB just routes to Caddy instead.

Cost: zero. Same Fargate task, +1 small container (~30 MB RAM, ~0 CPU).

## Storage

- **Basic auth.** Password is bcrypt-hashed at the backend; the hash lives in the encrypted env bag. Plaintext is never persisted server-side.
- **Token auth.** Token is KMS-encrypted at rest. Backend keeps the last 4 characters for audit (so `zibby app status` can show `Auth: token (…a3f9)` without leaking the secret). If the customer doesn't supply a token, the backend generates a 32-char URL-safe random one and returns it ONCE in the deploy response.

You can rotate either at any time without redeploying the app (see `set-auth` below) — only the Caddy container restarts.

## Deploying with auth

### Basic auth

```bash
zibby app deploy grafana \
  --project <project-id> \
  --name metrics \
  --auth-type basic \
  --auth-user admin \
  --auth-password 'S0me-long-passphrase!'
```

`--auth-user` is printable ASCII (no spaces), 1-64 chars. `--auth-password` is 8-256 chars and can come from `ZIBBY_APP_AUTH_PASSWORD` env to keep it out of shell history.

Verify:

```bash
curl -I https://a1b2c3d4.apps.zibby.dev
# HTTP/2 401  ← Caddy bounces unauthenticated request

curl -I -u 'admin:S0me-long-passphrase!' https://a1b2c3d4.apps.zibby.dev
# HTTP/2 302  ← Grafana redirects to /login (the app's own auth, behind the proxy)
```

### Token auth (auto-generated)

```bash
zibby app deploy gotify --project <id> --name notify --auth-type token
```

Response includes:

```
✔ Deployed (instanceId: f1e2d3c4)
→ Public URL: https://f1e2d3c4.apps.zibby.dev
→ Auth token: 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC
                 ^^^ shown ONCE — save it now, you can't retrieve it
```

Verify:

```bash
curl -I https://f1e2d3c4.apps.zibby.dev
# HTTP/2 401

curl -I -H 'Authorization: Bearer 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC' \
  https://f1e2d3c4.apps.zibby.dev
# HTTP/2 200
```

### Token auth (caller-supplied)

If you already have a token (e.g. minted by your own auth system), pass it explicitly:

```bash
zibby app deploy gotify \
  --project <id> --name notify \
  --auth-type token \
  --auth-token "$(cat ~/.secrets/gotify-bearer.txt)"
```

Also accepts `ZIBBY_APP_AUTH_TOKEN` env.

### No auth (the default)

```bash
zibby app deploy grafana --project <id> --name metrics
# No Caddy container; ALB routes straight to the app's port.
```

## Changing auth after deploy

`zibby app set-auth <instanceId>` is the rotate/replace endpoint. It has the same auth flags as `deploy`, with PATCH semantics — omitted flags preserve current state.

### Rotate just the password

```bash
zibby app set-auth a1b2c3d4 --auth-password 'N3w-passphrase-2026!'
```

Only the password changes — auth type and username are preserved. The ECS task rolls (~60-90s) to pick up the new bcrypt hash; the app container keeps its data.

### Switch from basic to token

```bash
zibby app set-auth a1b2c3d4 --auth-type token --auth-token 'mY-shared-bearer-token'
```

### Switch from no-auth to basic

```bash
zibby app set-auth a1b2c3d4 \
  --auth-type basic \
  --auth-user admin \
  --auth-password 'S0me-long-passphrase!'
```

This is the path most operators take: deploy quickly without auth, verify the app works, then put Caddy in front before exposing it to the world.

### Remove auth entirely

```bash
zibby app set-auth a1b2c3d4 --off
```

The Caddy container is stripped from the next task definition revision; the ALB target group flips back to the app's port. ~60-90s rolling replace.

## When to use which

- **Basic auth** — humans hitting a dashboard (Grafana, Uptime Kuma, draw.io, OpenObserve). One shared credential the team knows.
- **Token auth** — machines hitting an endpoint (webhook receivers, internal APIs, anything you're going to put in another tool's "header to send" box). Long random string, rotated on schedule.
- **No auth** — only when the app has its own SSO already wired up (Authentik, Zitadel) and you actively want the open URL.

The proxy doesn't replace per-user accounts inside the app — for that you still need the app's own auth (e.g. Grafana's user database, n8n's user table). Treat the proxy as a perimeter, not an identity system.

→ Next: [Goal-mode deploys](./goal-mode) or [Agent operator](./agent-ops)
