Verify Container Image Signatures with Podman

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Linux hosts with Podman 5.6 or later
Privilege sudo for system policy and /etc/pki/containers/ key placement on the lab host
Scope Image signature policy — policy.json scopes and precedence, podman image trust show and set, trust types (accept, reject, signedBy, sigstoreSigned), podman image sign with GPG simple signing, registries.d sigstore paths, pull-time verification including unsigned and wrong-key failures, and Podman 5.8 vs 6 --signature-policy. Does not cover registry login, TLS CA setup, Cosign tutorials, or Kubernetes admission policy.
Related guides Podman login and registry auth
Configure a private registry
Configure registries.conf
Push images to a registry
Podman containers.conf

Before you tune signature policy, separate three layers that often get mixed up on the same podman pull:

text
Registry authentication  → Who are you?        → podman login / auth.json
TLS verification         → Correct HTTPS host? → CA certificates in certs.d
Image signature policy   → Signed by trusted key? → policy.json

All three can apply to one pull. Logging in does not replace signature verification, and trusting a registry TLS certificate does not mean every image from that registry is signed the way your policy requires.

IMPORTANT
This guide covers image signature policy — policy.json, podman image trust, and pull-time verification with signedBy and sigstoreSigned. It does not cover registry username/password authentication (podman login), TLS CA trust in certs.d, Cosign keyless workflows, or Kubernetes admission controllers that verify signatures at deploy time.

What is policy.json?

When Podman pulls from transports such as docker://, containers/image consults signature policy before accepting content. The system file on this host is:

text
/etc/containers/policy.json

Rootless sessions may also read:

text
$HOME/.config/containers/policy.json

depending on configuration. RHEL ships extra sigstoreSigned rules for Red Hat registries in the system file — do not assume upstream Podman defaults match your distribution copy verbatim.

List the active system policy path:

bash
ls -la /etc/containers/policy.json

On this host the file exists and includes a permissive default plus Red Hat registry sigstore rules.

Rootful podman with sudo typically reads /etc/containers/policy.json. Rootless sessions may prefer $HOME/.config/containers/policy.json when that file exists — if pulls behave differently between sudo podman and podman as your user, compare both paths before chasing signature errors.


Inspect the current trust policy

podman image trust show presents policy.json in a table:

bash
podman image trust show

Sample output on a fresh RHEL 10.2 host (before lab changes):

output
TRANSPORT      NAME                        TYPE            ID          STORE
all            default                     accept
repository     registry.access.redhat.com  sigstoreSigned  N/A
repository     registry.redhat.io          sigstoreSigned  N/A
docker-daemon                              accept

Columns map to transport, scope name, trust type, key identity when applicable, and signature store path.

Read the raw JSON Podman uses:

bash
podman image trust show --raw

The CLI accept type corresponds to insecureAcceptAnything in raw JSON — search for the requirement type, not the literal word accept in the file.

Machine-readable output:

bash
podman image trust show --json

Use --json when scripts need structured policy data.


Understand trust types

Podman trust CLI type Meaning in practice
accept No signature required for this scope
reject Refuse images from this scope
signedBy Require simple-signing GPG signature from configured public key
sigstoreSigned Require sigstore-format signatures per policy key paths

In raw policy.json, accept appears as:

json
{ "type": "insecureAcceptAnything" }

signedBy uses keyPath pointing at a public key file. sigstoreSigned on RHEL includes keyPath and rekorPublicKeyPath entries for Red Hat registry content.


How policy scope precedence works

Policy matches the most specific scope first, then falls back toward broader rules:

text
specific image or tag
repository path
registry host
default

Example scope names you might configure:

text
registry.example.test:5000/demo/hello
registry.example.test:5000/demo
registry.example.test:5000
default

If nothing more specific matches, default applies. A signedBy rule on registry.example.test:5000/demo does not automatically cover registry.example.test:5000/other — scope strings matter when debugging surprise pulls.

When two rules could match, the engine picks the longest matching scope name. That is why a registry-wide accept does not override a repository-level signedBy on the same host — the more specific repository rule wins for images under that path.


Reject a registry scope

Prove enforcement with a lab registry at registry.example.test:5000 (HTTP, marked insecure in registries.conf.d for this lab). After pushing a test image, reject the whole registry:

