Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,15 @@ the image:

```powershell
cb add jq-corp --image registry.corp.example/devtools/jq:1.8.1
# For an intentionally local image, declare that identity explicitly:
cb add jq-local --image jq-local:dev --local
```

If a lockfile exists, it becomes intentionally incomplete until you run
`cb update jq-corp` or `cb lock`; execution fails closed in the meantime.
`cb update jq-corp` or `cb lock`; execution fails closed in the meantime. A
profile added with `--local` must be locked explicitly with
`cb update --local jq-local` or `cb lock --local jq-local`; the flag never
infers local identity from Docker metadata.
State, environment allowlists, path rules, command overrides, and mounts still
require an explicit reviewed registry edit followed by `cb install`.

Expand All @@ -199,10 +204,7 @@ and `stateful` profiles alike.
[tools.token-meter]
image = "example/token-meter:latest"
provider = "stateless"
host_mounts = [
"%USERPROFILE%\\.claude:/root/.claude:ro",
"%USERPROFILE%\\.codex:/root/.codex:ro",
]
host_mounts = ["%USERPROFILE%\\.claude:/root/.claude:ro", "%USERPROFILE%\\.codex:/root/.codex:ro"]
```

Entries follow the `SOURCE:/CONTAINER_PATH:MODE` shape used by
Expand Down Expand Up @@ -614,6 +616,20 @@ An image-ID lock is deliberately host-local: it makes execution immutable on
that Docker daemon, but it does not make the image portable or pullable. Keep
the Dockerfile/build inputs or export the image separately for recovery.

## Enterprise machine policy

Administrators can constrain resolved user configuration through a fixed,
machine-owned policy at `C:\ProgramData\ContainerBin\policy.toml` (Windows) or
`/etc/container-bin/policy.toml` (native Linux/WSL). A missing policy preserves
unmanaged behavior. A present but unreadable, invalid, expired, unsupported or
insufficiently protected policy fails closed before non-bootstrap work.

Schema 1 can require an exact image lock, reject local image-ID locks unless
explicitly allowed, and allowlist canonical registry/repository boundaries.
Lower-precedence registry or command-line choices cannot weaken it. See
[enterprise machine policy](docs/enterprise-policy.md) for the schema,
ownership rules, normalization behavior and stable diagnostic codes.

## State inspection and garbage collection

```powershell
Expand Down
30 changes: 25 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ is configuration (`container-bin.toml`), a generated lockfile
```
NAME.exe (hardlink to cb.exe)
→ argv[0] dispatch main() inspects its own invocation name
→ machine policy load fixed admin path, ownership/version validated
→ registry profile lookup container-bin.toml, schema-validated, fail-closed
→ argv normalization repair PowerShell-split "-opt=" "value" pairs
→ path mapping conservative Windows→container translation
→ host_mounts resolution explicit registry-declared bind mounts, provider-agnostic
→ provider assembly stateless | python | stateful volume/env setup
→ image lock resolution container-bin.lock digest, fail-closed
→ policy authorization lock/local-origin/repository constraints
→ docker run --rm ... stdio passthrough, exit code preserved
```

Expand Down Expand Up @@ -166,6 +168,21 @@ a fresh matching RepoDigest, while local locks only re-inspect the configured
tag and record its current image ID. A missing local tag is an error, not an
implicit switch to a registry image. `cb update --local TOOL` and
`cb update --registry TOOL` are the explicit mode-switch operations.
Repository entries are accepted only as an immutable, valid SHA-256 RepoDigest
whose repository matches the configured reference after Docker Hub alias
normalization; a mutable tag or foreign repository in a hand-edited lockfile is
invalid.

## Enterprise policy

`internal/policy` is deliberately independent of registry parsing. `main`
loads it from the fixed machine path before the user registry, then passes the
immutable result to request resolution and diagnostics. The zero value means
unmanaged operation. Managed policy authorizes the final configured image plus
its lock identity; repository-mode lock creation is authorized before pull.
This preserves the precedence boundary: user/project/CLI layers may choose a
request, but only the machine layer can authorize it. Full schema and ownership
rules are in [enterprise-policy.md](enterprise-policy.md).

