Skip to content

feat: preserve image-declared identity in Docker and Podman #2331

Description

@matthewgrossman

Problem Statement

Docker and Podman start the OpenShell supervisor as root and replace the image
entrypoint. Today, agent identity then falls back to the policy's named
sandbox:sandbox account, which requires every image to bake that exact user
and group.

The first implementation attempt in #2335 replaced that requirement with a
gateway-wide numeric identity, defaulting every local image to 10001:10001.
That is also the wrong local-driver abstraction: it discards the image's normal
OCI USER, can break image file ownership, and unnecessarily copies the
platform-assigned identity model used by Kubernetes/OpenShift.

Docker and Podman should preserve a sandbox image's declared non-root OCI
USER for agent children while continuing to run only the privileged
supervisor as 0:0.

This remains the Docker/Podman portion of #1959. PR #2335 is closed and
superseded by this design.

Design Decisions

  • Reject newly created images with no OCI USER; OCI defaults those images to
    root.
  • Reject USER root, USER 0, or any name resolving to UID or GID 0.
  • Use image identity by default for Docker and Podman.
  • Keep an explicit operator-controlled fixed identity mode for shared-storage
    deployments.
  • Block creator-selected bind and named-volume mounts in image mode.
  • Do not change Kubernetes/OpenShift or VM identity behavior.

User Contract

Image declaration Agent behavior
USER app Resolve app from the image passwd file and use its UID and primary GID.
USER app:staff Resolve both names from the image account files.
USER 1234:1235 Use the numeric pair without requiring passwd or group entries.
USER 1234 with a passwd entry Use UID 1234 and that entry's primary GID.
USER 1234 without a passwd entry Reject and require an explicit group.
No USER Reject and require a non-root declaration.
An identity resolving to UID/GID 0 Reject.

A passwd-less image therefore needs only:

USER 1234:1235

It does not need useradd, groupadd, or a user named sandbox.

Identity Modes

Local drivers expose an operator-selected identity source:

  • image is the default. The agent uses OCI Config.User and may use only the
    driver-owned per-sandbox workspace plus immutable image mounts.
  • fixed requires an explicit validated UID and GID. It supports deployments
    that intentionally attach external or shared storage under an
    operator-controlled principal.

The unshipped implicit 10001:10001 default and standalone Podman UID/GID flags
from #2335 must not return.

Image Inspection

Resolve or pull the final image before container creation, then inspect it and
carry both its immutable image ID and OCI Config.User through provisioning.
Create the container from that immutable ID so a mutable tag cannot change
between identity inspection and rootfs selection.

  • Docker already calls Bollard inspect_image() and currently discards
    ImageInspect; read inspect.config.user and retain the image ID.
  • Podman needs a typed wrapper for
    GET /v5.0.0/libpod/images/{name}/json, reading the image ID and
    Config.User.
  • Podman create must not perform another pull decision after an immutable local
    image ID has been selected.
  • Docker keeps container user = "0"; Podman keeps user = "0:0". Those
    values apply only to the supervisor.

Pass the raw OCI user declaration, immutable image ID, and identity source to
the supervisor through protected driver-owned metadata inserted after image and
request environment.

Supervisor Resolution

This extends an existing mechanism. The supervisor already runs inside the
sandbox image rootfs and resolves the legacy sandbox:sandbox identity before
filesystem preparation and the verified setgid/setuid sequence. Direct and
SSH children already share that privilege-drop boundary.

Do not reuse the current libc NSS path unchanged for arbitrary image users.
Resolve names with a bounded file-only parser:

  • Open /etc/passwd and /etc/group without following symlinks.
  • Require regular files and cap total, line, and field sizes.
  • Reject malformed or duplicate matching names and IDs.
  • Do not call getpwnam, getgrnam, NSS modules, or initgroups from the
    privileged supervisor.
  • Resolve every accepted form to one complete numeric UID/GID.
  • Use the passwd primary GID when the OCI declaration omits a group.
  • Require an explicit group for an accountless numeric UID.
  • Reject resolved UID/GID 0.

The resulting internal record contains at least:

ResolvedAgentIdentity {
    uid,
    gid,
    presentation_user,
    source,
    image_id,
}

Agent children receive no supplementary groups. Clear and verify the group list
before setgid/setuid for both named and numeric image declarations. Preserve
HOME=/sandbox; use the declared name for USER/LOGNAME when available and
the numeric UID otherwise.

OpenShell must not create or rewrite /etc/passwd or /etc/group.

Storage Boundary

The image is untrusted and may select any valid non-root UID/GID. That identity
may authorize access to its rootfs and isolated workspace, but it must not
select the principal used for unrelated storage.

