containers / containers/podman-compose
File secrets use bind mounts (breaks under enforcing SELinux); native podman secrets fix it but change update semantics — guidance on preferred approach?
- Dominant language
- Python
- Stars
- 6.2k
- Forks
- 622
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
File-based secrets (`secrets..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
```python
# 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`:
```yaml
services:
demo:
image: busybox
command: ["cat", "/run/secrets/pw"]
secrets:
- pw
secrets:
pw:
file: ./pw
```
```console
$ 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):
```console
$ 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 _
--secret _,type=mount,target=[,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:
```console
$ 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: #655 stays broken by default on enforcing SELinux.
2. **Native default, bind opt-out.** Make native secrets the default so #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.
Contributor guide
Research direction
Start with get_secret_args() in podman_compose.py around lines 911-928, then compare the file-secret path with create_secrets_from_environment() around line 802 and its compose_up call. Review the reported unit and integration coverage and the tradeoff matrix; done requires maintainer agreement on the default semantics before implementation proceeds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, linux, python
- Domain
- devops, infrastructure, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100