## Atomic writes

Expand Down Expand Up @@ -229,6 +246,7 @@ internal/state cb state, cb gc
↓
internal/dockervol docker volume primitives (leaf)
internal/lockfile container-bin.lock, digest resolution
internal/policy fixed machine policy, ownership and authorization
↓
internal/pathmap Windows path classification and mapping, project roots,
volume naming
Expand All @@ -245,14 +263,16 @@ The exact import edges, from `go list -f '{{.ImportPath}} {{.Imports}}' ./...`,
project-internal imports only:

```
main -> cli, diag, dockerrun, mutationlock, registry, state
cli -> atomicio, diag, dockerrun, lockfile, pathmap, registry, toml
diag -> dockerrun, dockervol, lockfile, pathmap, registry
dockerrun -> dockervol, lockfile, pathmap, registry
main -> cli, diag, dockerrun, mutationlock, policy, registry, state
cli -> atomicio, diag, dockerrun, lockfile, pathmap, policy, registry, statearchive, toml
diag -> dockerrun, dockervol, lockfile, pathmap, policy, registry
dockerrun -> dockervol, lockfile, pathmap, policy, registry
state -> dockervol, pathmap, registry
lockfile -> atomicio, registry, toml
statearchive -> dockervol, pathmap
lockfile -> atomicio, policy, registry, toml
pathmap -> registry
registry -> atomicio, toml
policy -> toml
atomicio, dockervol, mutationlock, toml -> (leaves)
```

Expand Down
114 changes: 114 additions & 0 deletions docs/enterprise-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Enterprise machine policy

ContainerBin can apply a fixed, administrator-owned authorization policy after
the user registry, lockfile and command line have resolved a request. The
policy is a constraint layer, not another registry: it cannot add profiles,
change their settings or be weakened by `container-bin.toml`.

## Location and protection

The location is compiled in and has no environment-variable or command-line
override:

- Windows: `C:\ProgramData\ContainerBin\policy.toml`
- native Linux and WSL: `/etc/container-bin/policy.toml`

A missing file means unmanaged operation and preserves the existing behavior.
A present file must be readable, valid and sufficiently protected. Otherwise
every non-bootstrap invocation fails before registry mutation or Docker use.
`cb version`, `cb config` and help remain available for recovery.

On Windows, neither the policy nor its parent directory may be a reparse point.
Both must be owned by `SYSTEM` or the built-in Administrators group. The policy
file may not grant write, modify, delete, ownership or permission-changing
rights to another principal. The parent may allow users to create entries, but
may not let them delete or replace protected children or change ownership or
permissions. A user-created lookalike still fails the file-owner check.

On Linux/WSL, both the file and parent directory must be real paths owned by
root and may not be group- or world-writable.

ContainerBin never creates or edits this file. Provision it and its ACL/mode
with the machine's normal administrator configuration-management mechanism.

## Schema 1

```toml
policy_version = 1
require_lock = true
allow_local_images = false
allowed_repositories = [
"docker.io/library",
"ghcr.io/acme/developer-tools",
"registry.example.com:5443/platform",
]
expires_at = "2027-01-01T00:00:00Z"
```

Unknown or duplicate keys, sections, malformed values, unsupported versions
and expired policies are errors. `expires_at` is optional and, when present,
must be an RFC 3339 timestamp. At least one actual constraint
(`require_lock` or a non-empty repository allowlist) is required.

`require_lock = true` rejects every unlocked or stale request, including shim
execution, expose discovery, self-test tool execution and diagnostics that
would inspect an image. It does not prevent `cb lock` or `cb update` from
creating the required entry.

Local image-ID locks have no registry origin and are rejected by default under
a managed policy. `allow_local_images = true` is the explicit exception. It
does not make an unlocked local tag acceptable when `require_lock = true`.
Use `cb add TOOL --image IMAGE --local` when creating a profile for such an
image, then follow the reported explicit `cb lock --local TOOL` or
`cb update --local TOOL` command. ContainerBin never guesses local intent from
the daemon's current image metadata.