In image mode, reject all creator-selected bind mounts and named-volume mounts.
The driver-owned per-sandbox /sandbox volume and immutable image mounts remain
allowed. A global bind-mount enable switch is not sufficient authorization for
image-selected IDs.

Operators that require external or shared storage must select fixed mode until
OpenShell has operator-authored mount grants binding storage sources to allowed
identities.

The supervisor may chown /sandbox itself and missing paths OpenShell creates.
It must not recursively chown existing descendants or nested mounts. Privileged
directory creation must use no-symlink ancestor traversal rather than checking
only the final path before create_dir_all.

Policy And Observability

Creator policy must not claim authority over an identity the managed runtime
ignores.

  • Reject explicitly supplied run_as_user and run_as_group for new managed
    Docker/Podman sandboxes before default normalization.
  • Retain those fields only for existing containers and unmanaged/offline
    execution.
  • Persist and expose the resolved identity separately: source, immutable image
    ID, UID, GID, and supplementary groups.
  • Emit the same record as an OCSF configuration-state event, but do not use logs
    as the authoritative API.

Resolve and persist an identity-versioned record before launching the first
agent. Restarts consume that record instead of re-resolving a mutable tag or
account file. Existing containers without the new metadata continue using their
legacy launch-effective policy and are not reinterpreted during upgrade.

User Namespaces

Validate resolved IDs against /proc/self/uid_map and
/proc/self/gid_map before chown or privilege drop. This covers rootless
Podman, rootless Docker, Docker userns-remap, and nested or remote setups.
Report the image declaration and unmapped ID without silently substituting a
different identity.

Respect /proc/self/setgroups: a denied clear is acceptable only if the current
supplementary-group list is already empty.

Compatibility And Documentation

  • First-party images already declaring USER sandbox retain their baked
    identity.
  • First-party and documented images with no final non-root USER must add one.
  • The BYOC and private-IP-routing examples need explicit users.
  • The userless fixture should declare a distinct numeric UID/GID without
    account entries.
  • Existing sandboxes retain their creation-time identity. Changing identity
    mode or fixed IDs applies only to newly created sandboxes; persisted
    workspaces require recreation or an explicit migration.
  • Update architecture docs, driver READMEs, BYOC documentation, gateway config
    reference, and examples. Clearly distinguish local image identity from
    Kubernetes/OpenShift platform-assigned identity.

Test Plan

Unit

  • Parse named, numeric, named-group, numeric-group, and mixed OCI forms.
  • Resolve named users to passwd UID and primary GID.
  • Reject missing declarations, accountless UID-only declarations, root aliases,
    malformed input, duplicate entries, symlinks, non-regular files, and
    oversized account databases.
  • Verify all image forms clear supplementary groups.
  • Verify /sandbox root ownership changes without touching descendants.
  • Verify passwd/group files remain unchanged.
  • Verify old containers without identity metadata retain legacy behavior.

Driver Integration

  • Docker inspects after local hits and every pull policy, carries the immutable
    image ID, and preserves supervisor user = 0.
  • Podman image inspection handles compatible response shapes and escaping,
    carries the immutable image ID, disables a second pull decision, and
    preserves supervisor user = 0:0.
  • Image mode rejects creator-selected external mounts; fixed mode uses explicit
    validated UID/GID.
  • Image and request environment cannot spoof protected identity metadata.

E2E

  • Passwd-less USER 1234:1235 runs as 1234:1235, has no supplementary
    groups or account entries, and writes to /sandbox.
  • Named USER app uses the image account's UID and primary GID.
  • Explicit named and mixed group forms resolve correctly.
  • Missing USER, explicit root, and root aliases fail clearly.
  • Rootless Podman, rootless Docker, and Docker userns-remap report unmapped IDs.
  • Direct and SSH-launched children use the same identity and environment.
  • Existing sandbox resume retains its persisted identity.

Alternatives Considered

  • Fixed 10001:10001 by default: rejected because it breaks normal local
    image ownership and incorrectly copies the OpenShift model.
  • Continue requiring sandbox:sandbox: rejected because it prevents
    passwd-less numeric images and arbitrary valid image users.
  • Allow image identity with creator-selected external mounts: rejected
    because an untrusted image could choose the principal owning unrelated
    storage.
  • Run the supervisor as OCI USER: rejected because namespace, mount,
    network, filesystem, and privilege-drop setup require root.
  • Resolve names through NSS: rejected because privileged resolution must not
    invoke image-controlled NSS configuration or modules.

Scope

This issue covers Docker, Podman, shared supervisor identity resolution,
creation-time identity persistence and observability, affected policy handling,
tests, examples, and documentation.

Kubernetes/OpenShift and VM identity selection are out of scope except for
regression tests protecting their existing behavior.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions