Эх сурвалжийг харах

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 сар өмнө
parent
commit
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
 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
 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
 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
 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
 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
 # Make sure we start it with the *right* perl
 PERL5BIN=$(perl -V:scriptdir | sed -e "s/^scriptdir=//g;s/[';]//g")":$PATH"
 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" \
 sed -e "s|__REPLACEME__|$REPLACEME|g" \
     -e "s|__PERL5BIN__|$PERL5BIN|g" \
     -e "s|__PERL5BIN__|$PERL5BIN|g" \
     -e "s|__DOMAIN__|$DOMAIN|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"
 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
 # The application is expected to take it back out of its own environment as it
 # starts, so that nothing it forks later inherits 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
 fi
 
 
 # Bind the various dirs we need for chroot to work
 # 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__/
 Environment=PATH='__PERL5BIN__' HOME=__REPLACEME__/
 ExecStart=__REPLACEME__/service/tpsgi.sh
 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.
 # /run/credentials that only this service can read.
 #
 #
 # Not fatal when there is nothing there, which is the point of ImportCredential
 # 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
 # The workers end up chrooted into WorkingDirectory and /run/credentials is
 # outside it, so service/tpsgi.sh reads what it needs before that happens.
 # outside it, so service/tpsgi.sh reads what it needs before that happens.
-ImportCredential=tcms-vault
+ImportCredential=tpsgi-vault
 WorkingDirectory=__REPLACEME__/
 WorkingDirectory=__REPLACEME__/
 Restart=always
 Restart=always
 OOMPolicy=stop
 OOMPolicy=stop