diff --git a/charts/durpdeploy/templates/deployment.yaml b/charts/durpdeploy/templates/deployment.yaml index 857ad4b..9f1e888 100644 --- a/charts/durpdeploy/templates/deployment.yaml +++ b/charts/durpdeploy/templates/deployment.yaml @@ -47,6 +47,10 @@ spec: containerPort: {{ .Values.service.targetPort }} protocol: TCP env: + # This pod intentionally has no mount capabilities. Refuse to run + # deployment scripts rather than exposing pod credentials. + - name: DURPDEPLOY_REQUIRE_SANDBOX + value: "1" # --- Database (Postgres DSN) --- # postgres://user:password@host:port/db?sslmode=... # ponytail: assembled inline rather than via a Secret+envFrom diff --git a/compose.yml b/compose.yml index 1bbf435..01b6a38 100644 --- a/compose.yml +++ b/compose.yml @@ -18,6 +18,9 @@ services: # Don't publish it to the host. environment: DURPDEPLOY_DB: /data/durpdeploy.db + # Fail closed: this profile does not grant the privileges needed for + # chroot isolation, so deployment scripts must never run beside the DB. + DURPDEPLOY_REQUIRE_SANDBOX: "1" # Optional. The mounted secret file at /etc/durpdeploy/key is the # primary path; secret/secret.go reads file first, env fallback. Set # this only when delivering the key via env (k8s, swarm, etc.). diff --git a/internal/runner/runner.go b/internal/runner/runner.go index cea2030..84ed1bc 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -195,6 +195,12 @@ func (r *DeploymentRunner) runStepAttempt( // mounts aren't permitted, e.g. local dev without CAP_SYS_ADMIN. chrooted := r.sandbox.setupChroot(tmpDir) defer r.sandbox.teardownChroot(tmpDir) + if os.Getenv("DURPDEPLOY_REQUIRE_SANDBOX") == "1" && + !r.sandbox.isolated(chrooted) { + return fmt.Errorf( + "runner sandbox is required but credential and chroot isolation are unavailable", + ) + } var cmd *exec.Cmd if chrooted { diff --git a/internal/runner/sandbox_linux.go b/internal/runner/sandbox_linux.go index 448ae97..61fc28e 100644 --- a/internal/runner/sandbox_linux.go +++ b/internal/runner/sandbox_linux.go @@ -52,6 +52,10 @@ type Sandbox struct { chrootWarned atomic.Bool } +func (s *Sandbox) isolated(chrooted bool) bool { + return s.enabled && chrooted +} + // newSandbox looks up the durpdeploy-runner account. If it does not exist // (e.g. local dev/CI where docs/deploy.md Step 5 was never run), the // sandbox is disabled and steps keep running as the server's own user — diff --git a/internal/runner/sandbox_other.go b/internal/runner/sandbox_other.go index 32d63e2..465d569 100644 --- a/internal/runner/sandbox_other.go +++ b/internal/runner/sandbox_other.go @@ -16,6 +16,8 @@ type Sandbox struct{} func newSandbox() *Sandbox { return &Sandbox{} } +func (s *Sandbox) isolated(chrooted bool) bool { return false } + func (s *Sandbox) applyCredential(cmd *exec.Cmd) {} func (s *Sandbox) createCgroup(deploymentID int64) string { return "" }