Skip to content

Refuse to export a layer that carries a private key - #14

Merged
marcos-mendez merged 2 commits into
19.xfrom
fix/refuse-private-keys
Sep 29, 2026
Merged

marcos-mendez merged 2 commits into
19.xfrom
fix/refuse-private-keys

Conversation

@marcos-mendez

@marcos-mendez marcos-mendez commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

keel-core#8: all twelve layers exported so far carry the same private keys, and nothing in the build noticed. A list of files to remove only holds while someone remembers every package that makes a key.

Fix

layer_check_build runs layer_audit_keys on the built tree before anything is packed: a PEM private key header on a line of its own, text files only (grep -I), symlinks not followed. Every key found is named by its path in the image, and the layer is refused. A grep that fails is a failed audit, not a clean one. The exceptions are four inert examples shipped by packages, in conf/layer-inert-keys with a reason each, accepted only at their path and digest.

Evidence

  • Measured on the build host's layer trees: the anchored pattern finds exactly the real keys plus those four, about a second per layer. The unanchored one matched binaries (ssh, sshd, libgnutls, magic.mgc).
  • tests/layer: every key format, a key after a certificate, the quoted marker, a binary, a symlink out of the tree, the inert list by path and digest, grep failing or missing, and a bt-layer run whose build leaks a host key: refused, no tarball, no manifest. Five mutations each turn it red.
  • Coverage unchanged: layer-lib and bt-layer each keep the one uncovered line they have on 19.x (threshold 99).

Deploying the three together

This is one of three pull requests for keel-core#8: Keel-Linux/inithooks (generate on the machine), Keel-Linux/common (remove from the build), Keel-Linux/buildtasks (refuse at export). Merging deploys nothing: the build host checkouts are updated by hand. The order that matters is deployment:

  1. Publish inithooks 2.3.6+keel6 to the archive (attended, signing). Until then a layer without keys would boot with no TLS certificate until 15regen-sslcert, which only restarts services already running.
  2. Update /turnkey/fab-keel/common and /turnkey/buildtasks-keel on the build host in the same sitting: the audit without the removals refuses every layer.
  3. Rebuild the chain and publish (attended). Images published before this still carry the keys; keel-host-keys replaces them on any machine made from one.

navigator and others added 2 commits September 29, 2026 03:57
Every image built from a layer holds the same bytes, so a private key in a
layer is a key every machine built from it shares, and anyone who downloads
the layer has. keel-core#8 found three such keys in all twelve published
layers: the turnkey-ssl certificate key, which its postinst also copies into
/etc/ssl/private where the web servers, Webmin and the init fence read it;
the SSH host keys openssh-server generates at build time; and Webmin's
miniserv.pem. lapp and postgresql add the ssl-cert snakeoil key.

Removing them is common's job (removelists-final). This is the check that
holds whether or not a list remembers every package: layer_check_build now
scans the built tree for a PEM private key header on a line of its own,
text files only, symlinks not followed, and refuses the layer, naming every
key by its path in the image. A grep that fails is a failed audit, not a
clean one.

The exceptions are four inert examples that Debian and npm packages ship,
listed in conf/layer-inert-keys with the reason for each and accepted only
at their own path and digest, so a package update that changes one stops
the build until somebody reads it. Measured on the build host's layer trees:
the anchored pattern finds exactly the real keys and those four, in about a
second per layer.

Refs keel-core#8

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
From the security review of #14: the pattern required the header to end in
"PRIVATE KEY", and an armoured OpenPGP secret key ends in "PRIVATE KEY
BLOCK", so a secret keyring exported into a layer passed. Test: a secret
key block is refused, a public key block is not.

The comment on the pattern now also says what it cannot see: binary key
formats (DER, PKCS#12, Java keystores, PuTTY, GnuPG's private-keys-v1.d).
The review checked every layer tree on the build host for them and found
none; one that appears needs its own check.

Refs keel-core#8

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfWQScmDZ94KCS5DMYrfe6
@marcos-mendez
marcos-mendez merged commit bd4b055 into 19.x Sep 29, 2026
1 check passed
marcos-mendez added a commit that referenced this pull request Sep 29, 2026
From the security review of #14: the pattern required the header to end in
"PRIVATE KEY", and an armoured OpenPGP secret key ends in "PRIVATE KEY
BLOCK", so a secret keyring exported into a layer passed. Test: a secret
key block is refused, a public key block is not.

The comment on the pattern now also says what it cannot see: binary key
formats (DER, PKCS#12, Java keystores, PuTTY, GnuPG's private-keys-v1.d).
The review checked every layer tree on the build host for them and found
none; one that appears needs its own check.

Refs keel-core#8
marcos-mendez added a commit that referenced this pull request Sep 29, 2026
Refuse to export a layer that carries a private key
@marcos-mendez
marcos-mendez deleted the fix/refuse-private-keys branch September 29, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant