diff --git a/content/en/docs/configuration.md b/content/en/docs/configuration.md index 18c21e6..21686cd 100644 --- a/content/en/docs/configuration.md +++ b/content/en/docs/configuration.md @@ -52,6 +52,41 @@ dns = ["172.30.5.1", "9.9.9.9"] Changing `bridge` or `subnet` affects machines started afterwards; the address recorded for a machine is reassigned from the new subnet on its next start. +## Signature policies + +What the images of a registry must carry, one table per registry (host, or +host:port). The hub's policy is built in (the project's key or the build +workflow's keyless identity, one of them required, its transparency log entry +checked): a table for `hub.nspawn.org` replaces it, and `verify = false` turns +the check off. Every other registry is verified only when it has a table. + +```toml +[registries."registry.example.com"] +key = "/etc/nspawn/keys/example.pub" # a cosign public key (PEM); keys = [...] for several +identity = "https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main" +issuer = "https://token.actions.githubusercontent.com" # goes with identity +required = true # default: no verifying signature fails the pull +rekor = true # default: the signature's transparency log entry must verify +trusted_root = "/etc/nspawn/trusted_root.json" # another Sigstore deployment; the public one otherwise + +[registries."hub.nspawn.org"] +verify = false # nothing is checked for this registry +``` + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `verify` | boolean | `true` | `false` turns verification off for the registry, the hub's built-in policy included. | +| `key`, `keys` | absolute path, list of absolute paths | none | Cosign public keys (PEM) the images may be signed with; `key` and `keys` add up. | +| `identity`, `issuer` | strings, both or neither | none | A keyless signer: the certificate's identity (the SAN, a URI or an email) and the issuer of the token behind it. A table names at least one key or an identity, unless `verify = false`. | +| `required` | boolean | `true` | `false` turns a missing signature into a note and lets the pull go on; a signature that is there but does not verify still fails it. | +| `rekor` | boolean | `true` | `false` trusts the key or certificate alone, without the transparency log entry (a private deployment that keeps no log). A bundle still has to carry a log entry or a timestamp, which cosign always adds. | +| `trusted_root` | absolute path | the public Sigstore root the binary embeds | The `trusted_root.json` of another Sigstore deployment. | + +The keys are read by the service when a pull needs them, and checked when it +starts: a key or a trusted root it cannot read is reported in its journal and +fails the pulls of that registry. What the command line was given with +`--registry` chooses the registry, never the policy. + The file is read by the service, so a change takes effect on its next start: `sudo systemctl restart nspawn.service` (or simply waiting for it to go idle) picks it up. `--config` on the command line does not reach it: the command line diff --git a/content/en/docs/faq.md b/content/en/docs/faq.md index 2c0a0ed..5f8a456 100644 --- a/content/en/docs/faq.md +++ b/content/en/docs/faq.md @@ -74,7 +74,15 @@ certificate from Fulcio, recorded in the Rekor transparency log), and with the project's key, whose public half is `cosign.pub` in that repository. The signatures are stored on the hub next to the image, as OCI referrers of its digest, so they cover every tag that points to it, and the hub verifies the -key one itself and shows the image as signed. To verify an image yourself: +key one itself and shows the image as signed. + +`nspawn pull` verifies them too, since 1.5.0: before a single layer is +downloaded, one of the two signatures has to verify offline against the +project's key and the workflow's identity, both built into nspawn, and an +image without a valid signature is refused (`--no-verify` skips the check for +one command; see [Signed images](/docs/images/#signed-images)). Other +registries get a [policy in the configuration](/docs/configuration/#signature-policies). +To verify an image yourself, with cosign: ```shell cosign verify \ diff --git a/content/en/docs/getting-started.md b/content/en/docs/getting-started.md index 22e6fe9..b9aef54 100644 --- a/content/en/docs/getting-started.md +++ b/content/en/docs/getting-started.md @@ -99,13 +99,18 @@ sudo nspawn pull fedora:44 ``` ```text -hub.nspawn.org/fedora:44: manifest 3f9c1a2b4d5e with 1 layer(s), assembling as overlay -blob 8a1e0c7f3b92: downloading -blob c0de4455aa01: downloading +hub.nspawn.org/fedora:44: signature verified (key 6wiWMtJZCUkV, keyless https://github.com/nspawn/mkosi-definitions/.github/workflows/mkosi.yml@refs/heads/master) +hub.nspawn.org/fedora:44: manifest 9240778b2c77 with 1 layer(s), assembling as overlay +blob f0c1e88e32b3: downloading +blob 28edf9a59c17: downloading +blob f0c1e88e32b3: downloaded +blob 28edf9a59c17: downloaded image fedora-44 (boot image) is ready: nspawn start fedora-44 ``` -The blobs went to `/var/lib/nspawn`, the root file system of the machine is +The image's signature was checked first (every image on the hub is signed by +the workflow that builds it; see [Signed images](/docs/images/#signed-images)), +then the blobs went to `/var/lib/nspawn`, the root file system of the machine is mounted at `/var/lib/machines/fedora-44`, `/etc/systemd/nspawn/fedora-44.nspawn` holds the settings nspawn boots it with, and a drop-in of `systemd-nspawn@fedora-44.service` makes the unit call nspawn around its life. diff --git a/content/en/docs/images.md b/content/en/docs/images.md index f171897..e51e34d 100644 --- a/content/en/docs/images.md +++ b/content/en/docs/images.md @@ -115,15 +115,60 @@ Docker Hub limits anonymous pulls per address; logging in lifts that. `push` authenticates before it uploads anything and, when the registry wants credentials it does not have, says which `login` to run. +## Signed images + +Every image on the hub is signed twice by the workflow that builds it +([nspawn/mkosi-definitions](https://github.com/nspawn/mkosi-definitions)) with +cosign: with the project's key, whose public half (`cosign.pub` in that +repository) is built into nspawn, and keyless, with the workflow's own identity +through Sigstore +(`https://github.com/nspawn/mkosi-definitions/.github/workflows/mkosi.yml@refs/heads/master`, +issued by GitHub). The signatures are referrers of the image on the registry: +Sigstore bundles that carry the certificate or the key's hint, the transparency +log entry and a timestamp, so `pull` verifies them offline, with nothing but the +registry consulted, against the Sigstore trusted root the binary embeds. One of +the two has to verify, and the check comes before a single layer is downloaded: + +```text +$ sudo nspawn pull fedora:44 +hub.nspawn.org/fedora:44: signature verified (key 6wiWMtJZCUkV, keyless https://github.com/nspawn/mkosi-definitions/.github/workflows/mkosi.yml@refs/heads/master) +hub.nspawn.org/fedora:44: manifest 9240778b2c77 with 1 layer(s), assembling as overlay +... +``` + +An image without a signature (a tag dated before the signing began, or one +pushed by hand with `nspawn push`) is refused with `carries no signature`, and +so is one whose signatures do not verify, with the reason of each. `--no-verify` +on `pull`, `run` and `create` skips the check for that command, like docker's +`--disable-content-trust`; `create` checks only an image it has to pull, never +a local source. `inspect` shows who signed (`signed_by`: `key , keyless +`) and when (`signed_at`), which a machine made with `create` +inherits from its source. + +Images of other registries are pulled unverified until the +[configuration file](/docs/configuration/#signature-policies) says what they +must carry, one table per registry; the same table can require other keys of +the hub, or turn its check off: + +```toml +[registries."hub.nspawn.org"] +verify = false +``` + +The policies belong to the service: `--registry` on the command line chooses +the registry, never the policy. + ## Pulling ```shell -sudo nspawn pull REFERENCE [--name NAME] [--backend BACKEND] [--mode MODE] [--force] +sudo nspawn pull REFERENCE [--name NAME] [--backend BACKEND] [--mode MODE] [--force] [--no-verify] ``` It resolves the reference to the manifest for the host's -platform (image indexes are followed), downloads every layer and the config -blob that is not already in the store, three at a time as docker does, checking +platform (image indexes are followed), checks the image's signatures where its +registry has a policy (the hub's is built in; see +[Signed images](#signed-images)) before anything is downloaded, downloads every +layer and the config blob that is not already in the store, three at a time as docker does, checking each one against its sha256 digest while it streams (a blob is written next to its final name and renamed only once verified, so an interrupted download never passes for a complete one, and a download cut short takes its part file with @@ -136,7 +181,7 @@ it), says of each blob when it is downloaded, and then: with, and the drop-in that makes `systemd-nspawn@NAME.service` call nspawn around its life; 4. records the image (reference, manifest digest, layers, backend, mode, - network) under `/var/lib/nspawn`, and keeps the manifest and blobs so that + network, who signed it) under `/var/lib/nspawn`, and keeps the manifest and blobs so that the image can be pushed or cloned later. An image with the same name is not replaced unless you pass `--force`, and diff --git a/content/en/docs/overview.md b/content/en/docs/overview.md index 81f28d3..f263d57 100644 --- a/content/en/docs/overview.md +++ b/content/en/docs/overview.md @@ -18,7 +18,8 @@ machines with a docker-like workflow: [mkosi](https://github.com/systemd/mkosi). Docker Hub and any other registry work as well: `search` looks on the hub and on Docker Hub at once, and `login` keeps credentials per registry, the way `docker login` does. -- Layers are downloaded once, verified against their digests and shared between +- An image of the hub is verified against its signature before anything comes + down; layers are downloaded once, verified against their digests and shared between the machines that use them. A machine's root file system is assembled from them by a [backend](/docs/images/#backends): an overlayfs mount, a native `systemd.mstack` directory, or a flat copy. `create` makes more machines from @@ -150,6 +151,7 @@ configured by systemd-networkd. See [Networking](/docs/networking/). - It does not build images by itself: `build` needs [mkosi](https://github.com/systemd/mkosi) installed on the host. - It does not sign images: the hub's are signed by the workflow that builds - them (see the [FAQ](/docs/faq/#are-the-images-signed)). Every blob is - checked against the sha256 digest in the manifest while it downloads, and - registries are reached over HTTPS only. + them, and `pull` verifies one of those signatures before it downloads + anything (see [Signed images](/docs/images/#signed-images)). Every blob is + then checked against the sha256 digest in the manifest while it downloads, + and registries are reached over HTTPS only. diff --git a/content/en/docs/reference.md b/content/en/docs/reference.md index c66809f..fb4f0b0 100644 --- a/content/en/docs/reference.md +++ b/content/en/docs/reference.md @@ -97,10 +97,12 @@ Forgets the credentials stored for a registry (the hub by default). ## pull ```text -nspawn pull REFERENCE [-n NAME] [--backend BACKEND] [--mode MODE] [-f] +nspawn pull REFERENCE [-n NAME] [--backend BACKEND] [--mode MODE] [-f] [--no-verify] ``` -Downloads an image from the hub or another registry and makes it available to +Downloads an image from the hub or another registry, checking its signature +first where the registry has a policy (the hub's is built in; see +[Signed images](/docs/images/#signed-images)), and makes it available to systemd-machined. On a terminal a bar shows how far each blob got, with its size, speed and time left; in a pipe or a log only the lines are written. @@ -111,6 +113,7 @@ size, speed and time left; in a pipe or a log only the lines are written. | `--backend auto\|overlay\|flat\|mstack` | How to assemble the image on this host. Default: `auto`, or the `backend` of the configuration file: `overlay`, or `flat` without overlayfs. `mstack` (systemd 261 or newer, managed user namespaces) is experimental. App images are assembled as `overlay` even when `mstack` is chosen. | | `--mode auto\|boot\|app` | Whether the image boots an init system or runs a single program. Default: `auto`. | | `-f`, `--force` | Replace an existing image with the same name. | +| `--no-verify` | Skip the signature check: the hub's images are signed and refused without a valid signature, and the configuration can ask the same of other registries; like docker's `--disable-content-trust`. | ## create @@ -119,7 +122,7 @@ nspawn create SOURCE NAME [--backend BACKEND] [--network NETWORK]... [--network- [-p [IP:]HOST:CONTAINER[/udp]]... [--entrypoint PROGRAM] [-e VAR[=VALUE]]... [-v SOURCE:TARGET[:ro]]... [-l KEY=VALUE]... [--restart POLICY] [-m SIZE] [--cpus N] [--pids-limit N] [HEALTHCHECK OPTIONS] [OTHER OPTIONS] [--secret SECRET]... - [-f] [-- ARGUMENTS...] + [-f] [--no-verify] [-- ARGUMENTS...] ``` Makes another machine from a local image, like `docker create`, without @@ -141,6 +144,7 @@ the image's own name, as `run` does. The layers are shared with the source. | `--restart`, `-m`, `--cpus`, `--pids-limit` | Restart policy and limits, as for [start](#start). Not inherited from the source. | | the healthcheck options, the other options, `--secret` | As for [start](#start). Not inherited from the source. | | `-f`, `--force` | Replace an existing machine with the same name. | +| `--no-verify` | Skip the signature check of an image that has to be pulled (a local source is not checked); like docker's `--disable-content-trust`. | | `-- ARGUMENTS...` | App images: replace the image's cmd; they follow its entrypoint, as with docker. | ## build @@ -243,9 +247,10 @@ nspawn inspect NAME... Prints everything nspawn knows about machines or images, running or not, as a JSON array with one object per name, like `docker inspect`: the record (image reference, digest, backend, mode, networks, addresses, aliases, ports, volumes, -environment, command, labels, restart policy, limits, healthcheck, secrets and -the other flags), for a running machine its state, start time, leader PID, OS -and health, and for a stopped one the exit code of its last run. The keys are +environment, command, labels, restart policy, limits, healthcheck, secrets, +the other flags, and who signed the image and when, as the pull verified it: +`signed_by`, `signed_at`), for a running machine its state, start time, leader +PID, OS and health, and for a stopped one the exit code of its last run. The keys are those of the [D-Bus interface](https://github.com/nspawn/nspawn/blob/master/docs/DBUS.md). @@ -315,7 +320,7 @@ Boots an image as a machine. Every option is remembered for the next start. ```text nspawn run [-d] [--rm] [-i] [-t] [-n NAME] [--pull missing|always|never] - [--backend BACKEND] [--mode MODE] [-f] [OPTIONS OF start] [--no-wait] + [--backend BACKEND] [--mode MODE] [-f] [--no-verify] [OPTIONS OF start] [--no-wait] REFERENCE [COMMAND [ARGUMENT...]] ``` @@ -346,6 +351,7 @@ image shows its console until it powers off, and Ctrl-C powers it off. | `--backend auto\|overlay\|flat\|mstack` | How to assemble the machine, as for `pull`. | | `--mode auto\|boot\|app` | As for `pull`; a mode other than `auto` always pulls. | | `-f`, `--force` | Make the machine anew when one of that name exists; it must be stopped. | +| `--no-verify` | Skip the signature check of the image, as for `pull`. | | the options of `start` | `--network`, `--network-alias`, `-p`, `--entrypoint`, `-e`, `-v`, `-l`, `--restart`, `-m`, `--cpus`, `--pids-limit`, the healthcheck options, the other options, `--secret` and `--no-wait` (with `-d` only), as for [start](#start). Options may come before or after the reference, as long as they come before the command. | ## stop diff --git a/hugo.yaml b/hugo.yaml index 053957e..a8e6596 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -53,7 +53,7 @@ params: privacy_policy: /about/#privacy # Version of nspawn the documentation describes. - version: 1.4.0 + version: 1.5.0 archived_version: false # In-page links to open issues and suggest changes.