Manage Container Secrets with Podman

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Any Linux host with Podman installed
Privilege Rootful examples on the lab host; rootless secret paths follow the same commands under the user account
Scope Runtime secrets — podman secret create, ls, inspect, rm, and exists; podman run --secret with type=mount vs type=env, mount targets, and uid/gid/mode; tested --replace behavior; file, pass, and shell drivers; podman build --secret; Quadlet Secret= mount and env forms with rotation via systemctl restart. Does not cover registry login credentials, external vault products, or Kubernetes Secrets.
Related guides Podman login to a registry
Run containers with Podman

Passwords, API tokens, and TLS keys do not belong in image layers or plain docker commit output. A Podman secret stores a small sensitive blob outside the image — passwords, tokens, keys, and similar values up to roughly 512 KB on current Podman releases. Secrets are managed separately from images, are not included in podman commit or podman export, and can be delivered as files or environment variables at runtime or during image build.

This is not a full enterprise secret-management platform. It is Podman's built-in way to keep sensitive values out of images while still feeding them to containers you control.


What is a Podman secret?

Think of a secret as a named blob Podman stores on the host. Typical contents include:

  • Database or application passwords
  • API tokens and OAuth client secrets
  • TLS private keys or client certificates
  • SSH private keys

podman secret inspect returns metadata — ID, name, driver, labels, timestamps — and by default does not include the secret value. That property matters when you are proving to yourself (or an auditor) that routine listing and inspection commands do not leak secret material.

Registry login credentials use a different mechanism (podman login and auth files). Runtime secrets are the topic here.


Create a secret from a file

For lab demos, write sample content to a file and create the secret:

bash
printf '%s' 'example-value' > secret.txt

Register it with Podman:

bash
podman secret create app-secret secret.txt

Sample output (secret ID):

output
d75332287c9e9dc7f5fb05192

Remove the plaintext file when you no longer need it — rm -f secret.txt. On production hosts, creating a temporary file may itself be undesirable; prefer stdin creation in the next section when your shell or orchestrator can pipe the value once.


Create a secret from stdin

Piping avoids leaving a persistent plaintext file on disk:

bash
printf '%s' 'stdin-value' | podman secret create --replace app-secret -

The - argument tells Podman to read secret content from standard input. Use a placeholder in documentation and real secret managers in production — do not paste live credentials into shell history.


Create a secret from an environment variable

When the value already exists in your shell session:

bash
export APP_SECRET='env-value'

Create the secret from that variable name:

bash
podman secret create --replace --env app-secret APP_SECRET

Podman reads APP_SECRET at creation time. It does not mean every future container automatically inherits that host environment variable — delivery still happens through --secret on podman run or Quadlet Secret= lines.


List Podman secrets

List stored secrets:

bash
podman secret ls

Sample output:

output
ID                         NAME        DRIVER      CREATED                 UPDATED
d5c69eefefb42d48c83a3b3c1  app-secret  file        Less than a second ago  Less than a second ago

Columns map to:

  • ID — unique secret identifier
  • NAME — friendly name you pass to --secret
  • DRIVER — backend (file, pass, shell, …)
  • CREATED / UPDATED — timestamps

The secret value never appears in this table.


Inspect a secret

Pull metadata for one secret:

bash
podman secret inspect app-secret

Sample output (trimmed):

output
[
    {
        "ID": "d5c69eefefb42d48c83a3b3c1",
        "Spec": {
            "Name": "app-secret",
            "Driver": {
                "Name": "file",
                "Options": {
                    "path": "/var/lib/containers/storage/secrets/filedriver"
                }
            }
        }
    }
]

Useful fields include ID, name, driver, labels, and driver options. By default, podman secret inspect does not include plaintext secret data. Podman provides --showsecret for explicit retrieval, so do not use that option in routine diagnostics or logged automation.


Mount a secret into a container

The default delivery mode is a file mount:

bash
podman run --rm --secret app-secret registry.access.redhat.com/ubi9/ubi-minimal:latest cat /run/secrets/app-secret

Sample output:

output
env-value

Default behavior:

text
type=mount
target=/run/secrets/SECRET_NAME

The secret appears as a file inside the container filesystem.


Change the secret mount target

Set an explicit path with target=:

bash
podman run --rm --secret source=app-secret,target=/run/config/token registry.access.redhat.com/ubi9/ubi-minimal:latest cat /run/config/token

Sample output:

output
env-value

A fully qualified path such as /run/config/token is used as-is. A relative target such as token is placed under /run/secrets/ on Linux.


Set secret UID, GID, and mode

Mount ownership and permissions apply only to type=mount:

bash
podman run --rm --secret source=app-secret,uid=1001,gid=1001,mode=0440 registry.access.redhat.com/ubi9/ubi-minimal:latest ls -l /run/secrets/app-secret

Sample output:

output
-r--r-----. 1 1001 1001 9 Aug 22 23:54 /run/secrets/app-secret

These IDs control how the secret file appears inside the container. They are not the same as ownership of Podman's backend storage on the host.


Expose a secret as an environment variable

Some applications expect configuration through environment variables:

bash
podman run --rm --secret source=app-secret,type=env,target=APP_SECRET registry.access.redhat.com/ubi9/ubi-minimal:latest printenv APP_SECRET

Sample output:

output
env-value

Environment delivery is convenient but the value can surface through process listings, diagnostics, crash dumps, and accidental logging. Mounted files are often easier to keep out of routine ps and /proc/PID/environ output. Pick the form your application already supports — this guide does not prescribe a universal security winner.


Compare type=mount and type=env

Mount (type=mount) Environment (type=env)
Delivery File under /run/secrets/ (or custom target) Named environment variable
Ownership Supports uid, gid, mode No mount permission knobs
App pattern Read a file path Read getenv / os.environ
Rotation signal Replace secret and recreate container to change file content Replace secret and recreate container to change PID 1 environ

The rotation row matters — tested behavior is in the next section.


Replace or rotate a secret

Create a secret with initial content:

bash
printf '%s' 'old-mount' | podman secret create --replace rotate-secret -

--replace updates an existing secret with the same name instead of failing on duplicate names.


Does --replace update a running container?

Podman documentation is inconsistent here: podman secret create --replace documents that existing containers are not updated, while the runtime --secret documentation says modifications affect secrets inside a container. On the tested Podman 5.8.2 host, the behavior matched the secret create documentation for the original workload process: recreate the container for deterministic rotation. Mount and environment delivery behave differently — and environment secrets have a subtle exec trap.

Test 1 — mounted secret file

Start a long-lived container with a mounted secret:

bash
podman run -d --name rotate-mount --secret rotate-secret registry.access.redhat.com/ubi9/ubi-minimal:latest sleep 600

Read the file before rotation:

bash
podman exec rotate-mount cat /run/secrets/rotate-secret

Sample output:

output
old-mount

Replace the secret:

bash
printf '%s' 'new-mount' | podman secret create --replace rotate-secret -

Read the same file in the still-running container:

bash
podman exec rotate-mount cat /run/secrets/rotate-secret

Sample output on Podman 5.8.2:

output
old-mount

The mount inside the running container keeps the original content. A new container sees the updated value:

bash
podman run --rm --secret rotate-secret registry.access.redhat.com/ubi9/ubi-minimal:latest cat /run/secrets/rotate-secret

Sample output:

output
new-mount

Test 2 — environment secret

Create and run with type=env:

bash
printf '%s' 'old-env' | podman secret create --replace rotate-env -

Start a long-lived container that reads the secret as an environment variable:

bash
podman run -d --name rotate-env --secret source=rotate-env,type=env,target=ROTATE_ENV registry.access.redhat.com/ubi9/ubi-minimal:latest sleep 600

Check PID 1's environment before replace:

bash
podman exec rotate-env sh -c 'tr "\0" "\n" < /proc/1/environ | grep ROTATE_ENV'

Sample output:

output
ROTATE_ENV=old-env

Replace the secret:

bash
printf '%s' 'new-env' | podman secret create --replace rotate-env -

PID 1 still holds the original value:

bash
podman exec rotate-env sh -c 'tr "\0" "\n" < /proc/1/environ | grep ROTATE_ENV'

Sample output:

output
ROTATE_ENV=old-env

A podman exec printenv in the same running container can mislead you — on Podman 5.8.2 it showed new-env here because exec resolves the current secret again:

bash
podman exec rotate-env printenv ROTATE_ENV

Sample output:

output
new-env

Do not treat podman exec printenv as proof the main workload process received a rotated value. Recreate the container when guaranteed rotation is required.


Remove a Podman secret

Delete a secret by name:

bash
podman secret create temp-secret - <<< 'temp'

Remove it when the demo is done:

bash
podman secret rm temp-secret

Sample output:

output
454f9a942e6b4db23ece144aa

Script existence checks after removal:

bash
podman secret exists temp-secret

The exit status is what scripts should test:

bash
echo exit:$?

Sample output after removal:

output
exit:1

On Podman 5.8.2, podman secret rm rotate-secret succeeded even while rotate-mount still had the old file mounted — removing the secret object does not unmount or rewrite files inside running containers. Plan rotation around container recreation, not secret deletion alone.


Secret drivers

Podman selects a backend through containers.conf and per-command --driver. The default is file.

Driver Backend
file Podman local protected storage (default)
pass pass GPG-encrypted store
shell Administrator-provided list/lookup/store/delete scripts

Driver configuration defaults live in Podman containers.conf.


file secret driver

Explicit file driver (same as default):

bash
printf '%s' 'file-driver' | podman secret create --driver=file --replace file-secret -

Podman stores the blob in its local secret storage. Do not edit files under the driver path by hand — use podman secret commands.


pass secret driver

The pass driver delegates storage to a GPG-encrypted password store. Prerequisites include the pass utility and a configured GPG key. The pass package was not installed on this RHEL 10.2 lab image, so the command below is the shape to use once pass is available:

bash
podman secret create --driver=pass app-secret secret-input.txt

This is not a GPG or pass initialization guide — install pass, initialize the store, then point Podman at the driver.


shell secret driver

The shell driver calls your scripts for list, lookup, store, and delete operations. Configure it through containers.conf — for example with CONTAINERS_CONF_OVERRIDE during testing:

toml
[secrets]
driver = "shell"

[secrets.opts]
list = "/root/podman-secrets-lab/shell-driver/list"
lookup = "/root/podman-secrets-lab/shell-driver/lookup"
store = "/root/podman-secrets-lab/shell-driver/store"
delete = "/root/podman-secrets-lab/shell-driver/delete"

The store helper receives the secret ID in the SECRET_ID environment variable and reads secret bytes from stdin:

bash
#!/bin/bash
set -euo pipefail
id="${SECRET_ID:?missing SECRET_ID}"
cat > "/var/lib/podman-shell-secrets/${id}"
chmod 600 "/var/lib/podman-shell-secrets/${id}"

With the override file exported, create and mount a shell-backed secret:

bash
export CONTAINERS_CONF_OVERRIDE=/tmp/shell-secrets.conf
printf '%s' 'shell-backend-value' | podman secret create shell-secret -

Mount it in a container:

bash
podman run --rm --secret shell-secret registry.access.redhat.com/ubi9/ubi-minimal:latest cat /run/secrets/shell-secret

Sample output:

output
shell-backend-value

Shell driver security depends entirely on your scripts and backing store — customizable does not mean automatically safe.


Use secrets during podman build

Build secrets are a separate workflow from runtime podman run --secret. They make a file available only to a specific RUN step.

Create a build context directory and host token file:

bash
mkdir -p buildctx
printf '%s' 'build-token-value' > token.txt

Containerfile (buildctx/Containerfile):

dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
RUN --mount=type=secret,id=api-token \
    test -s /run/secrets/api-token && echo "built-with-secret" > /built-marker

Build with the secret mount:

bash
podman build --secret id=api-token,src=token.txt -t secret-build-demo buildctx/

Verify the marker without persisting the token in a layer:

bash
podman run --rm secret-build-demo cat /built-marker

Sample output:

output
built-with-secret

Do not COPY the secret file or redirect secret content to a path that survives in the final image — that defeats the purpose. Deeper build patterns live in Build images with Podman.


Build secret and layer cache gotcha

Changing only the secret file does not necessarily invalidate a cached RUN layer. Swap the token file and rebuild without other changes:

bash
printf '%s' 'token-v2' > token.txt
podman build --secret id=api-token,src=token.txt -t secret-build-demo buildctx/ 2>&1 | tail -3

On this host Podman reused the same cached layer ID:

output
--> Using cache 9d7165919dbf5322abfa85fcfd4ee3134ed4eada4be7943879f50dc87f8aba30
Successfully tagged localhost/secret-build-demo:latest

If build output genuinely depends on secret content, add a cache-busting build argument, change a line above the secret-consuming RUN, or use --no-cache when you must force a rebuild.


Use secrets with Quadlet

Quadlet .container units accept a Secret= directive in the [Container] section. Create the secret first:

bash
printf '%s' 'quadlet-secret-value' | podman secret create --replace quadlet-secret -

Mount form:

ini
[Unit]
Description=Podman secret demo container

[Container]
Image=registry.access.redhat.com/ubi9/ubi-minimal:latest
ContainerName=secret-demo
Secret=quadlet-secret,type=mount,target=/run/secrets/app
Exec=sleep infinity

[Service]
Restart=always

Save as /etc/containers/systemd/secret-demo.container, reload, and start:

bash
systemctl daemon-reload
systemctl start secret-demo.service

Read the mounted file:

bash
podman exec secret-demo cat /run/secrets/app

Sample output:

output
quadlet-secret-value

Environment form (valid on Podman 5.8.2):

ini
Secret=quadlet-secret,type=env,target=QUADLET_SECRET

After switching the unit to the env form and restarting:

bash
systemctl restart secret-demo.service
podman exec secret-demo printenv QUADLET_SECRET

Sample output:

output
quadlet-secret-value

See Podman Quadlet container file for general [Container] directive syntax.


Rotate a secret used by Quadlet

A deterministic rotation workflow avoids relying on live-container secret propagation:

  1. Replace the secret — printf '%s' 'quadlet-rotated' | podman secret create --replace quadlet-secret -
  2. Restart the generated service — systemctl restart secret-demo.service
  3. Confirm a new container ID — podman ps --filter name=secret-demo
  4. Verify consumption — podman exec secret-demo printenv QUADLET_SECRET (env form) or cat the mount path

On this host, restart changed the container ID from 61ab6cd7e7c9 to a2618240edfc and the workload saw quadlet-rotated. That is the pattern to document in runbooks — replace, restart, verify — rather than assuming an already-running container picks up the new blob.


Podman secrets vs environment files

Podman secret Environment file (--env-file)
Storage Managed secret object Plain host file
Inspection Metadata only by default; --showsecret can reveal value File contains readable key=value lines
Delivery Mount or env via --secret Env vars from file
Drivers file, pass, shell No secret backend
Build integration podman build --secret Not a build-secret mechanism

Environment files remain appropriate for non-sensitive configuration. Secrets fit values that should not live in images or casual file reads.


Troubleshooting

Symptom Likely cause Fix
secret name already exists Duplicate name without --replace Add --replace or choose a new name
Mount path empty in container Wrong secret name or missing --secret Match source= to podman secret ls name
unknown flag: --module style error for secrets Flag after subcommand Keep --secret on podman run (not a global flag issue for secrets)
Rotated secret but app still shows old value Running container not recreated Restart workload or systemctl restart Quadlet service
podman exec printenv shows new value but app does not Exec re-resolves secrets; PID 1 does not Check /proc/1/environ or recreate container
Build step ignores new secret content Layer cache reused Bust cache or rebuild with --no-cache
Shell driver no such secret Script protocol mismatch Shell driver passes SECRET_ID in the environment — not $1

References


Summary

Podman secrets keep sensitive blobs out of images while still feeding them to containers. Create secrets from files, stdin, or host environment variables; list and inspect metadata by default without revealing values; deliver values as mounted files (with optional uid, gid, and mode) or environment variables.

The rotation story is where documentation often disagrees with intuition. podman secret create --replace says existing containers are not updated, while runtime --secret docs suggest otherwise. On Podman 5.8.2, --replace did not change a mounted secret file inside a running container, and environment secrets kept the original value in PID 1's environ even though podman exec printenv could show the updated secret. New containers and restarted Quadlet services picked up rotated values — plan on recreate or systemctl restart when rotation must be guaranteed.

Build secrets mount into individual RUN steps; cache can hide secret changes unless you bust the layer. The file driver covers most hosts; pass and shell backends integrate external stores when you need them. Registry authentication stays in login and registries.conf territory — runtime secrets solve a different problem.


Frequently Asked Questions

1. Where does Podman store secrets by default?

The default file driver stores secret blobs under Podman local secret storage, typically beneath /var/lib/containers/storage/secrets/. By default, podman secret inspect shows metadata and does not reveal the secret value. The --showsecret option can explicitly display the secret, so avoid it in logs and routine inspection.

2. What is the difference between type=mount and type=env for podman run --secret?

type=mount exposes the secret as a file, defaulting to /run/secrets/SECRET_NAME on Linux, and supports uid, gid, and mode. type=env injects the value into a named environment variable, which is convenient for apps that expect env vars but is visible to process environment inspection.

3. Does podman secret create --replace update a running container?

On Podman 5.8.2 tested here, a mounted secret file inside a running container keeps the old content after --replace. Environment secrets also keep the original value in PID 1 environ, while podman exec may show the new value because exec resolves secrets again. Recreate the container or restart the Quadlet service when you need guaranteed rotation.

4. Can I use Podman secrets during podman build?

Yes. Pass --secret id=NAME,src=FILE and mount it in a RUN line with --mount=type=secret,id=NAME. The secret is available only to that build step and should not be copied into image layers with COPY or redirected to a persistent path in RUN.

5. How do I pass a secret through a Quadlet container unit?

Add Secret=secret-name to the [Container] section. Use Secret=name,type=mount,target=/path for a file or Secret=name,type=env,target=VAR_NAME for an environment variable. After rotating the secret with podman secret create --replace, restart the generated systemd service so a new container picks up the value.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)