diff --git a/docs/install/ubuntu.md b/docs/install/ubuntu.md index 586eceb..a768da7 100644 --- a/docs/install/ubuntu.md +++ b/docs/install/ubuntu.md @@ -98,4 +98,18 @@ sudo apt update && sudo apt upgrade simpledeploy Schema migrations run automatically on the next start. See [Upgrading](/install/upgrading/) for rollback notes. +## Troubleshooting + +### `status=226/NAMESPACE` or `Failed to set up mount namespacing` + +Old releases (< the systemd unit fix) listed a path in `ReadWritePaths` that the package never created, and let Caddy's ACME state fall under `/root/.local/share/caddy`, which the unit's `ProtectHome=true` blocks. Both are fixed in current releases. If you hit this on an existing host: + +```bash +sudo apt update && sudo apt upgrade simpledeploy +sudo systemctl daemon-reload +sudo systemctl restart simpledeploy +``` + +ACME state now lives under `/var/lib/simpledeploy/caddy/`. If you previously had certs under `/root/.local/share/caddy/`, you can copy them over to skip a re-issue, or just let Let's Encrypt re-issue on next request. + Next: [First deploy](/first-deploy/prepare/). diff --git a/docs/reference/directory-layout.md b/docs/reference/directory-layout.md index ff985b3..e0a46be 100644 --- a/docs/reference/directory-layout.md +++ b/docs/reference/directory-layout.md @@ -29,12 +29,12 @@ Default: `/var/lib/simpledeploy/`. ├── backups/ # local backup target output │ └── / │ └── 2026-04-17T02-00-00Z.sql.gz -└── caddy/ # Caddy storage (only when tls.mode is "local") +└── caddy/ # Caddy storage (ACME state, certs, locks) ├── certificates/ └── locks/ ``` -For `tls.mode: auto`, Caddy stores ACME state and certificates under its default location (`$HOME/.local/share/caddy` for the user running `simpledeploy`, typically `/root/.local/share/caddy` under systemd). For `tls.mode: local`, state lives under `data_dir/caddy/`. For `tls.mode: custom`, you provide cert files and SimpleDeploy reads them from the path declared by each app. +Caddy storage lives under `data_dir/caddy/` for both `tls.mode: auto` (Let's Encrypt ACME state and issued certs) and `tls.mode: local` (self-signed CA). For `tls.mode: custom`, you provide cert files and SimpleDeploy reads them from the path declared by each app. Permissions: diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 2549a82..af4884d 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -235,7 +235,10 @@ func (c *CaddyProxy) buildConfig() map[string]interface{} { }, } - if (c.tlsMode == "local" || needsLocalTLS) && c.dataDir != "" { + // Pin Caddy storage under data_dir for all TLS modes. Without this, + // certmagic falls back to $HOME/.local/share/caddy, which is masked by + // systemd's ProtectHome=true in the shipped unit and breaks tls.mode=auto. + if c.dataDir != "" { cfg["storage"] = map[string]interface{}{ "module": "file_system", "root": filepath.Join(c.dataDir, "caddy"), diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 4706811..f5e85d4 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -200,6 +200,32 @@ func TestBuildConfigTLSLocalStorage(t *testing.T) { } } +func TestBuildConfigTLSAutoStorage(t *testing.T) { + // Storage must be pinned for auto mode too, otherwise certmagic falls + // back to $HOME/.local/share/caddy which ProtectHome=true blocks under + // the shipped systemd unit. + dataDir := "/tmp/testdata" + p := NewCaddyProxy(CaddyConfig{ + ListenAddr: ":443", + TLSMode: "auto", + TLSEmail: "ops@example.com", + DataDir: dataDir, + }) + cfg := parseConfig(t, p) + + storage, ok := cfg["storage"].(map[string]interface{}) + if !ok { + t.Fatal("storage not set for tls.mode=auto") + } + if storage["module"] != "file_system" { + t.Errorf("storage.module: got %v, want \"file_system\"", storage["module"]) + } + wantRoot := dataDir + "/caddy" + if storage["root"] != wantRoot { + t.Errorf("storage.root: got %v, want %q", storage["root"], wantRoot) + } +} + func TestBuildConfigMixedLocalAndOff(t *testing.T) { p := NewCaddyProxy(CaddyConfig{ ListenAddr: ":443", diff --git a/simpledeploy.service b/simpledeploy.service index e329e8c..b967d66 100644 --- a/simpledeploy.service +++ b/simpledeploy.service @@ -9,6 +9,12 @@ ExecStart=/usr/local/bin/simpledeploy serve --config /etc/simpledeploy/config.ya Restart=always RestartSec=5 +# Point HOME at a writable, allow-listed path. ProtectHome=true masks /root, +# so anything that consults $HOME (docker CLI config, certmagic ACME fallback, +# future deps) needs an alternate writable HOME. /var/lib/simpledeploy is +# already in ReadWritePaths below. +Environment=HOME=/var/lib/simpledeploy + # --- Hardening --- # SimpleDeploy must run as root by default because it talks to the docker # daemon socket and binds privileged ports (80/443) via Caddy. The @@ -23,7 +29,7 @@ NoNewPrivileges=true # Read-only system tree, with explicit writable paths for state. ProtectSystem=strict -ReadWritePaths=/etc/simpledeploy /var/lib/simpledeploy /var/log/simpledeploy +ReadWritePaths=/etc/simpledeploy /var/lib/simpledeploy ProtectHome=true PrivateTmp=true