bash
podman image trust set --type reject registry.example.test:5000

Remove the local copy so the next pull must pass policy, not reuse a cached layer:

bash
podman rmi registry.example.test:5000/demo/hello:v1

Pull again and confirm policy blocks the registry scope:

bash
podman pull registry.example.test:5000/demo/hello:v1

Sample output on Podman 5.8.2:

output
Trying to pull registry.example.test:5000/demo/hello:v1...
Error: unable to copy from source docker://registry.example.test:5000/demo/hello:v1: Source image rejected: Running image docker://registry.example.test:5000/demo/hello:v1 is rejected by policy.

That error comes from policy — not from registry login or TLS alone.


Allow a registry without signatures

Return the lab registry to unsigned acceptance:

bash
podman image trust set --type accept registry.example.test:5000

Pull succeeds again:

bash
podman pull registry.example.test:5000/demo/hello:v1

Sample output:

output
Getting image source signatures
Copying blob sha256:ce980a8f5545faa3125a489aad32c00d6cf13d80a302308c3963b524085657af
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destination

accept means policy does not require a signature at that scope. It does not disable TLS, skip podman login, or imply the registry is secure in every other sense.


Podman 5.8 vs Podman 6 image trust set

On Podman 5.8.2 tested here, podman image trust set updates the normal policy file without an extra flag:

bash
podman image trust set --type reject registry.example.test:5000

podman image trust set --help on 5.8.2 lists only --type and --pubkeysfile — no --signature-policy option.

Podman 6+ requires an explicit policy path:

bash
podman image trust set --signature-policy /etc/containers/policy.json --type reject registry.example.test:5000

That is a real CLI breaking change. Commands in this article use 5.8 syntax unless noted; add --signature-policy when you move to Podman 6.


Configure signedBy for a repository

Simple signing needs a GPG key pair. This lab uses a disposable key in a dedicated GNUPGHOME — use your organization's key management in production.

Generate a signing key in an isolated keyring (lab only):

bash
export GNUPGHOME=/root/podman-image-signing-lab/gnupg
gpg --batch --passphrase '' --quick-generate-key 'lab@example.test' rsa4096 sign 0

List the long key ID you will pass to --sign-by:

bash
gpg --list-secret-keys --keyid-format long

Export the public key to the conventional system path:

text
/etc/pki/containers/lab-signing.pub

Podman does not auto-discover every file under /etc/pki/containers/ — policy references the path you pass to --pubkeysfile.

Require signatures for one repository scope:

bash
podman image trust set --type signedBy --pubkeysfile /etc/pki/containers/lab-signing.pub registry.example.test:5000/demo

podman image trust show then lists the scope as signed with the key identity.

On Podman 6+, add --signature-policy /etc/containers/policy.json to every podman image trust set command in this section.


Podman accepts any readable path with --pubkeysfile. /etc/pki/containers/ is the conventional system location on RHEL-family hosts for container signing public keys — it keeps verification material separate from TLS CA bundles and matches paths you will see in distribution documentation.

Podman does not scan that directory automatically. Policy references the exact file you pass to --pubkeysfile or the keyPath entry in raw JSON. Moving a key into /etc/pki/containers/ alone changes nothing until policy points at it.


Configure registries.d storage for GPG simple signatures

registries.conf and registries.d are different families. GPG simple-signing lookaside and sigstore: storage paths live under registries.d — this is not the same as the sigstoreSigned policy requirement, which verifies Sigstore-format signatures through separate policy and attachment configuration.

text
/etc/containers/registries.d/
$HOME/.config/containers/registries.d/

Point the lab registry at a file-based sigstore:

yaml
# /etc/containers/registries.d/registry.example.test:5000.yaml
docker:
  registry.example.test:5000:
    sigstore: file:///var/lib/containers/sigstore

Rootful default sigstore also resolves under /var/lib/containers/sigstore; rootless uses ~/.local/share/containers/sigstore when not overridden. The YAML makes the storage location explicit for this registry.

After podman image sign, inspect the sigstore tree to confirm a signature file landed for the digest:

bash
find /var/lib/containers/sigstore -type f

Podman stores signatures below the configured sigstore: path in directories derived from the full registry/repository identity and digest — not a shortened repository name alone. If that directory stays empty after signing, policy may still reject pulls because containers/image cannot read a signature for the reference.


