Skip to content

File secrets use bind mounts (breaks under enforcing SELinux); native podman secrets fix it but change update semantics — guidance on preferred approach? #1551

Description

@brianredbeard

Summary

File-based secrets (secrets.<name>.file:) are implemented as plain read-only bind mounts rather than as native podman secrets. On enforcing-SELinux systems this makes the mounted secret unreadable by the container (the long-standing failure in #655), and it also means uid/gid/mode are silently ignored.

The obvious fix — registering file secrets as native podman secrets (the same way environment: secrets are already handled) — resolves both problems cleanly, but changes update semantics (native secrets are copied at container creation, not live-mounted). That is a real behavior change for live-testing / secret-rotation workflows.

Because there is no single mechanism that wins on every axis, I'd like maintainer guidance on the preferred route (in particular, the default) before opening a PR. I have a working implementation and am happy to shape it either way.

This is a focused re-scoping of the still-open portion of #655.

Environment

podman-compose: main (1.6.0, commit e3df104)
podman:         5.8.4
python:         3.14.7
OS:             Fedora Linux 44 (Workstation Edition)
SELinux:        Enforcing

Current behavior

For a file: secret, get_secret_args() does not create a podman secret. It rewrites the secret into a bind mount:

https://github.com/containers/podman-compose/blob/e3df104/podman_compose.py#L919

# get_secret_args(), run path (podman_compose.py:919 on main)
volume_ref = ["--volume", f"{source_file}:{dest_file}:{mount_options}"]

A bind mount preserves the host file's SELinux label. A confined container runs as container_t and is denied read on a file labeled e.g. var_t / unconfined_u:object_r:.... This is inherent to bind-mounting an unlabeled host file into a confined container — not a user misconfiguration.

The same design choice is why uid/gid/mode cannot be honored; the code only emits a warning:

https://github.com/containers/podman-compose/blob/e3df104/podman_compose.py#L921-L928

Reproduction

compose.yaml:

services:
  demo:
    image: busybox
    command: ["cat", "/run/secrets/pw"]
    secrets:
      - pw
secrets:
  pw:
    file: ./pw
$ echo hunter2 > pw
$ getenforce
Enforcing
$ podman-compose up
...
cat: can't open '/run/secrets/pw': Permission denied

Minimal confirmation that the bind mount itself is what SELinux blocks (no podman-compose involved):

$ podman run --rm -v $PWD/pw:/run/secrets/pw:ro busybox cat /run/secrets/pw
cat: can't open '/run/secrets/pw': Permission denied

The matching AVC (as reported by multiple users in #655):

avc: denied { read } for comm="cat" name="pw"
  scontext=system_u:system_r:container_t:s0:c...
  tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

Note: the environment: half of #655 is already fixed

The unparsable secret / not supported for runtime secrets tracebacks in #655 came from environment:-sourced secrets, and those are resolved on main: create_secrets_from_environment() (podman_compose.py:802, invoked from compose_up at :4179) runs podman secret create --env and references the secret with --secret. Users still hitting that are on an older release (e.g. Fedora's 1.5.0). Only the file: path remains broken.

Why the existing x-podman.relabel: z/Z is a band-aid

The x-podman.relabel option (podman_compose.py:911) fixes access by appending :z/:Z to the bind mount, which makes podman chcon the original host file:

  • It mutates the SELinux label of the source file (e.g. /srv/app/pw) in place — a side effect on a file podman doesn't own.
  • :Z (private) then locks any other service/container out of a shared secret file.
  • It's a podman-compose-only extension, so the compose file is no longer portable to docker.
  • It still doesn't address uid/gid/mode.

Candidate fix: native podman secrets

Treat file: secrets like environment: secrets already are — create a real podman secret and reference it:

podman secret create --replace --driver file <project>_<name> <src>
--secret <project>_<name>,type=mount,target=<dest>[,uid=..][,gid=..][,mode=..]

Podman then copies the secret into the container with the container's own SELinux label (no host relabeling), honors uid/gid/mode, and supports absolute target= paths. This fixes SELinux and the ownership fields out of the box.

The catch: copy vs. live semantics

Native mount secrets are copied at container creation, not live-mounted. I verified that rotating a secret does not reach a running container — and podman restart is not enough; only a full recreate picks it up:

$ echo v1 > s; podman secret create stest s
$ podman run -d --name c --secret stest,type=mount busybox sleep 300
$ podman exec c cat /run/secrets/stest
v1

# rotate the secret on the host
$ echo v2 > s; podman secret rm stest && podman secret create stest s

$ podman exec c cat /run/secrets/stest      # running container
v1                                          # <-- NOT updated
$ podman restart c && podman exec c cat /run/secrets/stest
v1                                          # <-- restart is NOT enough
# only `podman-compose up --force-recreate` (rm+create) picks up v2

Today's bind-mount behavior — and docker-compose's — propagates in-place edits live, which matters for dev/live-testing loops and for secret rotation. A straight swap to native secrets fixes SELinux but regresses that.

I also confirmed there is no third mechanism that gives live updates and correct SELinux without touching the host file: a per-mount context= SELinux option is not accepted for single-file bind mounts in podman's -v syntax.

Tradeoff matrix

Approach SELinux (enforcing) uid/gid/mode Live in-place edits Host file label Docker parity
Bind mount (current default) ❌ blocked (#655) ❌ ignored ✅ propagates untouched
Bind mount + :z/:Z (x-podman.relabel) ⚠️ mutated in place ⚠️ non-standard
Native secret (candidate fix) ❌ needs full recreate untouched ❌ (docker bind-mounts)

Question for maintainers — preferred route?

I'd like your call on the design before I open a PR. Options as I see them:

  1. Bind-mount default, native opt-in. Keep current bind-mount behavior as the default (no regression, preserves live edits + docker parity); add a per-secret opt-in (e.g. an x-podman.* field) to use native secrets for SELinux/uid/gid/mode correctness. Downside: Secrets aren't getting created properly when specified in the compose file #655 stays broken by default on enforcing SELinux.
  2. Native default, bind opt-out. Make native secrets the default so Secrets aren't getting created properly when specified in the compose file #655 is fixed out of the box on enforcing SELinux (podman's primary audience); add a per-secret opt-out to bind-mount for live-update workflows. Downside: regresses live updates by default and diverges from docker.
  3. Native only. Always use native secrets. Simplest, always correct on SELinux/ownership; no way to get live-update semantics.
  4. Something else — e.g. auto-detect enforcing SELinux, align with the compose-spec driver/driver_opts fields, or a global config toggle rather than per-secret.

Open questions that may steer the choice:

  • How much weight should strict docker-compose parity carry here vs. correct-by-default on podman's primary (enforcing-SELinux) platform?
  • Is a per-secret extension acceptable, or is a single global/project-level toggle preferred?
  • Should the created secrets be cleaned up on down (labeled by project), matching or diverging from how environment: secrets currently persist?

Happy to implement whichever direction you prefer — I already have the native-secret path working with unit + integration coverage.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions