diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 488346e8..59e65dc3 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -23,7 +23,9 @@ # the overlay. The job writes the policy-rc.d that exits 101 fab installs # under. systemd is PID 1 in the container, so the is-active assertions are # made as well; the image build's case, no /run/systemd/system, is the one -# the suite reproduces itself where it needs it. Last, with ntpsec +# the suite reproduces itself where it needs it. etckeeper is installed +# with them, so its postinst makes /etc/.git, and the suite checks that +# keel-overlay-installer keeps private keys out of it (common#49). Last, with ntpsec # installed, tests/container-units-live.bats applies # conf/turnkey.d/container-units to the container itself: systemd skips # ntpsec and the two kernel mounts on the drop-ins' condition, and starting @@ -124,7 +126,7 @@ jobs: ./dist/keel-overlay-etcd_*.deb \ ./dist/keel-overlay-crowdsec_*.deb \ ./dist/keel-overlay-vip_*.deb \ - ntpsec + etckeeper ntpsec # unit states and manifests on the machine KEEL_OVERLAY_INSTALL_TEST=1 KEEL_DEB="$(ls /tmp/deps/keel_*.deb)" \ bats tests/overlay-install.bats diff --git a/COVERAGE.md b/COVERAGE.md index ee44bc3c..a1caea94 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -4,6 +4,16 @@ Measured on 2026-09-24 against upstream 19.x (b60dd23), following the project decision 0003 (90 percent floor per repository, 95 percent for every file our changes touch). +## Branch fix/etckeeper-private-keys: 100 percent, one new file (2026-10-09) + +`packages/installer/etckeeper-ignore` is measured for the first time: 37 +of 37 lines, kcov 43 on Debian 13, from `tests/etckeeper-ignore.bats` (18 +tests). The scratch /etc is a real git repository, with the global and +system git configuration set aside; git itself says what is ignored and +tracked. The live tests in `tests/overlay-install.bats` (CI job install) +run the postinst with the real etckeeper and contribute no percentage. +Every other target is unchanged at 100. + ## Branch fix/no-build-time-db-password-and-mysqltuner: 100 percent, two new files (2026-10-07) `conf/pgsql` and `conf/mysql` are measured for the first time: 13 of 13 and diff --git a/packages/README.md b/packages/README.md index a6095f45..c79a33f3 100644 --- a/packages/README.md +++ b/packages/README.md @@ -9,7 +9,7 @@ directory each, with its own changelog and version, released on its own | Directory | Package | Rests on | Units it owns | | --- | --- | --- | --- | -| `installer/` | `keel-overlay-installer` | Keel's `inithooks` (>= 2.3.6+keel14), `confconsole` (>= 2.2.3+keel8), `keel` (>= 0.12.0) | none; its manifest names the first boot hooks the three ship | +| `installer/` | `keel-overlay-installer` | Keel's `inithooks` (>= 2.3.6+keel14), `confconsole` (>= 2.2.3+keel8), `keel` (>= 0.12.0) | none; its manifest names the first boot hooks the three ship. Its postinst keeps private keys out of etckeeper's `/etc/.git` (below) | | `wireguard/` | `keel-overlay-wireguard` | trixie's `wireguard-tools` 1.0.20210914 | none; the interface is the instance spec's | | `etcd/` | `keel-overlay-etcd` | trixie's `etcd-server` 3.5.16 and its `etcd-client` (etcdctl, which keel asks etcd with over gRPC, keel#83), both bounded to that upstream release, `keel` (>= 0.21.0), whose gate its drop-in runs (keel 0.22.0 asks etcd with the etcdctl this brings), and `keel-overlay-wireguard`, which its manifest `requires` | `etcd.service`, and its drop-in `keel-overlay-etcd.conf`: one member restarts at a time | | `vip/` | `keel-overlay-vip` | `keel` (>= 0.21.0), whose `keel vip` the units run, and `keel-overlay-wireguard`, which its manifest `requires` | `keel-vip.service`, the VIP's root helper with etcd, which starts its unprivileged controller as the transient `keel-vip-control`; and `keel-vip-check.timer`, part of it | @@ -43,6 +43,49 @@ listener, `http://[::1]:2381`, which keel renders (`ETCD_LISTEN_METRICS_URLS`): the client port, 2379, wants TLS and a client certificate signed in the mesh. +## Private keys and etckeeper + +etckeeper commits all of `/etc` after each apt run and once a day. Keys +that keel, the overlays and Debian write under `/etc` went into +`/etc/.git` with it: `wireguard/wg0.key` and `mysql/keel-tls/server.key` +were seen there on real nodes (Keel-Linux/common#49). A key that is +rotated later stays in the history. + +`keel-overlay-installer`, which every Keel appliance has through +`keel-core`, ships `/usr/lib/keel/overlays/installer/etckeeper-ignore`, +and its postinst runs it at each configure: + +1. It writes this list into `/etc/.gitignore`, between its own marker + lines, outside the section etckeeper manages. etckeeper's `init`, + `update-ignore` and `uninit` keep every line outside that section, so + the list stays when the first boot hook `92etckeeper` makes the + repository again. + + | Path under `/etc` | Written by | + | --- | --- | + | `wireguard/*.key` | keel, the node's WireGuard key (`keel network wireguard key`) | + | `mysql/keel-tls/*.key` | keel, the database's TLS key | + | `etcd/keel/*.key` | keel, the etcd keys of the mesh | + | `keel/secrets/` | the secrets of the instance spec | + | `ssl/private/` | turnkey-ssl and ssl-cert | + | `ssh/ssh_host_*_key` | openssh-server, the host keys (the `.pub` files stay tracked) | + | `crowdsec/local_api_credentials.yaml`, `crowdsec/online_api_credentials.yaml`, `crowdsec/bouncers/*.yaml.local` | CrowdSec's packages and keel | + | `anubis/keel.key`, `anubis/keel.key.*` | `keel-overlay-anubis-key.service` | + +2. Where `/etc/.git` exists, it removes the tracked files of the list from + the index (`git rm --cached`) and commits that removal with the new + `.gitignore`, and with no other change. The files stay on disk. The + history is not rewritten, so each key that was tracked must be rotated: + the postinst lists the files. keel has no command that rotates a key + yet; keel makes each key once, and a renewal of a certificate keeps + the key. + +A failure (for example, a lock on the index) does not fail the +installation: the postinst says so, and the script can be run again as +root. `tests/etckeeper-ignore.bats` tests the script on a scratch +repository; `tests/overlay-install.bats` tests it with the real +etckeeper on a booted trixie container. + ## Building ``` diff --git a/packages/installer/debian/changelog b/packages/installer/debian/changelog index 63fbb063..78653fc1 100644 --- a/packages/installer/debian/changelog +++ b/packages/installer/debian/changelog @@ -1,3 +1,31 @@ +keel-overlay-installer (0.2.0) trixie; urgency=medium + + * etckeeper does not commit private keys and secrets into /etc/.git + (Keel-Linux/common#49). Seen on real nodes with keel 0.23.3: + wireguard/wg0.key at the keel installation and + mysql/keel-tls/server.key at a reinstallation. + * The postinst runs /usr/lib/keel/overlays/installer/etckeeper-ignore at + each configure. It writes this list into /etc/.gitignore, outside the + section etckeeper manages, so etckeeper's init, update-ignore and + uninit keep it: wireguard/*.key, mysql/keel-tls/*.key, + etcd/keel/*.key, keel/secrets/, ssl/private/, ssh/ssh_host_*_key, + crowdsec/local_api_credentials.yaml, + crowdsec/online_api_credentials.yaml, crowdsec/bouncers/*.yaml.local, + anubis/keel.key and anubis/keel.key.*. + * Where /etc/.git exists, it removes the tracked files of the list from + the index (git rm --cached) and commits that removal with the new + .gitignore, and with no other change. The files stay on disk. The + history is not rewritten. + * Rotate every key that was tracked: the old commits of /etc/.git still + hold it, and so does each copy or backup of /etc/.git. The postinst + lists the files it removed from the index. keel 0.23.3 has no command + that rotates a key: keel makes each key once, and a renewal of a + certificate (etcd, the database's TLS) keeps the same key. Rotate the + SSH host keys, the TLS keys of ssl/private, CrowdSec's credentials and + Anubis's signing key with the tools of their own packages. + + -- KeelLinux maintainers Fri, 09 Oct 2026 12:00:00 +0000 + keel-overlay-installer (0.1.0) trixie; urgency=medium * First release (handbook decision 0041, first implementation, step 2). diff --git a/packages/installer/debian/control b/packages/installer/debian/control index f8f12040..ecf14f0b 100644 --- a/packages/installer/debian/control +++ b/packages/installer/debian/control @@ -26,3 +26,8 @@ Description: Keel overlay: the installer . The manifest names the first boot hooks the three packages ship. The overlay declares no process: it is enabled in every installation mode. + . + It also keeps private keys and secrets out of etckeeper's /etc/.git: + the postinst writes a list of their paths into /etc/.gitignore and + removes the keys that are already tracked from the index, without a + change to the history. diff --git a/packages/installer/debian/postinst b/packages/installer/debian/postinst new file mode 100644 index 00000000..d5fb37ac --- /dev/null +++ b/packages/installer/debian/postinst @@ -0,0 +1,19 @@ +#!/bin/sh +# keel-overlay-installer: keep private keys and secrets out of etckeeper's +# /etc/.git (Keel-Linux/common#49). At each configure, the first +# installation and every upgrade or reinstall, etckeeper-ignore writes its +# list into /etc/.gitignore and, where /etc/.git exists, removes the +# tracked keys from the index in one commit, without a change to the +# history (packages/README.md, "Private keys and etckeeper"). A failure +# does not fail the installation: the operator is told how to run it again. +set -e + +if [ "$1" = configure ]; then + if ! /usr/lib/keel/overlays/installer/etckeeper-ignore; then + echo "keel-overlay-installer: warning: private keys can still be tracked in /etc/.git; run /usr/lib/keel/overlays/installer/etckeeper-ignore as root when the cause is corrected" >&2 + fi +fi + +#DEBHELPER# + +exit 0 diff --git a/packages/installer/debian/rules b/packages/installer/debian/rules index 847c0180..0a5e21cd 100755 --- a/packages/installer/debian/rules +++ b/packages/installer/debian/rules @@ -1,11 +1,13 @@ #!/usr/bin/make -f -# The overlay manifest sits beside debian/ (docs/manifest-v1.md, "Kinds, -# and where each file lives") and is installed under the name keel looks -# it up by. +# The overlay manifest and etckeeper-ignore sit beside debian/ +# (docs/manifest-v1.md, "Kinds, and where each file lives") and are +# installed under the names keel and the postinst look them up by. + +PKG = debian/keel-overlay-installer %: dh $@ execute_after_dh_auto_install: - install -D -m 0644 manifest.yaml \ - debian/keel-overlay-installer/usr/share/keel/overlays/installer.yaml + install -D -m 0644 manifest.yaml $(PKG)/usr/share/keel/overlays/installer.yaml + install -D -m 0755 etckeeper-ignore $(PKG)/usr/lib/keel/overlays/installer/etckeeper-ignore diff --git a/packages/installer/etckeeper-ignore b/packages/installer/etckeeper-ignore new file mode 100755 index 00000000..1579fc03 --- /dev/null +++ b/packages/installer/etckeeper-ignore @@ -0,0 +1,83 @@ +#!/bin/bash +# /usr/lib/keel/overlays/installer/etckeeper-ignore +# +# Keeps private keys and secrets out of etckeeper's /etc/.git +# (Keel-Linux/common#49). etckeeper commits all of /etc after each apt run +# and once a day, so a key that keel, an overlay or a package writes under +# /etc goes into the history: wireguard/wg0.key and +# mysql/keel-tls/server.key were seen there on real nodes. +# +# 1. Writes the list below into /etc/.gitignore, between its own two marker +# lines, after a removal of the list an earlier run wrote. etckeeper +# keeps every line outside its "managed by etckeeper" section through +# init, update-ignore and uninit, so the list stays after a first boot +# makes the repository again. +# 2. Where /etc/.git exists, removes from the index the files the list +# matches that are tracked, and commits that removal with the new +# .gitignore, and with no other change. The files stay on disk. The +# history is not rewritten: the old commits still hold the keys, so +# each key that was tracked must be rotated. +# +# keel-overlay-installer's postinst runs it at each configure. Exit codes: +# 0 done, 1 the removal or the commit failed (nothing is committed then). +# +# KEEL_ETC_DIR exists for tests/etckeeper-ignore.bats; on a machine it is +# /etc. +set -euo pipefail + +ETC="${KEEL_ETC_DIR:-/etc}" +IGNORE="$ETC/.gitignore" +BEGIN="# begin keel-overlay-installer: private keys and secrets, never committed (Keel-Linux/common#49)" +END="# end keel-overlay-installer" +MESSAGE="keel-overlay-installer: private keys and secrets removed from the index (Keel-Linux/common#49)" + +# Relative to /etc. Keys keel writes: the WireGuard key beside the +# interface's configuration, the database's TLS key, the etcd keys of the +# mesh, the secrets of the instance spec. Keys the overlays and Debian +# write: Anubis's signing key, CrowdSec's credentials, the TLS keys of +# turnkey-ssl and ssl-cert, the SSH host keys. Each statement is kept on +# one line, so that kcov counts every line it runs. +PATTERNS=("/wireguard/*.key" "/mysql/keel-tls/*.key" "/etcd/keel/*.key" "/keel/secrets/" "/ssl/private/" "/ssh/ssh_host_*_key" "/crowdsec/local_api_credentials.yaml" "/crowdsec/online_api_credentials.yaml" "/crowdsec/bouncers/*.yaml.local" "/anubis/keel.key" "/anubis/keel.key.*") + +write_list() { + local staged + staged="$(mktemp "$ETC/.gitignore.keel.XXXXXX")" + trap 'rm -f -- "$staged"' EXIT + chmod 0644 "$staged" + if [ -f "$IGNORE" ]; then + awk -v end="$END" 'index($0, "# begin keel-overlay-installer") == 1 { skip = 1; next } skip && $0 == end { skip = 0; next } !skip' "$IGNORE" > "$staged" + chmod --reference="$IGNORE" "$staged" + fi + { echo "$BEGIN"; printf '%s\n' "${PATTERNS[@]}"; echo "$END"; } >> "$staged" + mv -f -- "$staged" "$IGNORE" + trap - EXIT +} + +untrack() { + local excludes=() pattern tracked + for pattern in "${PATTERNS[@]}"; do + excludes+=(-x "$pattern") + done + tracked="$(git -C "$ETC" -c core.quotePath=false ls-files --cached --ignored "${excludes[@]}")" + if [ -z "$tracked" ]; then + return 0 + fi + if ! printf '%s\n' "$tracked" | GIT_LITERAL_PATHSPECS=1 git -C "$ETC" rm --cached --quiet --pathspec-from-file=- || ! git -C "$ETC" add -- .gitignore; then + echo "etckeeper-ignore: could not remove the private files from the index of $ETC/.git; nothing was committed" >&2 + return 1 + fi + export GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-keel-overlay-installer}" GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-root@localhost}" + export GIT_COMMITTER_NAME="${GIT_COMMITTER_NAME:-$GIT_AUTHOR_NAME}" GIT_COMMITTER_EMAIL="${GIT_COMMITTER_EMAIL:-$GIT_AUTHOR_EMAIL}" + if ! git -C "$ETC" commit --quiet -m "$MESSAGE"; then + echo "etckeeper-ignore: could not commit in $ETC/.git" >&2 + return 1 + fi + echo "etckeeper-ignore: these files are no longer tracked in $ETC/.git:" + echo " ${tracked//$'\n'/$'\n' }" + echo "etckeeper-ignore: the history of $ETC/.git still holds them: rotate each key and secret listed (Keel-Linux/common#49)" +} + +write_list +if [ -d "$ETC/.git" ]; then + untrack +fi diff --git a/tests/coverage.sh b/tests/coverage.sh index ecc6a058..bfd25e00 100755 --- a/tests/coverage.sh +++ b/tests/coverage.sh @@ -41,6 +41,7 @@ targets=( "overlays/turnkey.d/resolvconf-ifupdown-ng/etc/network/if-down.d/resolvconf-ifupdown-ng:tests/resolvconf-ifupdown-ng.bats" "packages/coraza/state:tests/coraza-state.bats" "packages/anubis/signing-key:tests/anubis-signing-key.bats" + "packages/installer/etckeeper-ignore:tests/etckeeper-ignore.bats" "lib/version-files.sh:tests/version-files.bats" "bin/keel-version-files:tests/version-files.bats" ) diff --git a/tests/etckeeper-ignore.bats b/tests/etckeeper-ignore.bats new file mode 100644 index 00000000..ffa06539 --- /dev/null +++ b/tests/etckeeper-ignore.bats @@ -0,0 +1,338 @@ +#!/usr/bin/env bats +# Tests for packages/installer/etckeeper-ignore, which keeps private keys +# and secrets out of etckeeper's /etc/.git (Keel-Linux/common#49). +# keel-overlay-installer's postinst runs it at each configure. +# +# The scratch /etc is a real git repository, and the ignore rules are read +# back by git itself (check-ignore, ls-files, add --all as etckeeper's +# commit runs it). The global and system git configuration is set aside, +# so the excludes of the machine that runs the tests change nothing. The +# real etckeeper on a booted trixie container is tests/overlay-install.bats'. + +bats_require_minimum_version 1.5.0 + +setup() { + TESTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)" + SCRIPT="$TESTS_DIR/../packages/installer/etckeeper-ignore" + export KEEL_ETC_DIR="$BATS_TEST_TMPDIR/etc" + export HOME="$BATS_TEST_TMPDIR/home" + export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL + mkdir -p "$KEEL_ETC_DIR" "$HOME" + ETC="$KEEL_ETC_DIR" +} + +# the private files keel and the overlays write under /etc +PRIVATE=( + wireguard/wg0.key + wireguard/wg1.key + mysql/keel-tls/server.key + etcd/keel/member.key + etcd/keel/root.key + etcd/keel/admin.key + keel/secrets/root_password + keel/secrets/cloud_api_key + ssl/private/cert.pem + ssl/private/cert.key + ssl/private/ssl-cert-snakeoil.key + ssh/ssh_host_ed25519_key + ssh/ssh_host_rsa_key + ssh/ssh_host_ecdsa_key + crowdsec/local_api_credentials.yaml + crowdsec/online_api_credentials.yaml + crowdsec/bouncers/crowdsec-firewall-bouncer.yaml.local + anubis/keel.key + anubis/keel.key.from-primary +) + +# public files beside them, which etckeeper keeps tracking +PUBLIC=( + hosts + wireguard/wg0.conf + mysql/keel-tls/ca.pem + mysql/keel-tls/server.pem + mysql/keel-tls/crl.pem + etcd/keel/ca.crt + etcd/keel/member.crt + etcd/keel/crl.pem + keel/instance.yaml + keel/monitor.json + ssh/ssh_host_ed25519_key.pub + ssh/sshd_config + crowdsec/bouncers/crowdsec-firewall-bouncer.yaml + anubis/keel.env +) + +# put FILE... in the scratch /etc, each holding its own name +write_files() { + local file + for file in "$@"; do + mkdir -p "$ETC/$(dirname "$file")" + printf '%s\n' "$file" > "$ETC/$file" + done +} + +# an /etc that etckeeper initialised and committed, with its managed +# section in .gitignore, every file of PRIVATE and PUBLIC tracked +etckeeper_repo() { + write_files "${PRIVATE[@]}" "${PUBLIC[@]}" + printf '%s\n' \ + "# begin section managed by etckeeper (do not edit this section by hand)" \ + "*.dpkg-*" \ + "# end section managed by etckeeper" \ + "inithooks.conf" > "$ETC/.gitignore" + git -C "$ETC" init -q + git -C "$ETC" add --all + git -C "$ETC" -c user.name=etckeeper -c user.email=root@localhost \ + commit -q -m "initial commit" +} + +tracked() { + git -C "$ETC" ls-files --error-unmatch -- "$1" >/dev/null 2>&1 +} + +block_count() { + grep -c '^# begin keel-overlay-installer' "$ETC/.gitignore" +} + +# ------------------------------------------------- the ignore list + +@test "an /etc with no .gitignore gets the list, and git ignores every private file" { + write_files "${PRIVATE[@]}" "${PUBLIC[@]}" + git -C "$ETC" init -q + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(block_count)" -eq 1 ] + local file + for file in "${PRIVATE[@]}"; do + git -C "$ETC" check-ignore -q -- "$file" || { + echo "not ignored: $file" + return 1 + } + done +} + +@test "the public files beside the keys are not ignored" { + write_files "${PRIVATE[@]}" "${PUBLIC[@]}" + git -C "$ETC" init -q + + run "$SCRIPT" + + [ "$status" -eq 0 ] + local file + for file in "${PUBLIC[@]}"; do + if git -C "$ETC" check-ignore -q -- "$file"; then + echo "ignored: $file" + return 1 + fi + done +} + +@test "etckeeper's section and the other lines stay, and a second run adds no second list" { + etckeeper_repo + before="$(cat "$ETC/.gitignore")" + + run "$SCRIPT" + [ "$status" -eq 0 ] + run "$SCRIPT" + [ "$status" -eq 0 ] + + [ "$(block_count)" -eq 1 ] + [ "$(head -4 "$ETC/.gitignore")" = "$before" ] +} + +@test "an older list is replaced by the current one" { + write_files "${PRIVATE[@]}" + printf '%s\n' "keep-me" \ + "# begin keel-overlay-installer: an older list" \ + "/old/*.key" \ + "# end keel-overlay-installer" \ + "keep-me-too" > "$ETC/.gitignore" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(block_count)" -eq 1 ] + run grep -c '^/old/\*\.key$' "$ETC/.gitignore" + [ "$output" = 0 ] + grep -qx keep-me "$ETC/.gitignore" + grep -qx keep-me-too "$ETC/.gitignore" + grep -qx '/wireguard/\*.key' "$ETC/.gitignore" +} + +@test "the file keeps its mode" { + : > "$ETC/.gitignore" + chmod 0640 "$ETC/.gitignore" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(stat -c %a "$ETC/.gitignore")" = 640 ] +} + +@test "a new .gitignore is 0644, as etckeeper makes its own" { + run "$SCRIPT" + [ "$status" -eq 0 ] + [ "$(stat -c %a "$ETC/.gitignore")" = 644 ] +} + +@test "with no /etc/.git, the list is written and nothing else is done" { + write_files "${PRIVATE[@]}" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(block_count)" -eq 1 ] + [ -z "$output" ] + [ ! -e "$ETC/.git" ] +} + +# ------------------------------------------------- keys already tracked + +@test "tracked keys leave the index in a new commit, and stay on disk" { + etckeeper_repo + old="$(git -C "$ETC" rev-parse HEAD)" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(git -C "$ETC" rev-parse HEAD~1)" = "$old" ] + local file + for file in "${PRIVATE[@]}"; do + if tracked "$file"; then + echo "still tracked: $file" + return 1 + fi + [ -f "$ETC/$file" ] + done + git -C "$ETC" log -1 --format=%s | grep -q 'Keel-Linux/common#49' + [ "$(git -C "$ETC" log -1 --format='%an <%ae>')" = "keel-overlay-installer " ] +} + +@test "the public files stay tracked" { + etckeeper_repo + + run "$SCRIPT" + + [ "$status" -eq 0 ] + local file + for file in "${PUBLIC[@]}"; do + tracked "$file" || { + echo "not tracked: $file" + return 1 + } + done +} + +@test "the history is not rewritten: the old commit still holds the keys" { + etckeeper_repo + old="$(git -C "$ETC" rev-parse HEAD)" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + git -C "$ETC" cat-file -e "$old:wireguard/wg0.key" + git -C "$ETC" cat-file -e "$old:mysql/keel-tls/server.key" + git -C "$ETC" merge-base --is-ancestor "$old" HEAD +} + +@test "the operator is told which files left the index, and to rotate them" { + etckeeper_repo + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [[ "$output" == *"wireguard/wg0.key"* ]] + [[ "$output" == *"mysql/keel-tls/server.key"* ]] + [[ "$output" == *"history"* ]] + [[ "$output" == *"rotate"* ]] +} + +@test "the commit holds the list and the removals, and no other change" { + etckeeper_repo + printf 'changed\n' >> "$ETC/hosts" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + run git -C "$ETC" diff --name-only HEAD~1 HEAD + [[ "$output" == *".gitignore"* ]] + [[ "$output" == *"wireguard/wg0.key"* ]] + [[ "$output" != *"hosts"* ]] + run git -C "$ETC" diff --name-only + [ "$output" = hosts ] +} + +@test "the committer's identity of the environment wins" { + etckeeper_repo + export GIT_AUTHOR_NAME=operator GIT_AUTHOR_EMAIL=operator@example.org + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(git -C "$ETC" log -1 --format='%an <%ae>')" = "operator " ] +} + +@test "with no key tracked, no commit is made" { + write_files "${PUBLIC[@]}" + git -C "$ETC" init -q + git -C "$ETC" add --all + git -C "$ETC" -c user.name=t -c user.email=t@t commit -q -m first + old="$(git -C "$ETC" rev-parse HEAD)" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ "$(git -C "$ETC" rev-parse HEAD)" = "$old" ] + [ -z "$output" ] +} + +@test "a key written later is not taken by etckeeper's add --all" { + etckeeper_repo + run "$SCRIPT" + [ "$status" -eq 0 ] + + write_files wireguard/wg2.key etcd/keel/client.key ssl/private/new.pem + git -C "$ETC" add --all + + run git -C "$ETC" diff --cached --name-only + [ -z "$output" ] +} + +@test "a failed removal fails the run and commits nothing" { + etckeeper_repo + old="$(git -C "$ETC" rev-parse HEAD)" + : > "$ETC/.git/index.lock" + + run "$SCRIPT" + + [ "$status" -eq 1 ] + [[ "$output" == *"could not remove"* ]] + [ "$(git -C "$ETC" rev-parse HEAD)" = "$old" ] +} + +@test "a refused commit fails the run, and the operator is told" { + etckeeper_repo + old="$(git -C "$ETC" rev-parse HEAD)" + printf '#!/bin/sh\nexit 1\n' > "$ETC/.git/hooks/pre-commit" + chmod 0755 "$ETC/.git/hooks/pre-commit" + + run "$SCRIPT" + + [ "$status" -eq 1 ] + [[ "$output" == *"could not commit"* ]] + [ "$(git -C "$ETC" rev-parse HEAD)" = "$old" ] +} + +@test "etckeeper's pre-commit hook runs: the commit is not made with --no-verify" { + etckeeper_repo + printf '#!/bin/sh\ntouch "%s"\n' "$BATS_TEST_TMPDIR/hook-ran" > "$ETC/.git/hooks/pre-commit" + chmod 0755 "$ETC/.git/hooks/pre-commit" + + run "$SCRIPT" + + [ "$status" -eq 0 ] + [ -e "$BATS_TEST_TMPDIR/hook-ran" ] +} diff --git a/tests/overlay-install.bats b/tests/overlay-install.bats index 95202242..af92b802 100644 --- a/tests/overlay-install.bats +++ b/tests/overlay-install.bats @@ -15,6 +15,8 @@ # # Every verdict is the one systemctl, dpkg, apt and keel give on the # machine; nothing reads back a file the packages wrote to decide a state. +# The etckeeper tests read /etc/.gitignore only to see that the list is +# there; their verdict is git's and etckeeper's. # # Refutations are written "run ! cmd", never a bare "! cmd": bash does not # apply errexit to a negated command, so a bare one asserts nothing unless @@ -25,6 +27,8 @@ bats_require_minimum_version 1.5.0 OVERLAYS=(installer wireguard etcd crowdsec vip) # the units the simple installation of Keel Core keeps disabled and stopped DISABLED_UNITS=(etcd.service crowdsec.service crowdsec-firewall-bouncer.service keel-vip.service) +# the key the etckeeper tests write; teardown removes it +TEST_KEY=/etc/wireguard/keel-test.key setup_file() { if [ "${KEEL_OVERLAY_INSTALL_TEST:-}" != 1 ]; then @@ -46,6 +50,8 @@ setup() { } teardown() { + # the key of the etckeeper tests is not left on the machine + rm -f "$TEST_KEY" # a test that died while it played an image build gives systemd back # its marker before anything else asks whether systemd runs restore_systemd_marker @@ -571,3 +577,61 @@ assert_simple_state() { [ ! -L "/etc/systemd/system/$unit" ] done } + +# ------------------------------------------- private keys and etckeeper + +# A key the installer overlay keeps out of /etc/.git (Keel-Linux/common#49), +# with etckeeper's own repository of this machine: the job installs +# etckeeper, whose postinst makes /etc/.git and commits /etc. + +@test "etckeeper's /etc/.gitignore carries the installer overlay's list" { + [ -d /etc/.git ] + grep -q '^# begin section managed by etckeeper' /etc/.gitignore + grep -q '^# begin keel-overlay-installer' /etc/.gitignore + git -C /etc check-ignore -q wireguard/wg0.key + git -C /etc check-ignore -q mysql/keel-tls/server.key + git -C /etc check-ignore -q etcd/keel/member.key +} + +@test "a reinstall of keel-overlay-installer takes a key etckeeper tracked out of the index, and keeps the history" { + [ -d /etc/.git ] + mkdir -p "${TEST_KEY%/*}" + install -m 0600 /dev/null "$TEST_KEY" + echo "not a real key" > "$TEST_KEY" + # a machine from before the fix: the key went into the history + git -C /etc add -f "${TEST_KEY#/etc/}" + etckeeper commit "test: a key tracked before Keel-Linux/common#49" >/dev/null + local old + old="$(git -C /etc rev-parse HEAD)" + + run dpkg -i "$(overlay_deb installer)" + echo "$output" + [ "$status" -eq 0 ] + [[ "$output" == *"${TEST_KEY#/etc/}"* ]] + + run git -C /etc ls-files --error-unmatch "${TEST_KEY#/etc/}" + [ "$status" -ne 0 ] + [ -f "$TEST_KEY" ] + git -C /etc cat-file -e "$old:${TEST_KEY#/etc/}" + git -C /etc merge-base --is-ancestor "$old" HEAD + # etckeeper's next commit does not take it back + if etckeeper unclean; then + etckeeper commit "test: after the reinstall" >/dev/null + fi + run git -C /etc ls-files --error-unmatch "${TEST_KEY#/etc/}" + [ "$status" -ne 0 ] +} + +@test "etckeeper's uninit and init keep the list, and its first commit takes no key" { + [ -d /etc/.git ] + mkdir -p "${TEST_KEY%/*}" + install -m 0600 /dev/null "$TEST_KEY" + # what overlays/turnkey.d/etckeeper's first boot hook does + etckeeper uninit -f >/dev/null + etckeeper init >/dev/null + etckeeper commit "initial commit" >/dev/null + + grep -q '^# begin keel-overlay-installer' /etc/.gitignore + run git -C /etc ls-files --error-unmatch "${TEST_KEY#/etc/}" + [ "$status" -ne 0 ] +}