Sign an image with podman image sign

Signing needs a registry-reference image — not a bare local ID without transport. On Podman 5.8.2, include the docker:// prefix:

bash
podman image sign --sign-by 4C6B6835E2EE66CD docker://registry.example.test:5000/demo/hello:v1

Replace the key ID with your signing subkey from gpg --list-secret-keys --keyid-format long. The command exits silently on success.

Podman stores the signature below the configured sigstore in directories derived from the full registry/repository identity and digest. Inspect the actual path with:

bash
find /var/lib/containers/sigstore -type f

Verify a signed image at pull time

The proof is pull-time enforcement — not merely signing a local image you never removed.

Workflow:

  1. Set signedBy policy for the repository scope
  2. Place the public key at the path referenced in policy
  3. Sign with podman image sign
  4. Remove the local image
  5. Pull again

After signing hello:v1, remove the local copy so the pull must verify the signature from sigstore:

bash
podman rmi registry.example.test:5000/demo/hello:v1

Pull the same tag and watch for signature verification lines in the transcript:

bash
podman pull registry.example.test:5000/demo/hello:v1

Sample output when the signature matches policy:

output
Getting image source signatures
Checking if image destination supports signatures
Copying blob sha256:ce980a8f5545faa3125a489aad32c00d6cf13d80a302308c3963b524085657af
Copying config sha256:5dd467fce50b56951185da365b5feee75409968cbab5767b9b59e325fb2ecbc0
Writing manifest to image destination
Storing signatures

A successful pull while signedBy is active shows that policy accepted the image. Confirm enforcement by also testing an unsigned image and an image signed by an untrusted key; both should be rejected. The sample lines above are containers/image copy progress messages — they do not by themselves prove cryptographic acceptance by your configured key.


Reject an unsigned image under signedBy

Push another tag under the same registry.example.test:5000/demo scope without signing it, then pull:

bash
podman pull registry.example.test:5000/demo/unsigned:v1

Sample output:

output
Trying to pull registry.example.test:5000/demo/unsigned:v1...
Error: unable to copy from source docker://registry.example.test:5000/demo/unsigned:v1: Source image rejected: A signature was required, but no signature exists

Same repository policy, different tag — no signature means rejection.


Reject an image signed with the wrong key

Sign with a second lab key that policy does not trust, then pull:

bash
podman pull registry.example.test:5000/demo/wrongsig:v1

Sample output:

output
Error: unable to copy from source docker://registry.example.test:5000/demo/wrongsig:v1: Source image rejected: None of the signatures were accepted, reasons: Missing key 964D1EBC8024F77DC9362351B466FFE8CA8C8B83

Verification checks the key identity — not merely whether any signature blob exists.


Understand sigstoreSigned

sigstoreSigned is separate from simple signedBy. RHEL 10 ships policy for Red Hat registries in the base policy.json:

json
{
    "type": "sigstoreSigned",
    "keyPath": "/etc/pki/sigstore/SIGSTORE-redhat-release3",
    "rekorPublicKeyPath": "/etc/pki/sigstore/REKOR-signing-key"
}

podman image trust show reports these as sigstoreSigned for registry.access.redhat.com and registry.redhat.io. That is distribution-managed sigstore verification — not the GPG simple-signing workflow you configure with --sign-by.

On Podman 5.8.2, podman image trust set --help accepts signedBy, accept, and reject only. Editing raw policy.json (or distribution packages) is how sigstoreSigned entries typically arrive — do not assume every Cosign keyless workflow maps one-to-one without checking your policy schema.


Compare signedBy and sigstoreSigned

signedBy sigstoreSigned
Mechanism GPG simple signing via podman image sign Sigstore-format signatures per policy paths
Typical setup Lab or org GPG key in /etc/pki/containers/ Vendor keys under /etc/pki/sigstore/ on RHEL
CLI trust type podman image trust set --type signedBy Usually preloaded in policy.json
This article Tested end-to-end on private registry Observed on Red Hat registry scopes

Set a default reject policy

For a controlled test host, reject everything unless explicitly allowed:

bash
podman image trust set --type reject default

Then allow specific scopes:

