What do others use for ensuring the authenticity of images after downloading them with docker pull?
We’ve setup our CI build process to use docker for consistent, cross-platform builds. To ensure that our builds don’t use a malicious docker image (because the surface area of attack with TLS is enormous if you’re using X.509), we’ve been using DCT (Docker Content Trust).
Unfortunately, I just discovered that the official docker documentation says that DCT is being deprecated. Apparently this was announced last year, and in June this blog post was published with advice:
Cosign is not secure
We spent some time looking into cosign, but we discovered that the private keys aren’t actually in the hands of the developer.
Rather, they use this complicated setup using very insecure X.509 to issue temporary certificates.
The result is that the OIDC identity provider (e.g., GitHub) extends the vector of attack significantly – to probably tens of thousands of people – that can publish a malicious image that will be accepted by cosign as “trusted”
Notation (Notary v2)
I also looked at Notation (aka “Notary v2”), but there’s no way to bootstrap the software safely, since (perplexingly) their tool for verifying the authenticity of images using cryptographic signatures itself can’t be verified using a cryptographic signature.
Alternatives
Are there any other alternatives that I can use to replace DCT to ensure the authenticity (using cryptography) of the container images that I download – where the keys are actually held by the developer (thus significantly reducing the “insider threat” risk)?
What do you (or does your org do) to ensure that you’re not using maliciously-modified containers after pulling a new docker image?
Nix. ;)
Can I install nix securely (eg with
apt) to run inside the free CI runners provided by GitHub, GitLab, and Codeberg?Yes. Thats how I do it actually (not with apt). You can even point the runner at the nix community cache to prevent overly long build times. I have never trusted the Docker style of “I crammed everything I needed into this amorphous, way-too-capable, badly sandboxed, portable Alpine Linux box. Trust me bro, it’s secure” and prefer “here’s code showing exactly how I build this software with pinned, hashed versions that pass checks and you can use them too” with a simple flake file.
Here’s some pseudo code demonstrating it:
flake.nix
{ description = "hello-cli: pinned end to end by a lockfile, built in a sandbox"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { packages.default = pkgs.rustPlatform.buildRustPackage { pname = "hello-cli"; version = "0.1.0"; src = ./.; # No magic hash to paste. The committed Cargo.lock IS the pin: # Nix reads it and fetches every crate as a fixed-output # derivation. Change a dep, the lock changes, the build changes. cargoLock.lockFile = ./Cargo.lock; meta.mainProgram = "hello-cli"; }; devShells.default = pkgs.mkShell { packages = with pkgs; [ cargo rustc rust-analyzer clippy ]; }; }); }And a pertinent example runner.yml file:
when: - event: [push, pull_request, tag] steps: - name: build image: nixos/nix:2.28.3 environment: NIX_CONFIG: | experimental-features = nix-command flakes accept-flake-config = false sandbox = true substituters = https://cache.nixos.org/ https://nix-community.cachix.org/ trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= commands: # stale lock is a hard failure, so CI can never silently float - nix build .#default --no-update-lock-file --print-build-logs - ./result/bin/hello-cli nix # the money shot - nix path-info -r --closure-size --human-readable ./result # prove it again from scratch with zero cache and zero network - nix build .#default --rebuild --option substituters "" --print-build-logsIs there any way you can force it to use only software that’s crypographically signed (not hashes)?
My understanding is that nix uses a mix of source-based packages that are automatically pulled without verifying the sources crypgographically.
No. There’s no way to force it.
And the understanding is off in one spot: nothing gets “automatically pulled without verification.” Every source fetch has a pinned sha256 and fails loudly if it doesn’t match. It’s just that a hash proves the bytes didn’t change, not that upstream authored them.
Signatures do exist in Nix, but only on binary cache paths. Fixed-output derivations (every fetchurl, every fetchFromGitHub) are exempt by design, because the hash is the identity. That’s not a setting, it’s the model.
And there’s no signature to check even if you wanted one. Almost no nixpkgs package records an upstream key. So a hypothetical “signed only” mode would reject basically all of nixpkgs.
Guix does what you’re asking with guix git authenticate. Nix has no equivalent.
-
I use GPG. Podman supports GPG.
-
You can use cosign without any CA (full offline), using only your own CA.
Thanks. I haven’t looked into podman much. It came up in our research, but I saw
cosignin the docs and assumed it had the same vulnerabilities. I’d be curious of how to use podman with GPG (and assume you’re talking about the publishers signing the release, not me signing it?)Do you have any examples of popular, public images that you can use with podman, and verify the signature with GPG? Do you have any link to any guide that shows how to do this?
-


