First of all, we have to create a GPG key pair or select a locally available one:
> gpg --list-keys [email protected]
pub rsa2048 2018-11-26 [SC] [expires: 2020-11-25]
92836C5387398A449AF794CF8CE029DD1A866E52
uid [ultimate] Sascha Grunert <[email protected]>
sub rsa2048 2018-11-26 [E] [expires: 2020-11-25]
Now let’s assume that we run a container registry, for example on our local machine:
> sudo podman run -d -p 5000:5000 registry
The registry does not know anything about image signing, it just provides the remote storage for the container images. This means if we want to sign an image, we have to take care on our own how to distribute the GPG keys in the environment.
We choose a standard image alpine
image for our signing experiment:
> sudo podman pull docker://docker.io/alpine:latest
> sudo podman images alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
Now we re-tag the image to target it to our local registry:
> sudo podman tag alpine localhost:5000/alpine
> sudo podman images alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost:5000/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
docker.io/library/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
Podman would now be able to push the image and sign it in one command. But to
let this work, we have to modify our registries configuration at
/etc/containers/registries.d/default.yaml
:
# This is the default signature write location for docker registries.
default-docker:
sigstore: http://localhost:8000
sigstore-staging: file:///var/lib/containers/sigstore
We have two signature stores configured:
sigstore
: referencing a web server for signature readingsigstore-staging
: referencing a file path for signature writing
Now, let’s push and sign the image:
> sudo -E GNUPGHOME=$HOME/.gnupg \
podman push \
--tls-verify=false \
--sign-by [email protected] \
localhost:5000/alpine
…
Storing signatures
If we now take a look at the signature storage, then we see that there is a new signature available, which was caused by the image push:
> sudo ls /var/lib/containers/sigstore
'alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9'
The default signature store in
/etc/containers/registries.d/default.yaml
references a web server listening at
http://localhost:8000
. For our experiment, we simply start the server inside
the local staging signature store:
> sudo bash -c 'cd /var/lib/containers/sigstore && python3 -m http.server'
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Let’ remove the local images for our final test:
> sudo podman rmi docker.io/alpine localhost:5000/alpine
We have to write a policy to enforce that the signature has to be valid. This
can be done via a new rule in /etc/containers/policy.json
:
{
"default": [{ "type": "insecureAcceptAnything" }],
"transports": {
"docker": {
"localhost:5000": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/tmp/key.gpg"
}
]
}
}
}
The keyPath
does not exist yet, so we have to put the GPG key there:
> gpg --output /tmp/key.pgp --armor --export [email protected]
If we now pull the image:
> sudo podman pull --tls-verify=false localhost:5000/alpine
…
Storing signatures
e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a
Then we can see in the logs of the web server that the signature has been accessed:
127.0.0.1 - - [04/Mar/2020 11:18:21] "GET /alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9/signature-1 HTTP/1.1" 200 -
As an counterpart example, if we specify the wrong key at /tmp/key.pgp
:
> gpg --output /tmp/key.pgp --armor --export [email protected]
File '/tmp/key.pgp' exists. Overwrite? (y/N) y
Then a pull is not possible any more:
> sudo podman pull --tls-verify=false localhost:5000/alpine
Trying to pull localhost:5000/alpine...
Error: error pulling image "localhost:5000/alpine": unable to pull localhost:5000/alpine: unable to pull image: Source image rejected: Invalid GPG signature: …
@ecki I think it will be just a matter of time until they support sigstore signing as well :)