v3.3.0 b164fe9

upgrading.md RawHistoryFolder

Upgrading Gordon

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

v3.0.0: Declarative Apps (breaking)

Gordon v3 replaces route-container management with declarative apps. One standalone TOML file defines one app; gordon apps apply persists desired state and gordon apps deploy activates it. Push transfers OCI content only and never deploys.

Follow Migrate to Gordon v3 for the complete cutover procedure, including explicit migration from domain-scoped secrets to app- and service-scoped secrets.

Removed

  • App-workload keys in gordon.toml: [routes], [attachments], [network_groups], [service_routes], [auto_route] (+ _allowed_domains), and [previews]. Gordon fails boot/reload closed with a config-retired diagnostic naming the fix when any of them is present — never a silent migration.
  • Installation-level [[services]] (standalone L4 workloads) and [[network_services]] (L4 traffic plane) stay valid. Only their old app-workload semantics were removed; declare application workloads in standalone files instead. See Standalone Services.
  • CLI: pin, preview, attachments, bootstrap, autoroute allow, routes add/remove/purge, push deploy/route inference, implicit deploys on push/reload, label/env-file inference. Removed HTTP mutation endpoints answer 410 Gone.
  • Domain secrets: gordon secrets, /admin/secrets (now 410 Gone), the admin:secrets:* scopes, the [env] config section, and startup .env import into pass. Use gordon apps secrets. Existing gordon/env/... pass entries are left untouched.
  • CLI layout: gordon push is now gordon images push. status, logs, reload, config, tls status, traffic status, and networks list moved under gordon daemon (gordon daemon status, gordon daemon tls, gordon daemon traffic, gordon daemon networks, …). There are no aliases.
  • Scopes admin:routes:* and admin:secrets:* are replaced by admin:apps:read (list, show, diff, status) and admin:apps:write (apply, deploy, lifecycle, secrets). Regenerate CI tokens, e.g. --scopes "push,pull,admin:apps:read,admin:apps:write".
  • No historical rollback command: roll back by applying a manifest that references the previous tag and deploying again.

Migrating a v3 alpha app manifest

Early v3 alpha manifests used the array-of-tables form [[service]] with a name field and [service.*] children. That shape is rejected with a keyed-schema diagnostic, not converted. Rewrite each service as a [services.<name>] table.

Before (alpha, rejected):

name = "blog"

[[service]]
name = "web"
image = "registry.example.com/blog/web:1.2.3"

[[service.http]]
host = "blog.example.com"
port = 8080

After (current keyed schema):

name = "blog"

[services.web]
image = "registry.example.com/blog/web:1.2.3"

[[services.web.http]]
host = "blog.example.com"
port = 8080

Names containing dots must be quoted: [services."web.api"] and [[services."web.api".http]]. See App Manifest.

Manual migration

  1. Back up databases/volumes and pass entries with the existing procedures. Record original ownership and image versions. No update hook deletes volumes: unknown resources are preserved, never adopted.
  2. Delete the removed keys, including [env], from gordon.toml (installation settings only: entrypoints, TLS, limits, external routes, images policy, backups destinations stay).
  3. Write one <app>.toml per app (see App Manifest): services, [[services.<name>.http]] hosts, [services.<name>.secrets] names, volumes, [[network.shared]], backup declarations.
  4. Migrate secret values explicitly with gordon apps secrets set after applying each manifest. Gordon does not copy gordon/env/<domain>/... entries into gordon/apps/<uuid>/<service>/...; follow the v3 secrets migration procedure.
  5. gordon apps apply --file <app>.toml, then gordon apps deploy <app>.
  6. Staging is an ordinary app in another file. A binary downgrade against the new app-state format is unsupported: restore the old installation/config/state and backups through an operator-approved procedure.

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.

v2.31.0: 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.

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. Remove TLS-capable entrypoints 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 daemon status --remote ... and gordon apps 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:apps:read, admin:apps:write, 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://bnema.dev/gordon/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 app read + write
gordon auth token generate --subject ci --scopes "push,pull,admin:apps:read,admin:apps: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://bnema.dev/gordon/install | bash
    
  5. Restart Gordon:
    systemctl --user restart gordon
    
  6. Check logs for any errors:
    gordon daemon logs
    

Getting Help

