Skip to content

feat: ship a container image on ghcr.io - #106

Merged
plusky merged 1 commit into
mainfrom
feat/container-image
Aug 18, 2026
Merged

feat: ship a container image on ghcr.io#106
plusky merged 1 commit into
mainfrom
feat/container-image

Conversation

@plusky

@plusky plusky commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Multi-arch (amd64/arm64) container image at ghcr.io/plusky/bugwarden, modeled on ruoqa-mcp's container commit and adapted: workspace build, digest-pinned bases (both FROM lines, with a docker dependabot ecosystem to move them), BUGWARDEN_POLICY preset so a missing policy mount fails closed instead of inheriting allow-all (I9, recorded on DESIGN.md's CLI table row), native-runner release matrix with digest-join manifest, build-only CI leg behind a paths filter, README Container section, compose.yaml with hardening (read_only, cap_drop: ALL, no-new-privileges, init).

Verified on the built image (podman, both arches)

  • No token + no --insecure-no-auth → refuses before binding, exit 1 (nothing listening on the published port).
  • Write token → serves; unauthenticated/wrong-token requests get the byte-identical empty 401.
  • Read token → tools/list 11 vs 20; the 9 removed are exactly WRITE_TOOLS; calling one returns the router's own "tool not found".
  • No policy mount → startup error naming the path, no port bound.
  • Real TLS fetch from bugzilla.opensuse.org inside the image (distroless CA bundle + rustls-platform-verifier).
  • Hardened run passes; --init turns 13s SIGKILL stops into 1s exit 143.

Adversarial review before opening

Two reviewers, both initially NOT MERGE-SAFE; all findings fixed: the docs previously described the bearer gate before it existed (resolved by landing #105 first and re-verifying against the real gate); a false custody claim reproduced against the binary; a fabricated cross-reference (twice); .dockerignore/Dockerfile comments describing mechanisms that don't exist; tag-pinned bases vs the repo's SHA discipline (now digest-pinned; the fix pass caught its own first attempt producing two wrong digests); compose's audit-volume example that couldn't start; the DESIGN.md re-evaluation now dated per the file's DECIDED precedent. All four new action SHAs verified against upstream tags.

Verification

All five AGENTS.md commands independently re-run on the rebased tree: fmt/clippy×2 clean, 471 passed / 0 failed, deny fully green. actionlint clean; all YAML parses; zero broken README anchors.

Known follow-up (issue to come): bugwarden handles only SIGINT, so as PID 1 without --init a docker stop takes the 13s SIGKILL path — k8s has no --init, so the proper fix is a SIGTERM handler in main.rs.

Publish a multi-architecture (amd64/arm64) image built from a
digest-pinned rust:1.97.0-alpine stage onto a digest-pinned
gcr.io/distroless/static-debian13:nonroot, for HTTP-transport
deployments that would otherwise need a Rust toolchain. The release
workflow builds each architecture on a native runner and joins the
digests into one manifest only after both push, so a tag never resolves
to a half-populated multi-arch image; the container jobs are siblings
of publish, not upstream of it, so a broken image cannot hold back a
crates.io release whose binaries already shipped. CI gains a build-only
amd64 leg, gated on paths-filter because a full image build would
otherwise be the slowest job on every PR. A docker dependabot ecosystem
keeps the pinned base digests moving.

The image presets BUGWARDEN_POLICY, which turns the bare binary's
allow-all fallback into a startup error when the operator mounts no
policy: the one deployment shape that fails closed by construction
(tightening only, I9). MCP_HOST defaults to 0.0.0.0 because a
namespace-local bind is unreachable; the bearer gate, not the bind
address, is the access control, and the image refuses to start without
a token unless --insecure-no-auth is passed.

docs/DESIGN.md's TLS note carried a standing trigger — re-evaluate the
OS-trust-store decision if this project ever ships its own container
image. RE-EVALUATED 2026-08-18: the decision stands. The distroless
base carries the ca-certificates bundle, so the image works
unconfigured while an operator behind an internal CA can still mount
over the bundle's path, which roots baked into the binary would take
away.
@plusky
plusky merged commit c80c207 into main Aug 18, 2026
13 checks passed
@plusky
plusky deleted the feat/container-image branch August 18, 2026 12:36
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.

1 participant