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?

Open
#1,551 0 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.