소스 검색

Make the vault key, and do not remake it

build_service now writes /etc/credstore.encrypted/tpsgi-vault when it is not
there, with systemd-creds --with-key=auto: sealed to the TPM when the machine
has a usable one and to the host key when it does not, so there is one command
here rather than a choice for somebody to get wrong.  It says which it got,
since that is the difference between a stolen disk being useless and being a
stolen key.  The key goes down a pipe into systemd-creds and is never a file, an
argument, or a line of scrollback on the way.

Whether it is already there has to be asked as root.  The store is 700 and root
owned, so a plain -e in this script cannot see into it and reports the key
absent every time -- which would have replaced it on every run, and replacing
it is the same as losing it.

Named tpsgi-vault rather than for whatever is being served, because this is
tPSGI's unit and one service is one application in one directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADwKSaBSvUTdA3RtGQeNau
George Baugh 1 개월 전
부모
커밋
5df0bc2fe3
4개의 변경된 파일과 102개의 추가작업 그리고 31개의 파일을 삭제
  1. 21 18
      Readme.md
  2. 74 6
      bin/build_service
  3. 3 3
      service/tpsgi.sh
  4. 4 4
      service/tpsgi.tmpl

+ 21 - 18
Readme.md

@@ -207,28 +207,31 @@ It's configured to start as root, then drop privs thanks to Net::Server's capabi
 
 An application often needs a key or a password that has no business living in
 its own checkout, where it would go into every backup alongside whatever it
-protects.  The unit imports those from systemd's credential store and
-service/tpsgi.sh hands them on.
+protects.  bin/build\_service makes one and puts it in systemd's credential
+store; the unit imports it and service/tpsgi.sh hands it on.
 
-Put one there, sealed to this machine so that the file is useless anywhere else:
-
-    openssl rand -base64 32 \
-      | sudo systemd-creds encrypt --name=tcms-vault - /etc/credstore.encrypted/tcms-vault
-
-...or unencrypted, if the machine has no TPM, in which case it is a root-owned
-file rather than a sealed one:
-
-    openssl rand -base64 32 | sudo tee /etc/credstore/tcms-vault > /dev/null
-    sudo chmod 600 /etc/credstore/tcms-vault
+You do not have to do anything for this.  build\_service writes
+`/etc/credstore.encrypted/tpsgi-vault` if it is not there already, encrypted
+with `systemd-creds --with-key=auto`: sealed to the TPM when the machine has a
+usable one, and to the host key when it does not.  It says which you got, since
+that is the difference between a stolen disk being useless and a stolen disk
+being a stolen key.  An existing key is left alone.
 
 systemd decrypts it into a ramdisk at `/run/credentials/$service` that only this
-service can read.  Nothing is written down in the unit, and nothing breaks if it
-is not there: `ImportCredential=` is quiet about a credential that does not
-exist, so an installation with no secrets to hand over starts normally.
+service can read.  Nothing breaks if it is not there: `ImportCredential=` is
+quiet about a credential that does not exist, so an installation whose
+application wants no secrets starts normally.
 
 The workers are chrooted into the application directory and that ramdisk is
 outside it, so service/tpsgi.sh reads the credential before the chroot happens
-and exports it -- `tcms-vault` becomes `TCMS_VAULT_KEY`.  An application taking
-one of these is expected to read it out of its environment as it starts and
-delete it there and then, so that nothing it forks afterwards inherits it.
+and exports it -- `tpsgi-vault` becomes `TPSGI_VAULT_KEY`.  An application taking
+one is expected to read it out of its environment as it starts and delete it
+there and then, so that nothing it forks afterwards inherits it.
+
+It is named for tPSGI rather than for whatever is being served, because this is
+tPSGI's unit and one service is one application in one directory.
 
+The key is not backed up and cannot be recovered.  That is deliberate: it lives
+and dies with the machine, so that it is never sitting in the same backup as the
+data it protects.  Rebuilding the host means whatever was sealed under it is
+gone and gets stored again.

+ 74 - 6
bin/build_service

@@ -1,15 +1,83 @@
-#!/bin/sh
+#!/bin/bash
 
-DOMAIN=$1
-REPLACEME=$(dirname $0)/..
-REPLACEME=$(readlink -f $REPLACEME)
+# Write the systemd unit for this application, and make sure it has a key to
+# hand it.
+#
+# Usage: bin/build_service [domain]
+
+set -u
+
+DOMAIN=${1:-}
+REPLACEME=$(dirname "$0")/..
+REPLACEME=$(readlink -f "$REPLACEME")
 # Make sure we start it with the *right* perl
 PERL5BIN=$(perl -V:scriptdir | sed -e "s/^scriptdir=//g;s/[';]//g")":$PATH"
 
