mirror of
https://github.com/containers/podman.git
synced 2025-05-20 00:27:03 +08:00
Add image signing with GPG tutorial
Signed-off-by: Sascha Grunert <sgrunert@suse.com>
This commit is contained in:
@ -23,3 +23,7 @@ A brief how-to on using the Podman remote-client.
|
|||||||
**[How to use libpod for custom/derivative projects](podman-derivative-api.md)**
|
**[How to use libpod for custom/derivative projects](podman-derivative-api.md)**
|
||||||
|
|
||||||
How the libpod API can be used within your own project.
|
How the libpod API can be used within your own project.
|
||||||
|
|
||||||
|
**[Image Signing](image_signing.md)**
|
||||||
|
|
||||||
|
Learn how to setup and use image signing with Podman.
|
||||||
|
194
docs/tutorials/image_signing.md
Normal file
194
docs/tutorials/image_signing.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# How to sign and distribute container images using Podman
|
||||||
|
|
||||||
|
Signing container images originates from the motivation of trusting only
|
||||||
|
dedicated image providers to mitigate man-in-the-middle (MITM) attacks or
|
||||||
|
attacks on container registries. One way to sign images is to utilize a GNU
|
||||||
|
Privacy Guard ([GPG][0]) key. This technique is generally compatible with any
|
||||||
|
OCI compliant container registry like [Quay.io][1]. It is worth mentioning that
|
||||||
|
the OpenShift integrated container registry supports this signing mechanism out
|
||||||
|
of the box, which makes separate signature storage unnecessary.
|
||||||
|
|
||||||
|
[0]: https://gnupg.org
|
||||||
|
[1]: https://quay.io
|
||||||
|
|
||||||
|
From a technical perspective, we can utilize Podman to sign the image before
|
||||||
|
pushing it into a remote registry. After that, all systems running Podman have
|
||||||
|
to be configured to retrieve the signatures from a remote server, which can
|
||||||
|
be any simple web server. This means that every unsigned image will be rejected
|
||||||
|
during an image pull operation. But how does this work?
|
||||||
|
|
||||||
|
First of all, we have to create a GPG key pair or select an already locally
|
||||||
|
available one. To generate a new GPG key, just run `gpg --full-gen-key` and
|
||||||
|
follow the interactive dialog. Now we should be able to verify that the key
|
||||||
|
exists locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> gpg --list-keys sgrunert@suse.com
|
||||||
|
pub rsa2048 2018-11-26 [SC] [expires: 2020-11-25]
|
||||||
|
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||||
|
uid [ultimate] Sascha Grunert <sgrunert@suse.com>
|
||||||
|
sub rsa2048 2018-11-26 [E] [expires: 2020-11-25]
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let’s assume that we run a container registry. For example we could simply
|
||||||
|
start one on our local machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo podman run -d -p 5000:5000 docker.io/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 of how to distribute the signatures.
|
||||||
|
|
||||||
|
Let’s choose a standard `alpine` image for our signing experiment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo podman pull docker://docker.io/alpine:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo podman images alpine
|
||||||
|
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
docker.io/library/alpine latest e7d92cdc71fe 6 weeks ago 5.86 MB
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can re-tag the image to point it to our local registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo podman tag alpine localhost:5000/alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> 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 system-wide registries configuration at
|
||||||
|
`/etc/containers/registries.d/default.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
default-docker:
|
||||||
|
sigstore: http://localhost:8000 # Added by us
|
||||||
|
sigstore-staging: file:///var/lib/containers/sigstore
|
||||||
|
```
|
||||||
|
|
||||||
|
We can see that we have two signature stores configured:
|
||||||
|
|
||||||
|
- `sigstore`: referencing a web server for signature reading
|
||||||
|
- `sigstore-staging`: referencing a file path for signature writing
|
||||||
|
|
||||||
|
Now, let’s push and sign the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo -E GNUPGHOME=$HOME/.gnupg \
|
||||||
|
podman push \
|
||||||
|
--tls-verify=false \
|
||||||
|
--sign-by sgrunert@suse.com \
|
||||||
|
localhost:5000/alpine
|
||||||
|
…
|
||||||
|
Storing signatures
|
||||||
|
```
|
||||||
|
|
||||||
|
If we now take a look at the systems signature storage, then we see that there
|
||||||
|
is a new signature available, which was caused by the image push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> sudo ls /var/lib/containers/sigstore
|
||||||
|
'alpine@sha256=e9b65ef660a3ff91d28cc50eba84f21798a6c5c39b4dd165047db49e84ae1fb9'
|
||||||
|
```
|
||||||
|
|
||||||
|
The default signature store in our edited version of
|
||||||
|
`/etc/containers/registries.d/default.yaml` references a web server listening at
|
||||||
|
`http://localhost:8000`. For our experiment, we simply start a new server inside
|
||||||
|
the local staging signature store:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> 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’s remove the local images for our verification 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 by adding a new rule in `/etc/containers/policy.json`. From the
|
||||||
|
below example, copy the `"docker"` entry into the `"transports"` section of your
|
||||||
|
`policy.json`.
|
||||||
|
|
||||||
|
```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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> gpg --output /tmp/key.gpg --armor --export sgrunert@suse.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If we now pull the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> 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.gpg`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> gpg --output /tmp/key.gpg --armor --export mail@saschagrunert.de
|
||||||
|
File '/tmp/key.gpg' exists. Overwrite? (y/N) y
|
||||||
|
```
|
||||||
|
|
||||||
|
Then a pull is not possible any more:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> 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: …
|
||||||
|
```
|
||||||
|
|
||||||
|
So in general there are four main things to be taken into consideration when
|
||||||
|
signing container images with Podman and GPG:
|
||||||
|
|
||||||
|
1. We need a valid private GPG key on the signing machine and corresponding
|
||||||
|
public keys on every system which would pull the image
|
||||||
|
2. A web server has to run somewhere which has access to the signature storage
|
||||||
|
3. The web server has to be configured in any
|
||||||
|
`/etc/containers/registries.d/*.yaml` file
|
||||||
|
4. Every image pulling system has to be configured to contain the enforcing
|
||||||
|
policy configuration via `policy.conf`
|
||||||
|
|
||||||
|
That’s it for image signing and GPG. The cool thing is that this setup works out
|
||||||
|
of the box with [CRI-O][2] as well and can be used to sign container images in
|
||||||
|
Kubernetes environments.
|
||||||
|
|
||||||
|
[2]: https://cri-o.io
|
Reference in New Issue
Block a user