Repository rules are canonical namespace boundaries:

- `python:3.13` and `docker.io/python:3.13` normalize to
`docker.io/library/python`;
- `index.docker.io` and `registry-1.docker.io` normalize to `docker.io`;
- tags and digests do not affect origin authorization;
- a host-only rule allows that registry; a longer rule allows that repository
and descendants;
- a single-segment rule such as `python` means the Docker Hub namespace
`docker.io/python`; it does not match the official image repository
`docker.io/library/python`;
- an unqualified multi-segment rule such as `astral-sh/uv` means
`docker.io/astral-sh/uv`;
- `ghcr.io/acme` does **not** allow `ghcr.io/acme-tools`.

Authorization occurs before a repository-mode `docker pull`, local-image
inspection, expose discovery or `docker run`. A managed-policy restore is
preflighted against the complete archived registry and lock before any state or
configuration is changed. Switching a runtime default likewise requires every
target profile to be authorized first.

The compiled-in state backup/restore helper is not a user-selected registry
profile and is outside `allowed_repositories`. It is an exact Alpine digest,
is never pulled implicitly, and runs with networking disabled and a read-only
container root. Docker daemon policy may still reject it, and state operations
fail if that exact helper image is not already available.

## Diagnostics and error contract

`cb doctor`, `cb inspect TOOL`, `cb trace TOOL ...` and `cb bugreport` report
whether operation is managed, the schema, effective switches, rule count,
expiry, fixed source path and SHA-256 fingerprint. They do not print the policy
file contents. `cb inspect` and `cb trace` also show the selected tool's
authorization result.

Policy failures have a stable bracketed code suitable for log processing:

- `policy.unreadable`
- `policy.ownership`
- `policy.syntax`
- `policy.version`
- `policy.expired`
- `policy.lock_required`
- `policy.local_image_denied`
- `policy.repository_denied`

The fingerprint hashes the exact policy bytes. It is an audit correlation
value, not a signature. Registry-signature and image-signature policy are
separate roadmap stages and are not implied by schema 1.
5 changes: 1 addition & 4 deletions docs/proxy-airgap.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ For an explicit ContainerBin profile, prefer an allowlist such as:
image = "registry.corp.example/devtools/terraform:1.13.3"
provider = "stateless"
path_equals = ["-chdir"]
env_names = [
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
"http_proxy", "https_proxy", "no_proxy",
]
env_names = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]
```

The profile passes only variables already present in the host environment. Do
Expand Down
11 changes: 11 additions & 0 deletions docs/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Inside the boundary (whoever controls these controls execution):
need ContainerBin to attack you.
- Docker Desktop itself, and every image you configure or `docker pull`.

An optional administrator-owned machine policy sits above this user-controlled
boundary. Its fixed path, owner and permissions are validated before use. It
can require locking and restrict image origins, but schema 1 does not constrain
mounts, environment allowlists or commands and does not authenticate registry
or image signatures. See [enterprise machine policy](enterprise-policy.md).

Treat the registry and lockfile like your PowerShell `$PROFILE`: yours,
readable, and dangerous to let others edit.

Expand All @@ -43,6 +49,11 @@ readable, and dangerous to let others edit.
- **Fail-closed configuration.** Unknown registry keys, duplicate tool
sections, newer schema versions, incomplete lock entries, and
registry-image-not-in-lock all refuse to run rather than guess.
- **Machine policy cannot be redirected or weakened.** A present enterprise
policy is loaded only from the fixed OS path, requires administrator/root
ownership and restrictive permissions, and authorizes the already-resolved
image request before pulls, image inspection or execution. Missing means
unmanaged; unreadable, malformed, expired or unsupported means stop.
- **Reserved shim names.** Tool names that would collide with `cb` itself or
Windows device names (`con`, `nul`, `com1`, …) are rejected at validation,
as are versioned management-binary names beginning with `cb-v` plus a digit.
Expand Down
Loading
Loading