+CREDENTIAL=tpsgi-vault
+# systemd reads this one, and /run/credstore.encrypted, and /usr/lib/... .  The
+# override is for a distribution that puts it somewhere else, and for testing
+# this without writing to /etc.
+CREDSTORE=${TPSGI_CREDSTORE:-/etc/credstore.encrypted}
+
+# An application which keeps secrets needs a key which is not kept where the
+# secrets are.  systemd's credential store is that place: root owned, decrypted
+# into a ramdisk only this service can read, and never anywhere a backup of the
+# application would pick it up.
+#
+# systemd-creds encrypt --with-key=auto seals to the TPM when there is one and
+# to the host key when there is not, so there is one command here rather than a
+# choice for somebody to get wrong.  What differs is how much a stolen disk
+# gets you, which is what gets reported below.
+#
+# Made once and then left alone.  Replacing it is the same as losing it: whatever
+# was sealed under the old one stops opening, and whoever stored it stores it
+# again.  That is the deal a key which lives and dies with its machine makes, and
+# it is the reason there is nothing here that writes one into $REPLACEME.
+setup_credential() {
+    if ! command -v systemd-creds > /dev/null; then
+        echo "No systemd-creds here, so no vault key.  Applications which wanted one will do without."
+        return 0
+    fi
+
+    local sealing
+    case "$(systemd-creds has-tpm2 2> /dev/null)" in
+        yes) sealing="sealed to this machine's TPM" ;;
+        *)   sealing="encrypted with the host key -- no usable TPM here, so a stolen disk is a stolen key" ;;
+    esac
+
+    sudo mkdir -p "$CREDSTORE" && sudo chmod 700 "$CREDSTORE" || {
+        echo "Could not make $CREDSTORE; skipping the vault key." >&2
+        return 1
+    }
+
+    # Asked as root, because the store is 700 and root owned: a plain -e in here
+    # cannot see into it and says the key is absent every single time.  That
+    # would quietly replace the key on every build_service, which is the same as
+    # losing it -- everything sealed under the old one stops opening.
+    if sudo test -e "$CREDSTORE/$CREDENTIAL"; then
+        echo "Vault key already in $CREDSTORE/$CREDENTIAL; leaving it alone."
+        return 0
+    fi
+
+    # Straight down a pipe and into systemd-creds: the key is never a file, an
+    # argument, or a line of scrollback on its way there.
+    if ! openssl rand -base64 32 \
+        | sudo systemd-creds encrypt --name="$CREDENTIAL" - "$CREDSTORE/$CREDENTIAL"; then
+        echo "Could not write $CREDSTORE/$CREDENTIAL; skipping the vault key." >&2
+        return 1
+    fi
+
+    sudo chmod 600 "$CREDSTORE/$CREDENTIAL"
+    echo "Wrote $CREDSTORE/$CREDENTIAL, $sealing."
+    echo "It is not backed up and cannot be recovered.  Losing this machine loses what was stored under it."
+}
+
+setup_credential
+
 sed -e "s|__REPLACEME__|$REPLACEME|g" \
     -e "s|__PERL5BIN__|$PERL5BIN|g" \
     -e "s|__DOMAIN__|$DOMAIN|g" \
-    -e "s|__HOME__|$UHOME|g" \
-    $REPLACEME/service/tpsgi.tmpl > $REPLACEME/service/tpsgi.service
+    -e "s|__HOME__|${UHOME:-}|g" \
+    "$REPLACEME/service/tpsgi.tmpl" > "$REPLACEME/service/tpsgi.service"
 
 echo "Wrote $REPLACEME/service/tpsgi.service"

+ 3 - 3
service/tpsgi.sh

@@ -19,9 +19,9 @@ echo "tPSGI running with EGID $GROUP";
 #
 # The application is expected to take it back out of its own environment as it
 # starts, so that nothing it forks later inherits it.
-if [[ -n $CREDENTIALS_DIRECTORY && -r "$CREDENTIALS_DIRECTORY/tcms-vault" ]]; then
-    export TCMS_VAULT_KEY=$(cat "$CREDENTIALS_DIRECTORY/tcms-vault")
-    echo "Loaded the tcms-vault credential"
+if [[ -n ${CREDENTIALS_DIRECTORY:-} && -r "$CREDENTIALS_DIRECTORY/tpsgi-vault" ]]; then
+    export TPSGI_VAULT_KEY=$(cat "$CREDENTIALS_DIRECTORY/tpsgi-vault")
+    echo "Loaded the tpsgi-vault credential"
 fi
 
 # Bind the various dirs we need for chroot to work

+ 4 - 4
service/tpsgi.tmpl

@@ -12,9 +12,9 @@ PIDFile=__REPLACEME__/run/tpsgi.pid
 Environment=PATH='__PERL5BIN__' HOME=__REPLACEME__/
 ExecStart=__REPLACEME__/service/tpsgi.sh
 
-# Secrets the application needs and should not be storing next to itself.  Taken
-# from the system credential store -- /etc/credstore, or /etc/credstore.encrypted
-# for one sealed to this machine's TPM -- and put in a ramdisk under
+# The key an application uses to seal whatever it keeps for its users, which has
+# no business living next to what it protects.  bin/build_service puts it in
+# /etc/credstore.encrypted, and systemd decrypts it into a ramdisk under
 # /run/credentials that only this service can read.
 #
 # Not fatal when there is nothing there, which is the point of ImportCredential
@@ -23,7 +23,7 @@ ExecStart=__REPLACEME__/service/tpsgi.sh
 #
 # The workers end up chrooted into WorkingDirectory and /run/credentials is
 # outside it, so service/tpsgi.sh reads what it needs before that happens.
-ImportCredential=tcms-vault
+ImportCredential=tpsgi-vault
 WorkingDirectory=__REPLACEME__/
 Restart=always
 OOMPolicy=stop