source
1# Upgrading Gordon
2
3This guide covers breaking changes and migration steps between major versions.
4
5## v3.0.0: Declarative Apps (breaking)
6
7Gordon v3 replaces route-container management with declarative apps. One standalone TOML file defines one app; `gordon apps apply` persists desired state and `gordon apps deploy` activates it. Push transfers OCI content only and never deploys.
8
9Follow [Migrate to Gordon v3](./migrate-to-v3.md) for the complete cutover procedure, including explicit migration from domain-scoped secrets to app- and service-scoped secrets.
10
11### Removed
12
13- App-workload keys in `gordon.toml`: `[routes]`, `[attachments]`, `[network_groups]`, `[service_routes]`, `[auto_route]` (+ `_allowed_domains`), and `[previews]`. Gordon fails boot/reload closed with a `config-retired` diagnostic naming the fix when any of them is present — never a silent migration.
14- Installation-level `[[services]]` (standalone L4 workloads) and `[[network_services]]` (L4 traffic plane) stay valid. Only their old app-workload semantics were removed; declare application workloads in standalone files instead. See [Standalone Services](./config/services.md).
15- CLI: `pin`, `preview`, `attachments`, `bootstrap`, `autoroute allow`, `routes add/remove/purge`, push deploy/route inference, implicit deploys on push/reload, label/env-file inference. Removed HTTP mutation endpoints answer `410 Gone`.
16- Domain secrets: `gordon secrets`, `/admin/secrets` (now `410 Gone`), the `admin:secrets:*` scopes, the `[env]` config section, and startup `.env` import into pass. Use `gordon apps secrets`. Existing `gordon/env/...` pass entries are left untouched.
17- CLI layout: `gordon push` is now `gordon images push`. `status`, `logs`, `reload`, `config`, `tls status`, `traffic status`, and `networks list` moved under `gordon daemon` (`gordon daemon status`, `gordon daemon tls`, `gordon daemon traffic`, `gordon daemon networks`, …). There are no aliases.
18- Scopes `admin:routes:*` and `admin:secrets:*` are replaced by `admin:apps:read` (list, show, diff, status) and `admin:apps:write` (apply, deploy, lifecycle, secrets). Regenerate CI tokens, e.g. `--scopes "push,pull,admin:apps:read,admin:apps:write"`.
19- No historical rollback command: roll back by applying a manifest that references the previous tag and deploying again.
20
21### Migrating a v3 alpha app manifest
22
23Early v3 alpha manifests used the array-of-tables form `[[service]]` with a `name` field and `[service.*]` children. That shape is rejected with a keyed-schema diagnostic, not converted. Rewrite each service as a `[services.<name>]` table.
24
25Before (alpha, rejected):
26
27```toml
28name = "blog"
29
30[[service]]
31name = "web"
32image = "registry.example.com/blog/web:1.2.3"
33
34[[service.http]]
35host = "blog.example.com"
36port = 8080
37```
38
39After (current keyed schema):
40
41```toml
42name = "blog"
43
44[services.web]
45image = "registry.example.com/blog/web:1.2.3"
46
47[[services.web.http]]
48host = "blog.example.com"
49port = 8080
50```
51
52Names containing dots must be quoted: `[services."web.api"]` and `[[services."web.api".http]]`. See [App Manifest](./config/apps.md).
53
54### Manual migration
55
561. Back up databases/volumes and pass entries with the existing procedures. Record original ownership and image versions. No update hook deletes volumes: unknown resources are preserved, never adopted.
572. Delete the removed keys, including `[env]`, from `gordon.toml` (installation settings only: entrypoints, TLS, limits, external routes, images policy, backups destinations stay).
583. Write one `<app>.toml` per app (see [App Manifest](./config/apps.md)): services, `[[services.<name>.http]]` hosts, `[services.<name>.secrets]` names, volumes, `[[network.shared]]`, backup declarations.
594. Migrate secret values explicitly with `gordon apps secrets set` after applying each manifest. Gordon does not copy `gordon/env/<domain>/...` entries into `gordon/apps/<uuid>/<service>/...`; follow the [v3 secrets migration procedure](./migrate-to-v3.md#2-migrate-domain-secrets).
605. `gordon apps apply --file <app>.toml`, then `gordon apps deploy <app>`.
616. Staging is an ordinary app in another file. A binary downgrade against the new app-state format is unsupported: restore the old installation/config/state and backups through an operator-approved procedure.
62
63## Route-Domain Validation
64
65Route 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.
66
67## v2.31.0: Unified smart TCP entrypoints
68
69### Breaking: `server.port` / `server.tls_port` no longer define public listeners
70
71Public application traffic is configured entrypoint-first. Migrate old HTTP/HTTPS listener settings to a smart TCP edge entrypoint.
72
73**Before:**
74
75```toml
76[server]
77port = 8088
78tls_port = 8443
79registry_port = 5000
80gordon_domain = "gordon.example.com"
81```
82
83**After:**
84
85```toml
86[server]
87registry_port = 5000
88gordon_domain = "gordon.example.com"
89
90[entrypoints.edge]
91address = ":443"
92protocol = "smart_tcp"
93```
94
95`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.
96
97If you run rootless and cannot bind 443 directly, bind a high port and map external ports to it:
98
99```toml
100[entrypoints.edge]
101address = ":9000"
102protocol = "smart_tcp"
103```
104
105```bash
106sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=9000
107sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
108sudo firewall-cmd --reload
109```
110
111Keep `server.registry_port` for Docker/Podman push and pull traffic; it is separate from public app entrypoints.
112
113### ACME migration notes
114
115- DNS-01 (`cloudflare-dns-01`) does not require a special external port 80 edge.
116- HTTP-01 requires an HTTP-capable `smart_tcp` entrypoint reachable on external port 80 for each hostname being validated.
117- TLS-ALPN-01 is unsupported.
118- Normal HTTPS fallback certificate priority is static certificates, then public ACME certificates, then Gordon's internal CA.
119
120### Required for Cloudflare/Proxy Setups: `proxy_allowed_ips`
121
122The 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`:
123
124```toml
125[server]
126proxy_allowed_ips = [
127  "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
128  "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
129  "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
130  "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
131  "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
132]
133```
134
135Without this, all proxied HTTP traffic returns `403 Forbidden`. Remove TLS-capable entrypoints to disable the internal CA and skip this requirement.
136
137### Breaking: `server.gordon_domain` Replaces `server.registry_domain`
138
139Gordon now uses `server.gordon_domain` as the public registry and admin host. Migrate older configs that still set only `server.registry_domain` before restarting:
140
141**Before:**
142
143```toml
144[server]
145registry_domain = "gordon.example.com"
146```
147
148**After:**
149
150```toml
151[server]
152gordon_domain = "gordon.example.com"
153```
154
155If you do not migrate, `gordon daemon status --remote ...` and `gordon apps list --remote ...` can fail with `/auth/token` `404`, and `reg-domain/v2/` or `/admin/status` can return `404`.
156
157### Staged Registry Host Rename
158
159If 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:
160
161```toml
162[server]
163gordon_domain = "gordon.example.com"
164legacy_registry_domains = [
165  "registry.example.com",
166  "registry.example.com:5000",
167]
168```
169
170Recommended rollout:
171
1721. Set `gordon_domain` to the new host.
1732. Add every old Gordon registry host that clients still use to `legacy_registry_domains`.
1743. Restart Gordon.
1754. Move Docker/Podman logins, pushes, pulls, and image references to the new host.
1765. Remove `legacy_registry_domains` after every client has moved.
177
178During 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`.
179
180### Breaking: `gordon rollback` Renamed to `gordon pin`
181
182If you have scripts or runbooks using `gordon rollback`, switch them to `gordon pin`:
183
184```bash
185gordon pin app.example.com
186gordon pin app.example.com --tag v2.30.1
187gordon pin list app.example.com
188```
189
190### Breaking-ish: `routes list` Is Inventory-Only
191
192`gordon routes list` now shows domain/image inventory only. Detailed runtime state, HTTP probe status, and attachment status moved to `gordon routes status`.
193
194If you have automation parsing status-like fields from `routes list`, migrate it to `routes status --json`.
195
196### New: Public ACME TLS
197
198Gordon can now obtain public certificates with `[tls.acme]` using either `http-01` or `cloudflare-dns-01`.
199
200- Normal HTTPS fallback must be available on a TLS-capable entrypoint
201- `http-01` needs an HTTP-capable smart TCP entrypoint reachable on external port 80 for each hostname being validated
202- `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
203- `obtain_batch_size` limits new ACME orders per reconcile pass to reduce rate-limit spikes
204
205### New: DNS Resolver Settings for ACME DNS-01
206
207`[dns]` controls the recursive resolvers Gordon uses to verify public DNS propagation for ACME DNS-01.
208
209Add this if you need non-default resolvers, split-horizon-aware testing, or a slower propagation window:
210
211```toml
212[dns]
213resolvers = ["1.1.1.1:53", "8.8.8.8:53"]
214propagation_timeout = "5m"
215polling_interval = "5s"
216```
217
218### Runtime Change: Managed Containers Use Restart Policy `always`
219
220Newly created managed route containers now use the runtime restart policy `always`. Existing containers pick this up on redeploy/recreate.
221
222This improves crash recovery and keeps Docker/Podman behavior aligned with Gordon's startup reconciliation.
223
224### New: Saved Remote Inference for Targeted Commands
225
226When 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.
227
228If multiple remotes match or probing fails, Gordon now stops and asks you to pass `--remote` explicitly instead of guessing.
229
230### New: Dedicated Access Log
231
232You 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.
233
234## v2.16.0 to v2.30.0
235
236### Breaking: Password Authentication Removed
237
238Gordon v2.30.0 removes password-based authentication entirely. Only token-based authentication is supported.
239
240**What changed:**
241
242- `auth.type = "password"` is no longer accepted
243- `auth.password` and `auth.password_hash` config fields are removed
244- The `gordon auth password hash` CLI command is removed
245- The `gordon auth login` command now requires `--token` (no more interactive password prompt)
246- The `/auth/password` endpoint now returns `410 Gone`
247- Long-lived tokens are no longer accepted on admin/registry endpoints — they must be exchanged for ephemeral tokens via `/auth/token`
248
249**New features:**
250
251- `auth.access_token_ttl` configures the lifetime of ephemeral access tokens issued by `/auth/token` (default: `"15m"`)
252- `gordon auth show-token` prints the stored token for a remote
253- `gordon auth logout` removes the stored token locally
254- Automatic token exchange: the CLI transparently exchanges long-lived tokens for ephemeral ones before API calls
255- Admin scopes (`admin:*:*`, `admin:apps:read`, `admin:apps:write`, etc.) allow fine-grained access control for remote CLI operations
256
257**Migration steps:**
258
2591. **Before upgrading**, generate a token on your current Gordon instance:
260
261   ```bash
262   gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0
263   ```
264
265   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.
266
2672. **Update remotes** to use the generated token:
268
269   ```bash
270   gordon auth login --token <token>
271   # or
272   gordon remotes set-token prod <token>
273   ```
274
2753. **Update your config** to remove password fields:
276
277   **Before (v2.16.0):**
278
279   ```toml
280   [auth]
281   enabled = true
282   secrets_backend = "pass"
283   username = "deploy"
284   password_hash = "gordon/auth/password_hash"
285   token_secret = "gordon/auth/token_secret"
286   ```
287
288   **After (v2.30.0):**
289
290   ```toml
291   [auth]
292   enabled = true
293   secrets_backend = "pass"
294   token_secret = "gordon/auth/token_secret"
295   access_token_ttl = "15m"
296   ```
297
2984. **Update CI/CD pipelines** to use `GORDON_TOKEN` environment variable for authentication. See the [deployment guides](./deployment/index.md).
299
3005. **Upgrade the binary** and restart:
301
302   ```bash
303   curl -fsSL https://bnema.dev/gordon/install | bash
304   systemctl --user restart gordon
305   ```
306
3076. **Verify** the server starts without errors:
308
309   ```bash
310   journalctl --user -u gordon -f
311   ```
312
313### New: Configurable Access Token TTL
314
315Ephemeral access tokens issued by `/auth/token` now have a configurable lifetime via `auth.access_token_ttl` (default `"15m"`, maximum `"1h"`).
316
317Tokens 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`.
318
319```toml
320[auth]
321access_token_ttl = "30m"
322```
323
324### New: Admin Scopes
325
326Tokens can now include fine-grained admin scopes for remote CLI operations:
327
328```bash
329# Full admin access
330gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0
331
332# Read-only monitoring
333gordon auth token generate --subject monitor --scopes "admin:status:read" --expiry 30d
334
335# CI deploy with app read + write
336gordon auth token generate --subject ci --scopes "push,pull,admin:apps:read,admin:apps:write" --expiry 0
337```
338
339See [Token Scopes](./config/auth.md#token-scopes) for the full list.
340
341## v2.6.0 to v2.7.0
342
343### Breaking: Token Secret Required
344
345Gordon v2.7.0 requires a `token_secret` for JWT authentication. The server will not start without it configured.
346
347**Error you'll see:**
348```
349Error: token_secret is required for JWT token generation; set GORDON_AUTH_TOKEN_SECRET environment variable or configure auth.token_secret
350```
351
352**Choose one of these migration options:**
353
354#### Option A: Environment Variable (simplest)
355
356```bash
357# Generate a random secret
358export GORDON_AUTH_TOKEN_SECRET="$(openssl rand -base64 32)"
359
360# Add to your shell profile or systemd service
361echo 'export GORDON_AUTH_TOKEN_SECRET="your-secret"' >> ~/.bashrc
362```
363
364For systemd services:
365```bash
366mkdir -p ~/.config/systemd/user/gordon.service.d
367cat > ~/.config/systemd/user/gordon.service.d/token.conf << EOF
368[Service]
369Environment="GORDON_AUTH_TOKEN_SECRET=$(openssl rand -base64 32)"
370EOF
371systemctl --user daemon-reload
372systemctl --user restart gordon
373```
374
375#### Option B: Config File with Pass (recommended for production)
376
377```bash
378# Generate and store secret in pass
379openssl rand -base64 32 | pass insert -e gordon/auth/token_secret
380
381# Update your gordon.toml
382```
383
384```toml
385[auth]
386enabled = true
387secrets_backend = "pass"
388token_secret = "gordon/auth/token_secret"
389```
390
391#### Option C: Config File with Unsafe Backend (development only)
392
393```bash
394# Generate secret file
395mkdir -p ~/.gordon/secrets
396openssl rand -base64 32 > ~/.gordon/secrets/token_secret
397chmod 600 ~/.gordon/secrets/token_secret
398```
399
400```toml
401[auth]
402enabled = true
403secrets_backend = "unsafe"
404token_secret = "token_secret"
405```
406
407### Breaking: Unsafe Token Store Warning
408
409Using `secrets_backend = "unsafe"` now logs a warning on startup:
410```
411WRN using unsafe secrets backend - secrets are stored in plain text
412```
413
414This is intentional to encourage secure secret storage in production. The warning can be ignored for development.
415
416### New Features
417
418- **Attachment secrets discovery**: `gordon secrets list <domain>` now shows secrets for attachment containers
419- **Auth login command**: `gordon auth login --remote <name>` for token authentication
420- **Rate limiting**: Configurable rate limits under `[api.rate_limit]`
421
422### Security Improvements
423
424v2.7.0 includes a comprehensive security audit with:
425
426- JWT tokens now include "not before" (nbf) claim
427- SSRF protection for external routes
428- Security headers middleware
429- Rate limiting on registry and token endpoints
430- Command injection prevention in pass provider
431- Path traversal prevention in secrets store
432
433## v2.5.0 to v2.6.0
434
435### Breaking: Config Restructure
436
437The `[secrets]` and `[registry_auth]` sections were merged into `[auth]`.
438
439**Before (v2.5.0):**
440```toml
441[secrets]
442backend = "pass"
443
444[registry_auth]
445enabled = true
446type = "password"
447password_hash = "gordon/registry/password_hash"
448```
449
450**After (v2.6.0+):**
451```toml
452[auth]
453enabled = true
454secrets_backend = "pass"
455password_hash = "gordon/auth/password_hash"
456```
457
458### Breaking: Auth Enabled by Default
459
460Gordon keeps auth enabled by default. If you set `auth.enabled = false`, Gordon runs in local-only mode (`/admin/*` disabled, `/v2/*` loopback-only).
461
462### Breaking: Secret Paths Changed
463
464If using pass or sops, update your secret paths:
465- `gordon/registry/*` → `gordon/auth/*`
466
467## General Upgrade Process
468
4691. **Read the changelog** for your target version
4702. **Backup your config** before upgrading
4713. **Test in staging** if possible
4724. **Upgrade the binary**:
473   ```bash
474   curl -fsSL https://bnema.dev/gordon/install | bash
475   ```
4765. **Restart Gordon**:
477   ```bash
478   systemctl --user restart gordon
479   ```
4806. **Check logs** for any errors:
481   ```bash
482   gordon daemon logs
483   ```
484
485## Getting Help
486
487- [GitHub Issues](https://github.com/bnema/gordon/issues)
488- [Documentation](https://bnema.dev/gordon/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