bash
podman image trust set --type accept quay.io
podman image trust set --type signedBy --pubkeysfile /etc/pki/containers/lab-signing.pub registry.example.test:5000/demo

Resulting table excerpt:

output
all            default                          reject
repository     quay.io                          accept
repository     registry.example.test:5000/demo  signed          lab@example.test

podman pull quay.io/podman/hello succeeds while scopes without an explicit allow rule hit default reject. Do not apply default reject on a production workstation until every registry you need has a matching allow or signedBy rule — ordinary pulls will fail.


Policy scope example

Layered policy on the lab host:

text
default                          → reject
quay.io                          → accept
registry.example.test:5000/demo  → signedBy (lab-signing.pub)

Test matrix:

  • quay.io/podman/hello — pulls without a custom signature (allowed by quay.io accept)
  • registry.example.test:5000/demo/hello:v1 — pulls only when signed with the trusted key
  • registry.example.test:5000/demo/unsigned:v1 — rejected (signature required)
  • Any registry not listed — rejected by default

That makes precedence tangible: broader registry.example.test:5000 accept rules and narrower …/demo signed rules can coexist — the more specific scope wins, not the broader one. Check podman image trust show for which scope matched.

Never treat podman image sign on a laptop as proof of registry enforcement until you delete the local image and pull from the registry again. The sign → publish signature → remove local copy → pull → verify-or-reject cycle is what production runtimes depend on.


Troubleshooting

Symptom Likely cause Fix
Pull succeeds but you expected rejection A more-specific accept rule matches, or you are inspecting a different policy file Check the exact image scope and active policy.json with podman image trust show --raw
rejected by policy reject on registry or default Add accept or signedBy for that scope
A signature was required, but no signature exists signedBy without podman image sign Sign the digest, confirm sigstore path in registries.d
None of the signatures were accepted Wrong signing key Sign with the key whose public half is in policy
unknown transport on sign Missing docker:// prefix Use docker://registry/... with podman image sign
Rootful policy differs from rootless Different policy.json paths Check /etc/containers/ vs ~/.config/containers/
trust set demands --signature-policy Podman 6+ Pass --signature-policy /etc/containers/policy.json

References


Summary

Image signature policy answers a different question than podman login or TLS trust: whether signed content from a registry scope is acceptable. policy.json drives that decision; podman image trust show translates it into a readable table and raw JSON.

On Podman 5.8.2, reject and accept scopes produce immediate pull success or failure, and signedBy enforces GPG simple signing when you configure registries.d sigstore paths, sign with podman image sign --sign-by, delete the local image, and pull again. Unsigned and wrong-key pulls fail with distinct errors — proof that verification inspects key identity, not just the presence of a signature file.

sigstoreSigned covers a separate verification path — visible on RHEL for Red Hat registries. Plan for the Podman 6 --signature-policy requirement when you upgrade podman image trust set automation.

When you roll out policy on a shared host, start with podman image trust show on the default file, add narrow signedBy rules for internal registries first, and only then consider default reject once every registry you need has an explicit scope. Registry authentication and TLS setup stay in their own guides; this page owns pull-time signature policy only.


Frequently Asked Questions

1. What is the difference between podman login and image signature policy?

podman login proves identity to a registry for authentication. policy.json decides whether image content from a registry scope is accepted based on signatures. TLS certificate trust is a third layer that verifies you reached the intended HTTPS endpoint. All three can apply to the same pull.

2. Where does Podman read signature policy?

The system policy file is /etc/containers/policy.json. Rootless users may also use $HOME/.config/containers/policy.json depending on configuration. podman image trust show displays the active policy in a readable table.

3. Why does podman pull fail with rejected by policy?

A matching policy scope requires reject, or signedBy when no valid signature exists for the image digest. Check podman image trust show for the most specific scope that matches the registry and repository path.

4. Does podman image sign work without docker:// in the image reference?

On Podman 5.8.2, podman image sign requires a transport prefix such as docker://registry.example.test:5000/demo/hello:v1. A bare registry path fails with unknown transport.

5. What changed in podman image trust set on Podman 6?

Podman 6 requires --signature-policy /etc/containers/policy.json on podman image trust set. Podman 5.8.x updates the normal policy path without that flag. Verify with podman image trust set --help on your host.
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)