Skip to content

Harden updater container isolation - #667

Merged
brettfo merged 1 commit into
mainfrom
harden-updater-container
Oct 7, 2026
Merged

brettfo merged 1 commit into
mainfrom
harden-updater-container

Conversation

@brettfo

@brettfo brettfo commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Harden the untrusted Dependabot updater container while preserving the existing trust boundary: the updater remains non-root and internal-only, raw credentials remain in the trusted proxy, and the proxy remains the only component connected to both internal and internet-capable networks.

This is defense-in-depth hardening. It does not address a demonstrated credential disclosure.

Updater container security controls

internal/infra/updater.go now constructs the updater HostConfig with:

  • CapDrop: ALL
  • no CapAdd entries
  • no-new-privileges=true
  • seccomp=builtin

Dropping every capability is compatible with the updater because package-manager and repository code runs as the existing dependabot user and does not need privileged kernel operations. No capability was added back. no-new-privileges prevents updater processes from gaining privilege through setuid/setgid binaries or file capabilities. The explicit built-in seccomp selection preserves Docker's reviewed default syscall policy and never uses seccomp=unconfined.

The updater remains attached only to the no-internet network. The proxy continues to bridge the internal and internet-capable networks and remains solely responsible for resolving and injecting credentials into outbound requests.

Fail-closed seccomp compatibility

Before creating any updater or case-insensitive storage resources, the CLI calls Docker Info and decodes SecurityOptions with the pinned Docker system.DecodeSecurityOptions helper.

This accepts both daemon representations that indicate seccomp support:

  • modern: name=seccomp,profile=builtin
  • legacy Docker API 1.24: seccomp

Malformed security options are returned with decoding context. If seccomp is genuinely absent, updater creation fails with ErrSeccompUnavailable; there is no retry or fallback that removes the security option or runs the updater unconfined.

Request-level tests use API 1.44 for the modern representation and the actual minimum API 1.24 for the legacy representation. The legacy test requires /v1.24/info and /v1.24/containers/create, so it verifies protocol compatibility rather than only supplying a legacy value to a modern client.

Checked required setup commands

The main updater command intentionally records its exit code in Updater.ExitCode, so its existing RunCmd behavior is unchanged. Required setup operations now use a separate checked streaming execution path and stop the update immediately on any nonzero exit:

  • update-ca-certificates
  • case-insensitive git safe.directory configuration
  • creation of the local staging directory
  • local Git initialization, identity configuration, staging, and commit

This prevents setup failures such as a missing git executable from being mistaken for successful preparation.

Capability-free local staging

Local source files are no longer prepared by granting updater-wide ownership capabilities. The CLI:

  1. resolves the numeric UID and GID of the existing dependabot user;
  2. resolves the host source directory to an absolute path for reliable Windows archive traversal;
  3. creates the archive with the already pinned moby/go-archive TarWithOptions API;
  4. assigns the updater UID/GID through ChownOpts; and
  5. copies that archive into the updater through the Docker API.

This makes the staged files writable by the updater without CAP_CHOWN, CAP_DAC_READ_SEARCH, a root chown, or any persistent CapAdd entry.

Local Git setup preserves an existing repository instead of reinitializing it, which retains settings such as Windows core.filemode=false. A clean existing repository remains on its original commit. Dirty or non-repository sources receive the Dependabot CLI automated commit so updater changes are not lost. Unexpected Git failures still propagate; only the expected clean staged-diff result is treated as success.

Case-insensitive filesystem compatibility and cleanup

The CIFS mounts retain noperm, allowing the non-root updater to access files whose ownership is presented by CIFS rather than granting Linux capabilities. The updater configures /dpdbot/repo as a Git safe directory only when the case-insensitive experiment is enabled.

Case-insensitive storage initialization now has explicit resource ownership and rollback:

  • failures to start the storage container, wait for SMB readiness, or inspect its network address remove the storage container and generated volumes;
  • failure to create the updater after storage initialization also rolls those resources back;
  • rollback uses a bounded 30-second cleanup context independent of an already-cancelled job context;
  • cleanup errors are logged without replacing the primary initialization error; and
  • after successful updater creation, cleanup ownership transfers to Updater.Close, which removes the updater, storage volumes, and storage container at the end of the run.

This avoids leaking case-insensitive storage resources on partial initialization while preventing double cleanup after ownership transfer.

Tests and fixtures

Request and unit coverage

internal/infra/updater_test.go now verifies:

  • updater ContainerCreate uses the non-root user;
  • all capabilities are dropped and none are added;
  • no-new-privileges=true and seccomp=builtin are explicit and never unconfined;
  • only the no-internet network is attached;
  • existing mounts and updater environment/configuration remain present;
  • modern Docker seccomp detection uses API 1.44;
  • legacy seccomp detection genuinely uses API 1.24;
  • absent or malformed seccomp prevents updater ContainerCreate;
  • CIFS mount construction preserves the security settings and includes noperm;
  • storage initialization and updater-creation failures clean up exactly once; and
  • successful creation transfers cleanup ownership to Updater.Close.

