detached 63f113e

feat(server): add tls_port=0 opt-out and use unprivileged default ports

Allow disabling the HTTPS listener and internal CA entirely by setting
tls_port=0, for setups where TLS is handled externally (Tailscale,
Cloudflare, etc.).

Change default ports from privileged (80/443) to unprivileged
(8088/8443) to match the common rootless + firewalld port-forward
pattern.

Update all docs to reflect the new internal CA architecture and
remove references to deprecated tls_enabled, tls_cert_file,
tls_key_file, and force_hsts config fields.
commit
63f113ebd15223096b2f9c5f4c96f9b59fcb9a55
author
brice · · 2026-03-30 18:38 UTC
committer
brice · · 2026-03-30 18:40 UTC
parent
30b9539
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +69 -71
M docs/cli/remotes.md +1 -4
@@ -341,10 +341,7 @@ Configure Gordon to use them:
341341
342342 ```toml
343343 [server]
344-tls_enabled = true
345-tls_port = 443
346-tls_cert_file = "/etc/gordon/tls/cert.pem"
347-tls_key_file = "/etc/gordon/tls/key.pem"
344+tls_port = 8443 # Gordon's internal CA handles certificates automatically
348345 ```
349346
350347 `insecure_tls` only affects CLI -> Gordon admin HTTPS verification. It does not change runtime routing: Gordon reverse proxy and container routes can still serve wildcard app domains like `*.example.com`.
@@ -341,10 +341,7 @@ Configure Gordon to use them:
341 341
342 ```toml342 ```toml
343 [server]343 [server]
344-tls_enabled = true344+tls_port = 8443 # Gordon's internal CA handles certificates automatically
345-tls_port = 443
346-tls_cert_file = "/etc/gordon/tls/cert.pem"
347-tls_key_file = "/etc/gordon/tls/key.pem"
348 ```345 ```
349 346
350 `insecure_tls` only affects CLI -> Gordon admin HTTPS verification. It does not change runtime routing: Gordon reverse proxy and container routes can still serve wildcard app domains like `*.example.com`.347 `insecure_tls` only affects CLI -> Gordon admin HTTPS verification. It does not change runtime routing: Gordon reverse proxy and container routes can still serve wildcard app domains like `*.example.com`.
M docs/config/index.md +2 -2
@@ -30,7 +30,7 @@ For a complete list of all configuration options with their default values, see
3030 ```toml
3131 # Server settings
3232 [server]
33-port = 8080 # HTTP proxy port (default: 80)
33+port = 8088 # HTTP proxy port (default: 8088)
3434 registry_port = 5000 # Registry port (default: 5000)
3535 gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API)
3636 # data_dir = "~/.gordon" # Default for user installations
@@ -167,7 +167,7 @@ keep_last = 3
167167
168168 | Setting | Default |
169169 |---------|---------|
170-| `server.port` | `80` |
170+| `server.port` | `8088` |
171171 | `server.registry_port` | `5000` |
172172 | `server.data_dir` | `~/.gordon` |
173173 | `auth.enabled` | `true` |
@@ -30,7 +30,7 @@ For a complete list of all configuration options with their default values, see
30 ```toml30 ```toml
31 # Server settings31 # Server settings
32 [server]32 [server]
33-port = 8080 # HTTP proxy port (default: 80)33+port = 8088 # HTTP proxy port (default: 8088)
34 registry_port = 5000 # Registry port (default: 5000)34 registry_port = 5000 # Registry port (default: 5000)
35 gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API)35 gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API)
36 # data_dir = "~/.gordon" # Default for user installations36 # data_dir = "~/.gordon" # Default for user installations
@@ -167,7 +167,7 @@ keep_last = 3
167 167
168 | Setting | Default |168 | Setting | Default |
169 |---------|---------|169 |---------|---------|
170-| `server.port` | `80` |170+| `server.port` | `8088` |
171 | `server.registry_port` | `5000` |171 | `server.registry_port` | `5000` |
172 | `server.data_dir` | `~/.gordon` |172 | `server.data_dir` | `~/.gordon` |
173 | `auth.enabled` | `true` |173 | `auth.enabled` | `true` |
M docs/config/rate-limiting.md +1 -1
@@ -150,7 +150,7 @@ A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests ins
150150
151151 **Critical for correct IP detection in production.**
152152
153-Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (`server.tls_enabled = true`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer.
153+Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (via its internal CA on `server.tls_port`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer.
154154
155155 ```
156156 Internet → [Cloudflare/nginx] → Gordon → Containers
@@ -150,7 +150,7 @@ A burst of `100` with `per_ip_rps = 50` means a client can send 100 requests ins
150 150
151 **Critical for correct IP detection in production.**151 **Critical for correct IP detection in production.**
152 152
153-Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (`server.tls_enabled = true`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer.153+Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (via its internal CA on `server.tls_port`) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer.
154 154
155 ```155 ```
156 Internet → [Cloudflare/nginx] → Gordon → Containers156 Internet → [Cloudflare/nginx] → Gordon → Containers
M docs/config/reference.md +4 -12
@@ -9,19 +9,15 @@ Complete configuration reference with all options and their default values.
99 # SERVER
1010 # =============================================================================
1111 [server]
12-port = 80 # HTTP proxy port
12+port = 8088 # HTTP proxy port
1313 registry_port = 5000 # Container registry port
14-tls_enabled = false # Enable native HTTPS listener
15-tls_port = 443 # HTTPS proxy port when enabled
16-tls_cert_file = "" # PEM cert path (auto-generated if empty and TLS enabled)
17-tls_key_file = "" # PEM key path (auto-generated if empty and TLS enabled)
14+tls_port = 8443 # HTTPS proxy port (0 = disabled, no internal CA)
1815 gordon_domain = "" # Required: Gordon domain (registry + API)
1916 data_dir = "~/.gordon" # Data directory (varies by install type)
2017 max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk
2118 registry_allowed_ips = [] # IPs or CIDR ranges allowed to access the registry (empty = allow all)
2219 proxy_allowed_ips = [] # IPs or CIDR ranges allowed to reach the proxy (empty = allow all, e.g. Cloudflare IPs)
2320 registry_listen_address = "" # Bind address for registry (empty = all interfaces, "127.0.0.1" = loopback only)
24-force_hsts = false # Always send HSTS header (enable when behind TLS-terminating proxy)
2521
2622 # =============================================================================
2723 # AUTHENTICATION (required - Gordon won't start without credentials configured)
@@ -170,19 +166,15 @@ keep_last = 3 # Keep N newest tags per repository
170166
171167 | Setting | Default | Description |
172168 |---------|---------|-------------|
173-| `server.port` | `80` | HTTP proxy port |
169+| `server.port` | `8088` | HTTP proxy port |
174170 | `server.registry_port` | `5000` | Container registry port |
175-| `server.tls_enabled` | `false` | Enable native HTTPS listener |
176-| `server.tls_port` | `443` | HTTPS listener port |
177-| `server.tls_cert_file` | `""` | TLS cert path (auto-generated when empty) |
178-| `server.tls_key_file` | `""` | TLS key path (auto-generated when empty) |
171+| `server.tls_port` | `8443` | HTTPS listener port (0 = disabled) |
179172 | `server.gordon_domain` | `""` | **Required** - Gordon domain |
180173 | `server.data_dir` | `~/.gordon` | Data directory |
181174 | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk |
182175 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
183176 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
184177 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |
185-| `server.force_hsts` | `false` | Always send HSTS header (for TLS-terminating proxy setups) |
186178 | `auth.enabled` | `true` | Enable authentication; when `false`, run local-only mode (loopback-only `/v2/*`, `/admin/*` disabled) |
187179 | `auth.secrets_backend` | `"unsafe"` | Secrets storage |
188180 | `auth.token_expiry` | `"720h"` | 30 days |
@@ -9,19 +9,15 @@ Complete configuration reference with all options and their default values.
9 # SERVER9 # SERVER
10 # =============================================================================10 # =============================================================================
11 [server]11 [server]
12-port = 80 # HTTP proxy port12+port = 8088 # HTTP proxy port
13 registry_port = 5000 # Container registry port13 registry_port = 5000 # Container registry port
14-tls_enabled = false # Enable native HTTPS listener14+tls_port = 8443 # HTTPS proxy port (0 = disabled, no internal CA)
15-tls_port = 443 # HTTPS proxy port when enabled
16-tls_cert_file = "" # PEM cert path (auto-generated if empty and TLS enabled)
17-tls_key_file = "" # PEM key path (auto-generated if empty and TLS enabled)
18 gordon_domain = "" # Required: Gordon domain (registry + API)15 gordon_domain = "" # Required: Gordon domain (registry + API)
19 data_dir = "~/.gordon" # Data directory (varies by install type)16 data_dir = "~/.gordon" # Data directory (varies by install type)
20 max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk17 max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk
21 registry_allowed_ips = [] # IPs or CIDR ranges allowed to access the registry (empty = allow all)18 registry_allowed_ips = [] # IPs or CIDR ranges allowed to access the registry (empty = allow all)
22 proxy_allowed_ips = [] # IPs or CIDR ranges allowed to reach the proxy (empty = allow all, e.g. Cloudflare IPs)19 proxy_allowed_ips = [] # IPs or CIDR ranges allowed to reach the proxy (empty = allow all, e.g. Cloudflare IPs)
23 registry_listen_address = "" # Bind address for registry (empty = all interfaces, "127.0.0.1" = loopback only)20 registry_listen_address = "" # Bind address for registry (empty = all interfaces, "127.0.0.1" = loopback only)
24-force_hsts = false # Always send HSTS header (enable when behind TLS-terminating proxy)
25 21
26 # =============================================================================22 # =============================================================================
27 # AUTHENTICATION (required - Gordon won't start without credentials configured)23 # AUTHENTICATION (required - Gordon won't start without credentials configured)
@@ -170,19 +166,15 @@ keep_last = 3 # Keep N newest tags per repository
170 166
171 | Setting | Default | Description |167 | Setting | Default | Description |
172 |---------|---------|-------------|168 |---------|---------|-------------|
173-| `server.port` | `80` | HTTP proxy port |169+| `server.port` | `8088` | HTTP proxy port |
174 | `server.registry_port` | `5000` | Container registry port |170 | `server.registry_port` | `5000` | Container registry port |
175-| `server.tls_enabled` | `false` | Enable native HTTPS listener |171+| `server.tls_port` | `8443` | HTTPS listener port (0 = disabled) |
176-| `server.tls_port` | `443` | HTTPS listener port |
177-| `server.tls_cert_file` | `""` | TLS cert path (auto-generated when empty) |
178-| `server.tls_key_file` | `""` | TLS key path (auto-generated when empty) |
179 | `server.gordon_domain` | `""` | **Required** - Gordon domain |172 | `server.gordon_domain` | `""` | **Required** - Gordon domain |
180 | `server.data_dir` | `~/.gordon` | Data directory |173 | `server.data_dir` | `~/.gordon` | Data directory |
181 | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk |174 | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk |
182 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |175 | `server.registry_allowed_ips` | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
183 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |176 | `server.proxy_allowed_ips` | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
184 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |177 | `server.registry_listen_address` | `""` | Bind address for registry (empty = all interfaces) |
185-| `server.force_hsts` | `false` | Always send HSTS header (for TLS-terminating proxy setups) |
186 | `auth.enabled` | `true` | Enable authentication; when `false`, run local-only mode (loopback-only `/v2/*`, `/admin/*` disabled) |178 | `auth.enabled` | `true` | Enable authentication; when `false`, run local-only mode (loopback-only `/v2/*`, `/admin/*` disabled) |
187 | `auth.secrets_backend` | `"unsafe"` | Secrets storage |179 | `auth.secrets_backend` | `"unsafe"` | Secrets storage |
188 | `auth.token_expiry` | `"720h"` | 30 days |180 | `auth.token_expiry` | `"720h"` | 30 days |
M docs/config/server.md +28 -31
@@ -6,12 +6,9 @@ Core server settings for Gordon.
66
77 ```toml
88 [server]
9-port = 8080 # HTTP proxy port
9+port = 8088 # HTTP proxy port
1010 registry_port = 5000 # Container registry port
11-tls_enabled = false # Enable native TLS listener
12-tls_port = 443 # HTTPS listener port
13-tls_cert_file = "" # PEM certificate file (required when TLS enabled)
14-tls_key_file = "" # PEM key file (required when TLS enabled)
11+tls_port = 8443 # HTTPS listener port (0 = disabled)
1512 gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
1613 # data_dir = "~/.gordon" # Data storage directory (default)
1714 ```
@@ -20,12 +17,9 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
2017
2118 | Option | Type | Default | Description |
2219 |--------|------|---------|-------------|
23-| `port` | int | `80` | HTTP proxy port for routing traffic to containers |
20+| `port` | int | `8088` | HTTP proxy port for routing traffic to containers |
2421 | `registry_port` | int | `5000` | Docker registry port for image push/pull |
25-| `tls_enabled` | bool | `false` | Enable native HTTPS listener for proxy traffic |
26-| `tls_port` | int | `443` | HTTPS listener port when `tls_enabled` is true |
27-| `tls_cert_file` | string | `""` | Path to PEM certificate file |
28-| `tls_key_file` | string | `""` | Path to PEM private key file |
22+| `tls_port` | int | `8443` | HTTPS listener port with internal CA. Set to `0` to disable TLS entirely |
2923 | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) |
3024 | `registry_domain` | string | - | Deprecated alias for `gordon_domain` |
3125 | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files |
@@ -34,7 +28,6 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
3428 | `registry_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
3529 | `proxy_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
3630 | `registry_listen_address` | string | `""` | Bind address for registry (empty = all interfaces) |
37-| `force_hsts` | bool | `false` | Always send HSTS header (for TLS-terminating proxy setups) |
3831
3932 ## Port Configuration
4033
@@ -54,30 +47,39 @@ For rootless containers, you'll typically use a high port and configure firewall
5447 sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
5548 ```
5649
57-### Native TLS Listener
50+### Internal CA and TLS
5851
59-Enable native HTTPS directly in Gordon:
52+Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains. The HTTPS listener is enabled by default on `tls_port` (8443).
6053
6154 ```toml
6255 [server]
63-tls_enabled = true
64-tls_port = 443
65-tls_cert_file = "/etc/gordon/certs/fullchain.pem"
66-tls_key_file = "/etc/gordon/certs/privkey.pem"
56+tls_port = 8443 # Default. Set to 0 to disable TLS entirely.
6757 ```
6858
6959 With TLS enabled:
70-- Gordon terminates HTTPS on `tls_port`
71-- Host-based routing still works (apps, `/v2/*`, `/auth/*`, `/admin/*`)
72-- `port` remains available for plain HTTP unless you restrict it externally
73-- If `tls_cert_file`/`tls_key_file` are empty, Gordon auto-generates a self-signed pair in `{data_dir}/tls/`
60+- Gordon runs an internal CA (root + intermediate) stored in `{data_dir}/pki/`
61+- Leaf certificates are issued on-demand per domain (SNI-based) and cached in memory
62+- The intermediate CA auto-renews before expiry
63+- An onboarding page at `http://{host}/ca` lets clients download the root CA certificate
64+- The root CA is stable across restarts (generated once, persisted to disk)
7465
75-For rootless user services, do not bind directly to `443`. Use an unprivileged TLS port (for example `8443`) and forward `443` to it at the firewall or edge proxy.
66+#### Disabling TLS
67+
68+For setups where TLS is handled externally (Tailscale, Cloudflare, etc.):
7669
7770 ```toml
7871 [server]
79-tls_enabled = true
80-tls_port = 8443
72+tls_port = 0 # No HTTPS listener, no internal CA generated
73+```
74+
75+#### Firewall port forwarding
76+
77+For rootless user services, forward 443 to the high port at the firewall:
78+
79+```bash
80+# Forward 443 -> 8443
81+sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
82+sudo firewall-cmd --reload
8183 ```
8284
8385 With firewalld and Tailscale:
@@ -291,14 +293,9 @@ The `registry_listen_address` controls which interface the registry server binds
291293 registry_listen_address = "127.0.0.1" # Loopback only
292294 ```
293295
294-## Force HSTS
295-
296-When Gordon runs behind a TLS-terminating proxy (e.g. Cloudflare), `r.TLS` is nil so the HSTS header is not sent by default. Enable `force_hsts` to always include the `Strict-Transport-Security` header.
296+## HSTS
297297
298-```toml
299-[server]
300-force_hsts = true
301-```
298+HSTS headers are sent automatically on the HTTPS listener (when `r.TLS != nil`). When Gordon runs behind a TLS-terminating proxy (Cloudflare, Tailscale) and only receives plain HTTP, HSTS is not sent — the edge proxy is expected to handle it.
302299
303300 ## Examples
304301
@@ -6,12 +6,9 @@ Core server settings for Gordon.
6 6
7 ```toml7 ```toml
8 [server]8 [server]
9-port = 8080 # HTTP proxy port9+port = 8088 # HTTP proxy port
10 registry_port = 5000 # Container registry port10 registry_port = 5000 # Container registry port
11-tls_enabled = false # Enable native TLS listener11+tls_port = 8443 # HTTPS listener port (0 = disabled)
12-tls_port = 443 # HTTPS listener port
13-tls_cert_file = "" # PEM certificate file (required when TLS enabled)
14-tls_key_file = "" # PEM key file (required when TLS enabled)
15 gordon_domain = "gordon.mydomain.com" # Gordon domain (required)12 gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
16 # data_dir = "~/.gordon" # Data storage directory (default)13 # data_dir = "~/.gordon" # Data storage directory (default)
17 ```14 ```
@@ -20,12 +17,9 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
20 17
21 | Option | Type | Default | Description |18 | Option | Type | Default | Description |
22 |--------|------|---------|-------------|19 |--------|------|---------|-------------|
23-| `port` | int | `80` | HTTP proxy port for routing traffic to containers |20+| `port` | int | `8088` | HTTP proxy port for routing traffic to containers |
24 | `registry_port` | int | `5000` | Docker registry port for image push/pull |21 | `registry_port` | int | `5000` | Docker registry port for image push/pull |
25-| `tls_enabled` | bool | `false` | Enable native HTTPS listener for proxy traffic |22+| `tls_port` | int | `8443` | HTTPS listener port with internal CA. Set to `0` to disable TLS entirely |
26-| `tls_port` | int | `443` | HTTPS listener port when `tls_enabled` is true |
27-| `tls_cert_file` | string | `""` | Path to PEM certificate file |
28-| `tls_key_file` | string | `""` | Path to PEM private key file |
29 | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) |23 | `gordon_domain` | string | **required** | Domain for Gordon (registry + admin API) |
30 | `registry_domain` | string | - | Deprecated alias for `gordon_domain` |24 | `registry_domain` | string | - | Deprecated alias for `gordon_domain` |
31 | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files |25 | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files |
@@ -34,7 +28,6 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required)
34 | `registry_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |28 | `registry_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to access the registry (empty = allow all) |
35 | `proxy_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |29 | `proxy_allowed_ips` | []string | `[]` | IPs or CIDR ranges allowed to reach the proxy (empty = allow all) |
36 | `registry_listen_address` | string | `""` | Bind address for registry (empty = all interfaces) |30 | `registry_listen_address` | string | `""` | Bind address for registry (empty = all interfaces) |
37-| `force_hsts` | bool | `false` | Always send HSTS header (for TLS-terminating proxy setups) |
38 31
39 ## Port Configuration32 ## Port Configuration
40 33
@@ -54,30 +47,39 @@ For rootless containers, you'll typically use a high port and configure firewall
54 sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=808047 sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
55 ```48 ```
56 49
57-### Native TLS Listener50+### Internal CA and TLS
58 51
59-Enable native HTTPS directly in Gordon:52+Gordon includes an internal certificate authority that issues on-demand TLS certificates for proxied domains. The HTTPS listener is enabled by default on `tls_port` (8443).
60 53
61 ```toml54 ```toml
62 [server]55 [server]
63-tls_enabled = true56+tls_port = 8443 # Default. Set to 0 to disable TLS entirely.
64-tls_port = 443
65-tls_cert_file = "/etc/gordon/certs/fullchain.pem"
66-tls_key_file = "/etc/gordon/certs/privkey.pem"
67 ```57 ```
68 58
69 With TLS enabled:59 With TLS enabled:
70-- Gordon terminates HTTPS on `tls_port`60+- Gordon runs an internal CA (root + intermediate) stored in `{data_dir}/pki/`
71-- Host-based routing still works (apps, `/v2/*`, `/auth/*`, `/admin/*`)61+- Leaf certificates are issued on-demand per domain (SNI-based) and cached in memory
72-- `port` remains available for plain HTTP unless you restrict it externally62+- The intermediate CA auto-renews before expiry
73-- If `tls_cert_file`/`tls_key_file` are empty, Gordon auto-generates a self-signed pair in `{data_dir}/tls/`63+- An onboarding page at `http://{host}/ca` lets clients download the root CA certificate
64+- The root CA is stable across restarts (generated once, persisted to disk)
74 65
75-For rootless user services, do not bind directly to `443`. Use an unprivileged TLS port (for example `8443`) and forward `443` to it at the firewall or edge proxy.66+#### Disabling TLS
67+
68+For setups where TLS is handled externally (Tailscale, Cloudflare, etc.):
76 69
77 ```toml70 ```toml
78 [server]71 [server]
79-tls_enabled = true72+tls_port = 0 # No HTTPS listener, no internal CA generated
80-tls_port = 844373+```
74+
75+#### Firewall port forwarding
76+
77+For rootless user services, forward 443 to the high port at the firewall:
78+
79+```bash
80+# Forward 443 -> 8443
81+sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
82+sudo firewall-cmd --reload
81 ```83 ```
82 84
83 With firewalld and Tailscale:85 With firewalld and Tailscale:
@@ -291,14 +293,9 @@ The `registry_listen_address` controls which interface the registry server binds
291 registry_listen_address = "127.0.0.1" # Loopback only293 registry_listen_address = "127.0.0.1" # Loopback only
292 ```294 ```
293 295
294-## Force HSTS296+## HSTS
295-
296-When Gordon runs behind a TLS-terminating proxy (e.g. Cloudflare), `r.TLS` is nil so the HSTS header is not sent by default. Enable `force_hsts` to always include the `Strict-Transport-Security` header.
297 297
298-```toml298+HSTS headers are sent automatically on the HTTPS listener (when `r.TLS != nil`). When Gordon runs behind a TLS-terminating proxy (Cloudflare, Tailscale) and only receives plain HTTP, HSTS is not sent — the edge proxy is expected to handle it.
299-[server]
300-force_hsts = true
301-```
302 299
303 ## Examples300 ## Examples
304 301
M docs/getting-started.md +1 -1
@@ -6,7 +6,7 @@ Deploy your first app with Gordon in under 5 minutes.
66
77 - A Linux VPS with Docker or Podman installed
88 - A domain pointing to your VPS (DNS A record)
9-- HTTPS setup: Cloudflare proxy **or** native TLS in Gordon (`server.tls_enabled = true`)
9+- HTTPS setup: Cloudflare proxy, Tailscale, or Gordon's internal CA (`server.tls_port = 8443`)
1010 - [pass](https://www.passwordstore.org/) (password manager) with GPG key initialized
1111
1212 ## 1. Install Gordon
@@ -6,7 +6,7 @@ Deploy your first app with Gordon in under 5 minutes.
6 6
7 - A Linux VPS with Docker or Podman installed7 - A Linux VPS with Docker or Podman installed
8 - A domain pointing to your VPS (DNS A record)8 - A domain pointing to your VPS (DNS A record)
9-- HTTPS setup: Cloudflare proxy **or** native TLS in Gordon (`server.tls_enabled = true`)9+- HTTPS setup: Cloudflare proxy, Tailscale, or Gordon's internal CA (`server.tls_port = 8443`)
10 - [pass](https://www.passwordstore.org/) (password manager) with GPG key initialized10 - [pass](https://www.passwordstore.org/) (password manager) with GPG key initialized
11 11
12 ## 1. Install Gordon12 ## 1. Install Gordon
M docs/index.md +1 -1
@@ -80,7 +80,7 @@ Gordon runs on your VPS and provides:
8080 - Domain pointing to your server
8181 - Cloudflare account for HTTPS (free tier works)
8282
83-> **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly when `server.tls_enabled = true`.
83+> **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly via its internal CA (enabled by default on `server.tls_port`). Set `tls_port = 0` to disable.
8484
8585 ## Related
8686
@@ -80,7 +80,7 @@ Gordon runs on your VPS and provides:
80 - Domain pointing to your server80 - Domain pointing to your server
81 - Cloudflare account for HTTPS (free tier works)81 - Cloudflare account for HTTPS (free tier works)
82 82
83-> **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly when `server.tls_enabled = true`.83+> **Note:** Gordon can run behind Cloudflare/nginx, or terminate TLS directly via its internal CA (enabled by default on `server.tls_port`). Set `tls_port = 0` to disable.
84 84
85 ## Related85 ## Related
86 86
M internal/adapters/in/cli/local.go +1 -1
@@ -46,7 +46,7 @@ func (l *LocalServices) GetDataDir() string {
4646 func GetLocalServices(configPath string) (*LocalServices, error) {
4747 // Set up viper with defaults
4848 v := viper.New()
49- v.SetDefault("server.port", 80)
49+ v.SetDefault("server.port", 8088)
5050 v.SetDefault("server.registry_port", 5000)
5151 v.SetDefault("server.data_dir", app.DefaultDataDir())
5252
@@ -46,7 +46,7 @@ func (l *LocalServices) GetDataDir() string {
46 func GetLocalServices(configPath string) (*LocalServices, error) {46 func GetLocalServices(configPath string) (*LocalServices, error) {
47 // Set up viper with defaults47 // Set up viper with defaults
48 v := viper.New()48 v := viper.New()
49- v.SetDefault("server.port", 80)49+ v.SetDefault("server.port", 8088)
50 v.SetDefault("server.registry_port", 5000)50 v.SetDefault("server.registry_port", 5000)
51 v.SetDefault("server.data_dir", app.DefaultDataDir())51 v.SetDefault("server.data_dir", app.DefaultDataDir())
52 52
M internal/app/run.go +28 -16
@@ -375,13 +375,17 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
375375 return nil, log.WrapErr(err, "failed to load configuration")
376376 }
377377
378- // Create PKI (internal CA)
379- caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
380- if err != nil {
381- return nil, log.WrapErr(err, "failed to initialize internal CA")
378+ // Create PKI (internal CA) — disabled when tls_port is 0.
379+ if cfg.Server.TLSPort != 0 {
380+ caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381+ if err != nil {
382+ return nil, log.WrapErr(err, "failed to initialize internal CA")
383+ }
384+ svc.caAdapter = caAdapter
385+ svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
386+ } else {
387+ log.Info().Msg("internal CA disabled (server.tls_port=0)")
382388 }
383- svc.caAdapter = caAdapter
384- svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
385389
386390 var backend domain.SecretsBackend
387391 var passStore *domainsecrets.PassStore
@@ -1895,15 +1899,19 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
18951899 if err := waitForServerReady(proxyReady, errChan); err != nil {
18961900 return err
18971901 }
1898- if err := waitForServerReady(tlsReady, errChan); err != nil {
1899- return err
1902+ if tlsReady != nil {
1903+ if err := waitForServerReady(tlsReady, errChan); err != nil {
1904+ return err
1905+ }
19001906 }
19011907
1902- log.Info().
1908+ logEvent := log.Info().
19031909 Int("proxy_port", cfg.Server.Port).
1904- Int("tls_port", cfg.Server.TLSPort).
1905- Int("registry_port", cfg.Server.RegistryPort).
1906- Msg("Gordon is running")
1910+ Int("registry_port", cfg.Server.RegistryPort)
1911+ if cfg.Server.TLSPort != 0 {
1912+ logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
1913+ }
1914+ logEvent.Msg("Gordon is running")
19071915
19081916 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)
19091917 if err != nil {
@@ -2166,8 +2174,8 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
21662174 log.Info().Msg("Gordon stopped")
21672175 }
21682176
2169-// startProxyServers sets up the HTTP proxy server and the HTTPS proxy server
2170-// with on-demand TLS certificates from the internal CA.
2177+// startProxyServers sets up the HTTP proxy server and, when tls_port != 0,
2178+// an HTTPS proxy server with on-demand TLS certificates from the internal CA.
21712179 func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSvc *pkiusecase.Service, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
21722180 // HTTP listener (Cloudflare proxy + onboarding for direct clients)
21732181 var httpProtos http.Protocols
@@ -2182,6 +2190,10 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
21822190 log,
21832191 )
21842192
2193+ if cfg.Server.TLSPort == 0 {
2194+ return httpSrv, httpReady, nil, nil, nil
2195+ }
2196+
21852197 // HTTPS listener (on-demand TLS via internal CA)
21862198 tlsConfig := &tls.Config{
21872199 MinVersion: tls.VersionTLS12,
@@ -2521,9 +2533,9 @@ func isProcessAlive(pid int) bool {
25212533
25222534 // loadConfig loads configuration from file and sets defaults.
25232535 func loadConfig(v *viper.Viper, configPath string) error {
2524- v.SetDefault("server.port", 80)
2536+ v.SetDefault("server.port", 8088)
25252537 v.SetDefault("server.registry_port", 5000)
2526- v.SetDefault("server.tls_port", 443)
2538+ v.SetDefault("server.tls_port", 8443)
25272539 v.SetDefault("server.data_dir", DefaultDataDir())
25282540 v.SetDefault("server.runtime", "auto")
25292541 v.SetDefault("logging.level", "info")
@@ -375,13 +375,17 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
375 return nil, log.WrapErr(err, "failed to load configuration")375 return nil, log.WrapErr(err, "failed to load configuration")
376 }376 }
377 377
378- // Create PKI (internal CA)378+ // Create PKI (internal CA) — disabled when tls_port is 0.
379- caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)379+ if cfg.Server.TLSPort != 0 {
380- if err != nil {380+ caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381- return nil, log.WrapErr(err, "failed to initialize internal CA")381+ if err != nil {
382+ return nil, log.WrapErr(err, "failed to initialize internal CA")
383+ }
384+ svc.caAdapter = caAdapter
385+ svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
386+ } else {
387+ log.Info().Msg("internal CA disabled (server.tls_port=0)")
382 }388 }
383- svc.caAdapter = caAdapter
384- svc.pkiSvc = pkiusecase.NewService(ctx, caAdapter, svc.configSvc, log)
385 389
386 var backend domain.SecretsBackend390 var backend domain.SecretsBackend
387 var passStore *domainsecrets.PassStore391 var passStore *domainsecrets.PassStore
@@ -1895,15 +1899,19 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
1895 if err := waitForServerReady(proxyReady, errChan); err != nil {1899 if err := waitForServerReady(proxyReady, errChan); err != nil {
1896 return err1900 return err
1897 }1901 }
1898- if err := waitForServerReady(tlsReady, errChan); err != nil {1902+ if tlsReady != nil {
1899- return err1903+ if err := waitForServerReady(tlsReady, errChan); err != nil {
1904+ return err
1905+ }
1900 }1906 }
1901 1907
1902- log.Info().1908+ logEvent := log.Info().
1903 Int("proxy_port", cfg.Server.Port).1909 Int("proxy_port", cfg.Server.Port).
1904- Int("tls_port", cfg.Server.TLSPort).1910+ Int("registry_port", cfg.Server.RegistryPort)
1905- Int("registry_port", cfg.Server.RegistryPort).1911+ if cfg.Server.TLSPort != 0 {
1906- Msg("Gordon is running")1912+ logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
1913+ }
1914+ logEvent.Msg("Gordon is running")
1907 1915
1908 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)1916 schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)
1909 if err != nil {1917 if err != nil {
@@ -2166,8 +2174,8 @@ func gracefulShutdown(registrySrv, proxySrv, tlsSrv *http.Server, containerSvc *
2166 log.Info().Msg("Gordon stopped")2174 log.Info().Msg("Gordon stopped")
2167 }2175 }
2168 2176
2169-// startProxyServers sets up the HTTP proxy server and the HTTPS proxy server2177+// startProxyServers sets up the HTTP proxy server and, when tls_port != 0,
2170-// with on-demand TLS certificates from the internal CA.2178+// an HTTPS proxy server with on-demand TLS certificates from the internal CA.
2171 func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSvc *pkiusecase.Service, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {2179 func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSvc *pkiusecase.Service, errChan chan<- error, log zerowrap.Logger) (*http.Server, <-chan struct{}, *http.Server, <-chan struct{}, error) {
2172 // HTTP listener (Cloudflare proxy + onboarding for direct clients)2180 // HTTP listener (Cloudflare proxy + onboarding for direct clients)
2173 var httpProtos http.Protocols2181 var httpProtos http.Protocols
@@ -2182,6 +2190,10 @@ func startProxyServers(cfg Config, httpHandler, httpsHandler http.Handler, pkiSv
2182 log,2190 log,
2183 )2191 )
2184 2192
2193+ if cfg.Server.TLSPort == 0 {
2194+ return httpSrv, httpReady, nil, nil, nil
2195+ }
2196+
2185 // HTTPS listener (on-demand TLS via internal CA)2197 // HTTPS listener (on-demand TLS via internal CA)
2186 tlsConfig := &tls.Config{2198 tlsConfig := &tls.Config{
2187 MinVersion: tls.VersionTLS12,2199 MinVersion: tls.VersionTLS12,
@@ -2521,9 +2533,9 @@ func isProcessAlive(pid int) bool {
2521 2533
2522 // loadConfig loads configuration from file and sets defaults.2534 // loadConfig loads configuration from file and sets defaults.
2523 func loadConfig(v *viper.Viper, configPath string) error {2535 func loadConfig(v *viper.Viper, configPath string) error {
2524- v.SetDefault("server.port", 80)2536+ v.SetDefault("server.port", 8088)
2525 v.SetDefault("server.registry_port", 5000)2537 v.SetDefault("server.registry_port", 5000)
2526- v.SetDefault("server.tls_port", 443)2538+ v.SetDefault("server.tls_port", 8443)
2527 v.SetDefault("server.data_dir", DefaultDataDir())2539 v.SetDefault("server.data_dir", DefaultDataDir())
2528 v.SetDefault("server.runtime", "auto")2540 v.SetDefault("server.runtime", "auto")
2529 v.SetDefault("logging.level", "info")2541 v.SetDefault("logging.level", "info")
M internal/usecase/config/service_test.go +2 -2
@@ -1412,8 +1412,8 @@ backend = ["app.example.com"]
14121412
14131413 v := viper.New()
14141414 v.SetConfigFile(configFile)
1415- v.SetDefault("server.port", 80)
1416- v.SetDefault("server.tls_port", 443)
1415+ v.SetDefault("server.port", 8088)
1416+ v.SetDefault("server.tls_port", 8443)
14171417 v.SetDefault("auth.secrets_backend", "")
14181418 v.SetDefault("auto_route.enabled", false)
14191419 v.SetDefault("network_isolation.enabled", false)
@@ -1412,8 +1412,8 @@ backend = ["app.example.com"]
1412 1412
1413 v := viper.New()1413 v := viper.New()
1414 v.SetConfigFile(configFile)1414 v.SetConfigFile(configFile)
1415- v.SetDefault("server.port", 80)1415+ v.SetDefault("server.port", 8088)
1416- v.SetDefault("server.tls_port", 443)1416+ v.SetDefault("server.tls_port", 8443)
1417 v.SetDefault("auth.secrets_backend", "")1417 v.SetDefault("auth.secrets_backend", "")
1418 v.SetDefault("auto_route.enabled", false)1418 v.SetDefault("auto_route.enabled", false)
1419 v.SetDefault("network_isolation.enabled", false)1419 v.SetDefault("network_isolation.enabled", false)

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