v2.31.0-dev23 58a92f3

upgrading.md RawHistoryFolder

Upgrading Gordon

This guide covers breaking changes and migration steps between major versions.

Route-Domain Validation

Route keys must be plain hostnames. Use inline tables like "app.example.com" = { image = "myapp:latest" }. Gordon still reads legacy http://... route entries for backward compatibility and rewrites them on the next save. Update [routes], CLI commands, and automation that reference the old values.

Next major: Unified smart TCP entrypoints

Breaking: server.port / server.tls_port no longer define public listeners

Public application traffic is configured entrypoint-first. Migrate old HTTP/HTTPS listener settings to a smart TCP edge entrypoint.

Before:

[server]
port = 8088
tls_port = 8443
registry_port = 5000
gordon_domain = "gordon.example.com"

After:

[server]
registry_port = 5000
gordon_domain = "gordon.example.com"

[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"

edge is the conventional route default, but it has no built-in port. address = ":443", address = ":9000", firewall forwarding, and container mappings such as -p 443:9000 are deployment choices, not protocol modes. The smart_tcp protocol accepts TCP, sniffs HTTP/h2c/TLS, routes TLS passthrough by SNI when configured, and otherwise uses normal Gordon HTTPS fallback.

If you run rootless and cannot bind 443 directly, bind a high port and map external ports to it:

[entrypoints.edge]
address = ":9000"
protocol = "smart_tcp"
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=9000
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
sudo firewall-cmd --reload

Keep server.registry_port for Docker/Podman push and pull traffic; it is separate from public app entrypoints.

ACME migration notes

  • DNS-01 (cloudflare-dns-01) does not require a special external port 80 edge.
  • HTTP-01 requires an HTTP-capable smart_tcp entrypoint reachable on external port 80 for each hostname being validated.
  • TLS-ALPN-01 is unsupported.
  • Normal HTTPS fallback certificate priority is static certificates, then public ACME certificates, then Gordon's internal CA.

v2.30.0 to v2.31.0

Breaking: Default Port Changes

Gordon now defaults to unprivileged ports that work without root or firewall rules:

Setting Old default New default
server.port 80 8088
server.registry_port 5000 5000 (unchanged)
server.tls_port (n/a) 8443

If you relied on the old defaults (no explicit port in your config), add the port explicitly:

[server]
port = 80

Or set up firewall port forwarding to the new defaults:

sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
sudo firewall-cmd --reload

New: Internal Certificate Authority

Gordon now includes an internal CA for automatic on-demand TLS. Set tls_port = 0 to disable. See Server Configuration for details.

Required for Cloudflare/Proxy Setups: proxy_allowed_ips

The internal CA's HTTP onboarding gate rejects non-localhost HTTP requests by default. If Gordon sits behind Cloudflare or another reverse proxy, add the proxy's edge IPs to proxy_allowed_ips:

[server]
proxy_allowed_ips = [
  "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
  "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
  "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
  "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
  "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
]

Without this, all proxied HTTP traffic returns 403 Forbidden. Set tls_port = 0 to disable the internal CA and skip this requirement.

Breaking: server.gordon_domain Replaces server.registry_domain

Gordon now uses server.gordon_domain as the public registry and admin host. Migrate older configs that still set only server.registry_domain before restarting:

Before:

[server]
registry_domain = "gordon.example.com"

After:

[server]
gordon_domain = "gordon.example.com"

If you do not migrate, gordon status --remote ... and gordon routes list --remote ... can fail with /auth/token 404, and reg-domain/v2/ or /admin/status can return 404.

Staged Registry Host Rename

If you cannot rename the Gordon registry host in one step, keep the new host in server.gordon_domain and list the old Gordon registry hosts in server.legacy_registry_domains during the cutover:

[server]
gordon_domain = "gordon.example.com"
legacy_registry_domains = [
  "registry.example.com",
  "registry.example.com:5000",
]

Recommended rollout:

  1. Set gordon_domain to the new host.
  2. Add every old Gordon registry host that clients still use to legacy_registry_domains.
  3. Restart Gordon.
  4. Move Docker/Podman logins, pushes, pulls, and image references to the new host.
  5. Remove legacy_registry_domains after every client has moved.

During the transition, Gordon treats both the current and legacy hosts as its own registry for image matching and internal pulls, then saves canonical refs back to gordon_domain. Remote CLI and admin API traffic should use the new gordon_domain.

Breaking: gordon rollback Renamed to gordon pin

If you have scripts or runbooks using gordon rollback, switch them to gordon pin:

gordon pin app.example.com
gordon pin app.example.com --tag v2.30.1
gordon pin list app.example.com

Breaking-ish: routes list Is Inventory-Only

gordon routes list now shows domain/image inventory only. Detailed runtime state, HTTP probe status, and attachment status moved to gordon routes status.

If you have automation parsing status-like fields from routes list, migrate it to routes status --json.

New: Public ACME TLS

Gordon can now obtain public certificates with [tls.acme] using either http-01 or cloudflare-dns-01.

  • Normal HTTPS fallback must be available on a TLS-capable entrypoint
  • http-01 needs an HTTP-capable smart TCP entrypoint reachable on external port 80 for each hostname being validated
  • cloudflare-dns-01 needs a Cloudflare token with zone-read and DNS-edit access for every zone used by HTTPS routes and does not require a special port-80 edge
  • obtain_batch_size limits new ACME orders per reconcile pass to reduce rate-limit spikes

New: DNS Resolver Settings for ACME DNS-01

[dns] controls the recursive resolvers Gordon uses to verify public DNS propagation for ACME DNS-01.

Add this if you need non-default resolvers, split-horizon-aware testing, or a slower propagation window:

[dns]
resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
propagation_timeout = "5m"
polling_interval = "5s"

Runtime Change: Managed Containers Use Restart Policy always

Newly created managed route containers now use the runtime restart policy always. Existing containers pick this up on redeploy/recreate.

This improves crash recovery and keeps Docker/Podman behavior aligned with Gordon's startup reconciliation.

New: Saved Remote Inference for Targeted Commands

When you do not pass --remote, Gordon can now infer a saved remote for some target-based commands when exactly one configured remote matches the route, image, attachment target, or repository.

If multiple remotes match or probing fails, Gordon now stops and asks you to pass --remote explicitly instead of guessing.

New: Dedicated Access Log

You can now enable logging.access_log.* for a dedicated HTTP access log separate from the main process log. This is useful for request auditing, GoAccess, CrowdSec, or fail2ban-style tooling.

v2.16.0 to v2.30.0

Breaking: Password Authentication Removed

Gordon v2.30.0 removes password-based authentication entirely. Only token-based authentication is supported.

What changed:

  • auth.type = "password" is no longer accepted
  • auth.password and auth.password_hash config fields are removed
  • The gordon auth password hash CLI command is removed
  • The gordon auth login command now requires --token (no more interactive password prompt)
  • The /auth/password endpoint now returns 410 Gone
  • Long-lived tokens are no longer accepted on admin/registry endpoints — they must be exchanged for ephemeral tokens via /auth/token

New features:

  • auth.access_token_ttl configures the lifetime of ephemeral access tokens issued by /auth/token (default: "15m")
  • gordon auth show-token prints the stored token for a remote
  • gordon auth logout removes the stored token locally
  • Automatic token exchange: the CLI transparently exchanges long-lived tokens for ephemeral ones before API calls
  • Admin scopes (admin:*:*, admin:routes:read, etc.) allow fine-grained access control for remote CLI operations

Migration steps:

  1. Before upgrading, generate a token on your current Gordon instance:

    gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0
    

    Save this token securely. You will need it after upgrading. Admin scopes (admin:*:*) are only available after upgrading to v2.30.0 — regenerate your token with admin scopes after the upgrade if needed.

  2. Update remotes to use the generated token:

    gordon auth login --token <token>
    # or
    gordon remotes set-token prod <token>
    
  3. Update your config to remove password fields:

    Before (v2.16.0):

    [auth]
    enabled = true
    secrets_backend = "pass"
    username = "deploy"
    password_hash = "gordon/auth/password_hash"
    token_secret = "gordon/auth/token_secret"
    

    After (v2.30.0):

    [auth]
    enabled = true
    secrets_backend = "pass"
    token_secret = "gordon/auth/token_secret"
    access_token_ttl = "15m"
    
  4. Update CI/CD pipelines to use GORDON_TOKEN environment variable for authentication. See the deployment guides.

  5. Upgrade the binary and restart:

    curl -fsSL https://gordon.bnema.dev/install | bash
    systemctl --user restart gordon
    
  6. Verify the server starts without errors:

    journalctl --user -u gordon -f
    

New: Configurable Access Token TTL

Ephemeral access tokens issued by /auth/token now have a configurable lifetime via auth.access_token_ttl (default "15m", maximum "1h").

Tokens with a lifetime at or below MaxAccessTokenLifetime (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they cannot be individually revoked. They become invalid only when they naturally expire. Shortening auth.access_token_ttl only reduces the exposure window; it does not enable per-token revocation. If you need explicit revocation, use a stored long-lived token instead of /auth/token.

[auth]
access_token_ttl = "30m"

New: Admin Scopes

Tokens can now include fine-grained admin scopes for remote CLI operations:

# Full admin access
gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0

# Read-only monitoring
gordon auth token generate --subject monitor --scopes "admin:status:read" --expiry 30d

# CI deploy with route read + config write
gordon auth token generate --subject ci --scopes "push,pull,admin:routes:read,admin:config:write" --expiry 0

See Token Scopes for the full list.

v2.6.0 to v2.7.0

Breaking: Token Secret Required

Gordon v2.7.0 requires a token_secret for JWT authentication. The server will not start without it configured.

Error you'll see:

Error: token_secret is required for JWT token generation; set GORDON_AUTH_TOKEN_SECRET environment variable or configure auth.token_secret

Choose one of these migration options:

Option A: Environment Variable (simplest)

# Generate a random secret
export GORDON_AUTH_TOKEN_SECRET="$(openssl rand -base64 32)"

# Add to your shell profile or systemd service
echo 'export GORDON_AUTH_TOKEN_SECRET="your-secret"' >> ~/.bashrc

For systemd services:

mkdir -p ~/.config/systemd/user/gordon.service.d
cat > ~/.config/systemd/user/gordon.service.d/token.conf << EOF
[Service]
Environment="GORDON_AUTH_TOKEN_SECRET=$(openssl rand -base64 32)"
EOF
systemctl --user daemon-reload
systemctl --user restart gordon
# Generate and store secret in pass
openssl rand -base64 32 | pass insert -e gordon/auth/token_secret

# Update your gordon.toml
[auth]
enabled = true
secrets_backend = "pass"
token_secret = "gordon/auth/token_secret"

Option C: Config File with Unsafe Backend (development only)

# Generate secret file
mkdir -p ~/.gordon/secrets
openssl rand -base64 32 > ~/.gordon/secrets/token_secret
chmod 600 ~/.gordon/secrets/token_secret
[auth]
enabled = true
secrets_backend = "unsafe"
token_secret = "token_secret"

Breaking: Unsafe Token Store Warning

Using secrets_backend = "unsafe" now logs a warning on startup:

WRN using unsafe secrets backend - secrets are stored in plain text

This is intentional to encourage secure secret storage in production. The warning can be ignored for development.

New Features

  • Attachment secrets discovery: gordon secrets list <domain> now shows secrets for attachment containers
  • Auth login command: gordon auth login --remote <name> for token authentication
  • Rate limiting: Configurable rate limits under [api.rate_limit]

Security Improvements

v2.7.0 includes a comprehensive security audit with:

  • JWT tokens now include "not before" (nbf) claim
  • SSRF protection for external routes
  • Security headers middleware
  • Rate limiting on registry and token endpoints
  • Command injection prevention in pass provider
  • Path traversal prevention in secrets store

v2.5.0 to v2.6.0

Breaking: Config Restructure

The [secrets] and [registry_auth] sections were merged into [auth].

Before (v2.5.0):

[secrets]
backend = "pass"

[registry_auth]
enabled = true
type = "password"
password_hash = "gordon/registry/password_hash"

After (v2.6.0+):

[auth]
enabled = true
secrets_backend = "pass"
password_hash = "gordon/auth/password_hash"

Breaking: Auth Enabled by Default

Gordon keeps auth enabled by default. If you set auth.enabled = false, Gordon runs in local-only mode (/admin/* disabled, /v2/* loopback-only).

Breaking: Secret Paths Changed

If using pass or sops, update your secret paths:

  • gordon/registry/* → gordon/auth/*

General Upgrade Process

  1. Read the changelog for your target version
  2. Backup your config before upgrading
  3. Test in staging if possible
  4. Upgrade the binary:
    curl -fsSL https://gordon.bnema.dev/install | bash
    
  5. Restart Gordon:
    systemctl --user restart gordon
    
  6. Check logs for any errors:
    gordon logs
    

Getting Help

source
1# Upgrading Gordon
2
3This guide covers breaking changes and migration steps between major versions.
4
5## Route-Domain Validation
6
7Route keys must be plain hostnames. Use inline tables like `"app.example.com" = { image = "myapp:latest" }`. Gordon still reads legacy `http://...` route entries for backward compatibility and rewrites them on the next save. Update `[routes]`, CLI commands, and automation that reference the old values.
8
9## Next major: Unified smart TCP entrypoints
10
11### Breaking: `server.port` / `server.tls_port` no longer define public listeners
12
13Public application traffic is configured entrypoint-first. Migrate old HTTP/HTTPS listener settings to a smart TCP edge entrypoint.
14
15**Before:**
16
17```toml
18[server]
19port = 8088
20tls_port = 8443
21registry_port = 5000
22gordon_domain = "gordon.example.com"
23```
24
25**After:**
26
27```toml
28[server]
29registry_port = 5000
30gordon_domain = "gordon.example.com"
31
32[entrypoints.edge]
33address = ":443"
34protocol = "smart_tcp"
35```
36
37`edge` is the conventional route default, but it has no built-in port. `address = ":443"`, `address = ":9000"`, firewall forwarding, and container mappings such as `-p 443:9000` are deployment choices, not protocol modes. The `smart_tcp` protocol accepts TCP, sniffs HTTP/h2c/TLS, routes TLS passthrough by SNI when configured, and otherwise uses normal Gordon HTTPS fallback.
38
39If you run rootless and cannot bind 443 directly, bind a high port and map external ports to it:
40
41```toml
42[entrypoints.edge]
43address = ":9000"
44protocol = "smart_tcp"
45```
46
47```bash
48sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=9000
49sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
50sudo firewall-cmd --reload
51```
52
53Keep `server.registry_port` for Docker/Podman push and pull traffic; it is separate from public app entrypoints.
54
55### ACME migration notes
56
57- DNS-01 (`cloudflare-dns-01`) does not require a special external port 80 edge.
58- HTTP-01 requires an HTTP-capable `smart_tcp` entrypoint reachable on external port 80 for each hostname being validated.
59- TLS-ALPN-01 is unsupported.
60- Normal HTTPS fallback certificate priority is static certificates, then public ACME certificates, then Gordon's internal CA.
61
62## v2.30.0 to v2.31.0
63
64### Breaking: Default Port Changes
65
66Gordon now defaults to unprivileged ports that work without root or firewall rules:
67
68| Setting | Old default | New default |
69|---------|-------------|-------------|
70| `server.port` | `80` | `8088` |
71| `server.registry_port` | `5000` | `5000` (unchanged) |
72| `server.tls_port` | *(n/a)* | `8443` |
73
74**If you relied on the old defaults** (no explicit `port` in your config), add the port explicitly:
75
76```toml
77[server]
78port = 80
79```
80
81Or set up firewall port forwarding to the new defaults:
82
83```bash
84sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8088
85sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
86sudo firewall-cmd --reload
87```
88
89### New: Internal Certificate Authority
90
91Gordon now includes an internal CA for automatic on-demand TLS. Set `tls_port = 0` to disable. See [Server Configuration](./config/server.md#internal-ca-and-tls) for details.
92
93### Required for Cloudflare/Proxy Setups: `proxy_allowed_ips`
94
95The internal CA's HTTP onboarding gate rejects non-localhost HTTP requests by default. If Gordon sits behind Cloudflare or another reverse proxy, add the proxy's edge IPs to `proxy_allowed_ips`:
96
97```toml
98[server]
99proxy_allowed_ips = [
100  "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
101  "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
102  "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
103  "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
104  "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
105]
106```
107
108Without this, all proxied HTTP traffic returns `403 Forbidden`. Set `tls_port = 0` to disable the internal CA and skip this requirement.
109
110### Breaking: `server.gordon_domain` Replaces `server.registry_domain`
111
112Gordon now uses `server.gordon_domain` as the public registry and admin host. Migrate older configs that still set only `server.registry_domain` before restarting:
113
114**Before:**
115
116```toml
117[server]
118registry_domain = "gordon.example.com"
119```
120
121**After:**
122
123```toml
124[server]
125gordon_domain = "gordon.example.com"
126```
127
128If you do not migrate, `gordon status --remote ...` and `gordon routes list --remote ...` can fail with `/auth/token` `404`, and `reg-domain/v2/` or `/admin/status` can return `404`.
129
130### Staged Registry Host Rename
131
132If you cannot rename the Gordon registry host in one step, keep the new host in `server.gordon_domain` and list the old Gordon registry hosts in `server.legacy_registry_domains` during the cutover:
133
134```toml
135[server]
136gordon_domain = "gordon.example.com"
137legacy_registry_domains = [
138  "registry.example.com",
139  "registry.example.com:5000",
140]
141```
142
143Recommended rollout:
144
1451. Set `gordon_domain` to the new host.
1462. Add every old Gordon registry host that clients still use to `legacy_registry_domains`.
1473. Restart Gordon.
1484. Move Docker/Podman logins, pushes, pulls, and image references to the new host.
1495. Remove `legacy_registry_domains` after every client has moved.
150
151During the transition, Gordon treats both the current and legacy hosts as its own registry for image matching and internal pulls, then saves canonical refs back to `gordon_domain`. Remote CLI and admin API traffic should use the new `gordon_domain`.
152
153### Breaking: `gordon rollback` Renamed to `gordon pin`
154
155If you have scripts or runbooks using `gordon rollback`, switch them to `gordon pin`:
156
157```bash
158gordon pin app.example.com
159gordon pin app.example.com --tag v2.30.1
160gordon pin list app.example.com
161```
162
163### Breaking-ish: `routes list` Is Inventory-Only
164
165`gordon routes list` now shows domain/image inventory only. Detailed runtime state, HTTP probe status, and attachment status moved to `gordon routes status`.
166
167If you have automation parsing status-like fields from `routes list`, migrate it to `routes status --json`.
168
169### New: Public ACME TLS
170
171Gordon can now obtain public certificates with `[tls.acme]` using either `http-01` or `cloudflare-dns-01`.
172
173- Normal HTTPS fallback must be available on a TLS-capable entrypoint
174- `http-01` needs an HTTP-capable smart TCP entrypoint reachable on external port 80 for each hostname being validated
175- `cloudflare-dns-01` needs a Cloudflare token with zone-read and DNS-edit access for every zone used by HTTPS routes and does not require a special port-80 edge
176- `obtain_batch_size` limits new ACME orders per reconcile pass to reduce rate-limit spikes
177
178### New: DNS Resolver Settings for ACME DNS-01
179
180`[dns]` controls the recursive resolvers Gordon uses to verify public DNS propagation for ACME DNS-01.
181
182Add this if you need non-default resolvers, split-horizon-aware testing, or a slower propagation window:
183
184```toml
185[dns]
186resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
187propagation_timeout = "5m"
188polling_interval = "5s"
189```
190
191### Runtime Change: Managed Containers Use Restart Policy `always`
192
193Newly created managed route containers now use the runtime restart policy `always`. Existing containers pick this up on redeploy/recreate.
194
195This improves crash recovery and keeps Docker/Podman behavior aligned with Gordon's startup reconciliation.
196
197### New: Saved Remote Inference for Targeted Commands
198
199When you do not pass `--remote`, Gordon can now infer a saved remote for some target-based commands when exactly one configured remote matches the route, image, attachment target, or repository.
200
201If multiple remotes match or probing fails, Gordon now stops and asks you to pass `--remote` explicitly instead of guessing.
202
203### New: Dedicated Access Log
204
205You can now enable `logging.access_log.*` for a dedicated HTTP access log separate from the main process log. This is useful for request auditing, GoAccess, CrowdSec, or fail2ban-style tooling.
206
207## v2.16.0 to v2.30.0
208
209### Breaking: Password Authentication Removed
210
211Gordon v2.30.0 removes password-based authentication entirely. Only token-based authentication is supported.
212
213**What changed:**
214
215- `auth.type = "password"` is no longer accepted
216- `auth.password` and `auth.password_hash` config fields are removed
217- The `gordon auth password hash` CLI command is removed
218- The `gordon auth login` command now requires `--token` (no more interactive password prompt)
219- The `/auth/password` endpoint now returns `410 Gone`
220- Long-lived tokens are no longer accepted on admin/registry endpoints — they must be exchanged for ephemeral tokens via `/auth/token`
221
222**New features:**
223
224- `auth.access_token_ttl` configures the lifetime of ephemeral access tokens issued by `/auth/token` (default: `"15m"`)
225- `gordon auth show-token` prints the stored token for a remote
226- `gordon auth logout` removes the stored token locally
227- Automatic token exchange: the CLI transparently exchanges long-lived tokens for ephemeral ones before API calls
228- Admin scopes (`admin:*:*`, `admin:routes:read`, etc.) allow fine-grained access control for remote CLI operations
229
230**Migration steps:**
231
2321. **Before upgrading**, generate a token on your current Gordon instance:
233
234   ```bash
235   gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0
236   ```
237
238   Save this token securely. You will need it after upgrading. Admin scopes (`admin:*:*`) are only available after upgrading to v2.30.0 — regenerate your token with admin scopes after the upgrade if needed.
239
2402. **Update remotes** to use the generated token:
241
242   ```bash
243   gordon auth login --token <token>
244   # or
245   gordon remotes set-token prod <token>
246   ```
247
2483. **Update your config** to remove password fields:
249
250   **Before (v2.16.0):**
251
252   ```toml
253   [auth]
254   enabled = true
255   secrets_backend = "pass"
256   username = "deploy"
257   password_hash = "gordon/auth/password_hash"
258   token_secret = "gordon/auth/token_secret"
259   ```
260
261   **After (v2.30.0):**
262
263   ```toml
264   [auth]
265   enabled = true
266   secrets_backend = "pass"
267   token_secret = "gordon/auth/token_secret"
268   access_token_ttl = "15m"
269   ```
270
2714. **Update CI/CD pipelines** to use `GORDON_TOKEN` environment variable for authentication. See the [deployment guides](./deployment/index.md).
272
2735. **Upgrade the binary** and restart:
274
275   ```bash
276   curl -fsSL https://gordon.bnema.dev/install | bash
277   systemctl --user restart gordon
278   ```
279
2806. **Verify** the server starts without errors:
281
282   ```bash
283   journalctl --user -u gordon -f
284   ```
285
286### New: Configurable Access Token TTL
287
288Ephemeral access tokens issued by `/auth/token` now have a configurable lifetime via `auth.access_token_ttl` (default `"15m"`, maximum `"1h"`).
289
290Tokens with a lifetime at or below `MaxAccessTokenLifetime` (1 hour) are treated as ephemeral: they skip token store validation for performance, which means they **cannot be individually revoked**. They become invalid only when they naturally expire. Shortening `auth.access_token_ttl` only reduces the exposure window; it does not enable per-token revocation. If you need explicit revocation, use a stored long-lived token instead of `/auth/token`.
291
292```toml
293[auth]
294access_token_ttl = "30m"
295```
296
297### New: Admin Scopes
298
299Tokens can now include fine-grained admin scopes for remote CLI operations:
300
301```bash
302# Full admin access
303gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0
304
305# Read-only monitoring
306gordon auth token generate --subject monitor --scopes "admin:status:read" --expiry 30d
307
308# CI deploy with route read + config write
309gordon auth token generate --subject ci --scopes "push,pull,admin:routes:read,admin:config:write" --expiry 0
310```
311
312See [Token Scopes](./config/auth.md#token-scopes) for the full list.
313
314## v2.6.0 to v2.7.0
315
316### Breaking: Token Secret Required
317
318Gordon v2.7.0 requires a `token_secret` for JWT authentication. The server will not start without it configured.
319
320**Error you'll see:**
321```
322Error: token_secret is required for JWT token generation; set GORDON_AUTH_TOKEN_SECRET environment variable or configure auth.token_secret
323```
324
325**Choose one of these migration options:**
326
327#### Option A: Environment Variable (simplest)
328
329```bash
330# Generate a random secret
331export GORDON_AUTH_TOKEN_SECRET="$(openssl rand -base64 32)"
332
333# Add to your shell profile or systemd service
334echo 'export GORDON_AUTH_TOKEN_SECRET="your-secret"' >> ~/.bashrc
335```
336
337For systemd services:
338```bash
339mkdir -p ~/.config/systemd/user/gordon.service.d
340cat > ~/.config/systemd/user/gordon.service.d/token.conf << EOF
341[Service]
342Environment="GORDON_AUTH_TOKEN_SECRET=$(openssl rand -base64 32)"
343EOF
344systemctl --user daemon-reload
345systemctl --user restart gordon
346```
347
348#### Option B: Config File with Pass (recommended for production)
349
350```bash
351# Generate and store secret in pass
352openssl rand -base64 32 | pass insert -e gordon/auth/token_secret
353
354# Update your gordon.toml
355```
356
357```toml
358[auth]
359enabled = true
360secrets_backend = "pass"
361token_secret = "gordon/auth/token_secret"
362```
363
364#### Option C: Config File with Unsafe Backend (development only)
365
366```bash
367# Generate secret file
368mkdir -p ~/.gordon/secrets
369openssl rand -base64 32 > ~/.gordon/secrets/token_secret
370chmod 600 ~/.gordon/secrets/token_secret
371```
372
373```toml
374[auth]
375enabled = true
376secrets_backend = "unsafe"
377token_secret = "token_secret"
378```
379
380### Breaking: Unsafe Token Store Warning
381
382Using `secrets_backend = "unsafe"` now logs a warning on startup:
383```
384WRN using unsafe secrets backend - secrets are stored in plain text
385```
386
387This is intentional to encourage secure secret storage in production. The warning can be ignored for development.
388
389### New Features
390
391- **Attachment secrets discovery**: `gordon secrets list <domain>` now shows secrets for attachment containers
392- **Auth login command**: `gordon auth login --remote <name>` for token authentication
393- **Rate limiting**: Configurable rate limits under `[api.rate_limit]`
394
395### Security Improvements
396
397v2.7.0 includes a comprehensive security audit with:
398
399- JWT tokens now include "not before" (nbf) claim
400- SSRF protection for external routes
401- Security headers middleware
402- Rate limiting on registry and token endpoints
403- Command injection prevention in pass provider
404- Path traversal prevention in secrets store
405
406## v2.5.0 to v2.6.0
407
408### Breaking: Config Restructure
409
410The `[secrets]` and `[registry_auth]` sections were merged into `[auth]`.
411
412**Before (v2.5.0):**
413```toml
414[secrets]
415backend = "pass"
416
417[registry_auth]
418enabled = true
419type = "password"
420password_hash = "gordon/registry/password_hash"
421```
422
423**After (v2.6.0+):**
424```toml
425[auth]
426enabled = true
427secrets_backend = "pass"
428password_hash = "gordon/auth/password_hash"
429```
430
431### Breaking: Auth Enabled by Default
432
433Gordon keeps auth enabled by default. If you set `auth.enabled = false`, Gordon runs in local-only mode (`/admin/*` disabled, `/v2/*` loopback-only).
434
435### Breaking: Secret Paths Changed
436
437If using pass or sops, update your secret paths:
438- `gordon/registry/*` → `gordon/auth/*`
439
440## General Upgrade Process
441
4421. **Read the changelog** for your target version
4432. **Backup your config** before upgrading
4443. **Test in staging** if possible
4454. **Upgrade the binary**:
446   ```bash
447   curl -fsSL https://gordon.bnema.dev/install | bash
448   ```
4495. **Restart Gordon**:
450   ```bash
451   systemctl --user restart gordon
452   ```
4536. **Check logs** for any errors:
454   ```bash
455   gordon logs
456   ```
457
458## Getting Help
459
460- [GitHub Issues](https://github.com/bnema/gordon/issues)
461- [Documentation](https://gordon.bnema.dev/docs)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close