internal/infra/run_test.go verifies:

  • failures from every required setup category propagate;
  • only the case-insensitive experiment configures the CIFS Git safe directory; and
  • the tar stream supplied to Docker contains the updater UID/GID and the expected files.

Runtime/script coverage

The runtime Dockerfiles use portable COPY plus RUN chmod instructions so the repository's restricted script harness can use Docker's legacy builder without requiring BuildKit.

The focused scripts verify:

  • normal updater execution: CapEff=0, NoNewPrivs=1, Seccomp=2;
  • local staging: the same controls plus a successful non-root write;
  • case-insensitive storage: the same controls and case-insensitive lookup;
  • combined --local plus case-insensitive execution: the same controls, successful local staging onto /dpdbot/repo, and a real Git operation without a dubious-ownership error;
  • existing clean Git repositories retain their commit;
  • dirty repositories receive the automated commit; and
  • the portable dirty-worktree fixture uses git apply, avoiding shell-redirection differences across platforms.

Validation

Validated from commit cf22640 on October 6, 2026:

  • go test -count=1 ./internal/infra
  • focused security, local, smb-mount, and smb-mount-local script tests with DOCKER_BUILDKIT and custom Docker configuration removed
  • go test -run '^$' ./...
  • go vet ./...
  • go mod tidy -diff
  • gofmt -l .
  • git diff --check HEAD^..HEAD

The focused runtime tests reached and passed all assertions for CapEff=0, NoNewPrivs=1, and Seccomp=2 in normal, local, case-insensitive, and combined local-plus-case-insensitive flows.

Representative update tests against brettfo/db-test also passed from the same HEAD:

  • npm/yarn, directory /npm-version-range: pad-left 2.0.1 -> 2.1.0
  • NuGet with use_case_insensitive_filesystem, directory /casing: Newtonsoft.Json 13.0.1 -> 13.0.4

Both output files contained create_pull_request, and neither contained the raw GitHub access token.

Compatibility and rollout

Docker hosts must advertise seccomp through either the modern or legacy Docker Info security-option format. Hosts without kernel/daemon seccomp support now fail before updater creation instead of silently running unconfined.

Downstream Azure DevOps integrations must use the first released Dependabot CLI version that contains this change. That release version has not yet been assigned.

Run updater containers as the existing non-root user while dropping all Linux capabilities, enabling no-new-privileges, and explicitly selecting Docker's built-in seccomp profile. Reject Docker hosts that do not advertise seccomp support, including correct handling of modern and legacy API security-option formats.

Keep local staging capability-free by assigning the updater UID and GID through moby/go-archive, and run required certificate, repository, and case-insensitive setup commands with checked exit handling. Preserve existing repository configuration, CIFS behavior, internal-only updater networking, proxy credential separation, and main updater exit semantics.

Roll back case-insensitive storage containers and volumes on initialization failures with bounded cleanup and explicit ownership transfer to Updater on success. Cover container requests, minimum Docker API compatibility, setup failures, cleanup paths, archive ownership, and effective runtime controls for normal, local, case-insensitive, and combined flows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 090f48f7-de21-4eb1-b076-8c5852c16f14
@brettfo
brettfo requested review from a team as code owners October 6, 2026 22:06
@JamieMagee
JamieMagee requested a balanced review from Copilot October 6, 2026 22:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The security controls and cleanup paths are well covered; only a non-blocking fixture spelling correction was noted.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Hardens updater container isolation and makes setup failures fail closed while preserving local and case-insensitive workflows.

Changes:

  • Drops all updater capabilities and explicitly enables seccomp and no-new-privileges.
  • Adds checked setup execution, capability-free local staging, and bounded storage rollback.
  • Expands unit and runtime security coverage.
File Description
internal/​infra/​updater.go Adds container hardening, seccomp checks, checked execution, and storage cleanup.
internal/​infra/​updater_test.go Tests security configuration and cleanup ownership.
internal/​infra/​run.go Adds checked setup and UID/GID-aware local staging.
internal/​infra/​run_test.go Tests setup failure propagation and archive ownership.
testdata/​scripts/​security.txt Verifies runtime security and writable local staging.
testdata/​scripts/​smb-mount.txt Verifies case-insensitive runtime security.
testdata/​scripts/​smb-mount-local.txt Covers combined local and case-insensitive operation.
testdata/​scripts/​local.txt Tests clean and dirty local repository handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread testdata/scripts/local.txt
@brettfo
brettfo added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 7, 2026
@brettfo
brettfo added this pull request to the merge queue Oct 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 7, 2026
@brettfo
brettfo added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit e166a3e Oct 7, 2026
206 of 210 checks passed
@brettfo
brettfo deleted the harden-updater-container branch October 7, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants