Skip to main content

DEPLOY WITH HEADLESS OAUTH

This guide shows you how to deploy swamp serve with OAuth authentication in an environment where no browser is available — Kubernetes pods, CI runners, or systemd services.

Prerequisites

  • A collective with owner or admin role
  • swamp auth login completed (to create the token)

1. Create a collective API token

Create a token with both oauth:manage and serve:* scopes:

swamp auth token create --collective myorg \
  --scopes oauth:manage,serve:* \
  --name headless-serve

The output shows the token's key and a signin token. Copy both immediately — they are shown only once, and a lost signin token can only be replaced by creating a new collective token.

oauth:manage allows the server to register itself as an OAuth client. serve:* allows instance registration and heartbeats.

2. Set the token in your deployment environment

Store the key as SWAMP_API_KEY and the signin token as SWAMP_SIGNIN_TOKEN in your deployment's secret management. Every Swamp process needs a credential to start; the signin token lets swamp serve start while swamp-club.com is unreachable — see Swamp Account Requirement.

Kubernetes Secret:

kubectl create secret generic swamp-serve \
  --from-literal=SWAMP_API_KEY=swamp_org_abc123... \
  --from-literal=SWAMP_SIGNIN_TOKEN=eyJmcHIiOi...

Reference both in your pod spec:

env:
  - name: SWAMP_API_KEY
    valueFrom:
      secretKeyRef:
        name: swamp-serve
        key: SWAMP_API_KEY
  - name: SWAMP_SIGNIN_TOKEN
    valueFrom:
      secretKeyRef:
        name: swamp-serve
        key: SWAMP_SIGNIN_TOKEN

systemd:

# /etc/swamp-serve.env
SWAMP_API_KEY=swamp_org_abc123...
SWAMP_SIGNIN_TOKEN=eyJmcHIiOi...
# In the [Service] section of the unit file
EnvironmentFile=/etc/swamp-serve.env

Shell (CI or scripts):

export SWAMP_API_KEY=swamp_org_abc123...
export SWAMP_SIGNIN_TOKEN=eyJmcHIiOi...

Read the key from a mounted file

To keep the key out of the process environment, mount it as a file and pass the path with --club-api-key-file. In Kubernetes, mount the secret as a volume:

containers:
  - name: swamp-serve
    args:
      - serve
      - --auth-mode=oauth
      - --club-api-key-file=/run/secrets/swamp/SWAMP_API_KEY
      - --admins=swampadmin
      - --allowed-collectives=myorg
    env:
      - name: SWAMP_SIGNIN_TOKEN
        valueFrom:
          secretKeyRef:
            name: swamp-serve
            key: SWAMP_SIGNIN_TOKEN
    volumeMounts:
      - name: swamp-api-key
        mountPath: /run/secrets/swamp
        readOnly: true
volumes:
  - name: swamp-api-key
    secret:
      secretName: swamp-serve
      items:
        - key: SWAMP_API_KEY
          path: SWAMP_API_KEY

Setting SWAMP_API_KEY_FILE to the path works too. --club-api-key-file takes precedence over both environment variables; setting SWAMP_API_KEY and SWAMP_API_KEY_FILE together is an error. Whitespace around the key in the file is ignored.

For a daemon, pass the flag to swamp serve daemon enable. The service definition stores only the file's path.

3. Start the server

swamp serve --auth-mode oauth \
  --admins swampadmin \
  --allowed-collectives myorg

With SWAMP_API_KEY set, the server registers its OAuth client and resolves admin usernames without any browser interaction. No verification URL is shown, no code needs to be entered.

Setting a stable client name

In container and Kubernetes environments, the default client name (swamp-serve-{repoName}-{hostname}) includes an ephemeral hostname that changes on every pod restart. Use --oauth-client-name or SWAMP_OAUTH_CLIENT_NAME to set a stable, deployment-meaningful name:

swamp serve --auth-mode oauth \
  --oauth-client-name prod-api-server \
  --admins swampadmin \
  --allowed-collectives myorg

Or via environment variable in a pod spec:

env:
  - name: SWAMP_OAUTH_CLIENT_NAME
    value: prod-api-server

4. Verify the bootstrap succeeded

Check the server logs for these messages in order:

SWAMP_API_KEY verified with oauth:manage scope — registering OAuth client (headless)
Registered OAuth client <clientId> via SWAMP_API_KEY (headless)
Using SWAMP_API_KEY to resolve admin/allowed-user usernames: swampadmin
Resolved admin swampadmin to user:<sub>

Confirm the server is ready:

curl -s https://your-server:9090/ready | jq .
{ "ready": true, "instanceId": "a1b2c3d4" }

Subsequent starts

After the first boot, OAuth client credentials and admin mappings are cached in the vault. Subsequent starts use the cached values — SWAMP_API_KEY is still read (for instance registration) but the OAuth registration step is skipped:

No stored OAuth client credentials found — first-time setup required

This message appears only on the first boot. On subsequent starts, the server loads credentials from the vault silently.

Key rotation

SWAMP_API_KEY is read fresh from the environment on each boot. To rotate the key:

  1. Create a new token with the same scopes
  2. Update the secret in your deployment environment
  3. Restart the server

The server picks up the new key on restart. A key read from --club-api-key-file or SWAMP_API_KEY_FILE follows the same rule: replacing the file's contents takes effect on the next restart. No vault changes or OAuth re-registration is needed — the stored client credentials remain valid.

Troubleshooting

Missing oauth:manage scope:

SWAMP_API_KEY is missing the "oauth:manage" scope required for headless OAuth
client registration. Generate a new collective API token that includes
"oauth:manage".

Create a new token with both oauth:manage and serve:* scopes.

Admin username not found:

Failed to resolve admin 'swampadmin': <message>. Ensure the username exists on
https://swamp-club.com.

Verify the username in --admins exists on swamp-club. The server resolves usernames to principal IDs at startup.

Key validation failed:

SWAMP_API_KEY validation failed: <status> <statusText>

Check that the token has not been revoked or expired. Verify with swamp auth whoami.

Key file not found:

Error: --club-api-key-file file not found: /run/secrets/swamp/SWAMP_API_KEY

The error names the source and the path it read. Check the volume mount and the path passed to --club-api-key-file or set in SWAMP_API_KEY_FILE. An empty or unreadable file fails the same way, with file is empty or file not readable.