Files
pi-extensions/scripts/issue-client-cert.sh
T
shahondin1624 cdae16562a fix(scripts): correct ORG default Shah*ODin → Shahondin1624
Typo in the issuer org for newly minted client certs. Existing certs are
unaffected (Caddy validates against the root CA's public key, not subject
text). Future certs issued via this script will carry the corrected
O=Shahondin1624.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:43:18 +02:00

101 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# issue-client-cert.sh — Generate a new mTLS client identity signed by the
# local root CA. Run this ON THE CADDY HOST (where root-ca.key lives).
#
# Produces four files in $CERT_DIR, keyed on a per-device name:
# client-<name>.crt client cert (for pi CLI, nginx config, etc.)
# client-<name>.key client private key
# client-<name>.p12 modern bundle (PBES2/AES-256) — CLI use only
# client-<name>-legacy.p12 legacy bundle (3DES/RC2-40) — for browser import
#
# Usage:
# scripts/issue-client-cert.sh <device-name> [--force]
# <device-name> alphanumerics + dashes/underscores (e.g. laptop-alice)
# --cert-dir PATH override $CERT_DIR (default /mnt/ssdpool/@docker/caddy/certs)
# --days N validity (default 3650)
# --force overwrite existing files for this name
# --help show this message
set -euo pipefail
CERT_DIR="${CERT_DIR:-/mnt/ssdpool/@docker/caddy/certs}"
DAYS="${DAYS:-3650}"
ORG="${ORG:-Shahondin1624}"
COUNTRY="${COUNTRY:-DE}"
FORCE=0
NAME=""
usage() { sed -n '2,/^$/p' "$0" | sed 's/^#\{0,1\} \{0,1\}//'; exit 0; }
while [[ $# -gt 0 ]]; do
case "$1" in
--cert-dir) CERT_DIR="$2"; shift 2 ;;
--days) DAYS="$2"; shift 2 ;;
--force) FORCE=1; shift ;;
-h|--help) usage ;;
-*) echo "Unknown arg: $1" >&2; exit 1 ;;
*)
if [[ -z "$NAME" ]]; then NAME="$1"; shift
else echo "Extra positional arg: $1" >&2; exit 1; fi ;;
esac
done
[[ -n "$NAME" ]] || { echo "Usage: $0 <device-name>" >&2; exit 1; }
[[ "$NAME" =~ ^[A-Za-z0-9_-]+$ ]] || { echo "Invalid name: $NAME (alphanumerics, - and _ only)" >&2; exit 1; }
[[ -d "$CERT_DIR" ]] || { echo "Cert dir not found: $CERT_DIR" >&2; exit 1; }
cd "$CERT_DIR"
[[ -f root-ca.key && -f root-ca.pem ]] || { echo "root-ca.key and root-ca.pem must exist in $CERT_DIR" >&2; exit 1; }
for f in "client-${NAME}.key" "client-${NAME}.crt" "client-${NAME}.p12" "client-${NAME}-legacy.p12"; do
if [[ -e "$f" && $FORCE -eq 0 ]]; then
echo "Refusing to overwrite $CERT_DIR/$f (use --force)" >&2; exit 1
fi
done
echo "==> Generating 4096-bit RSA key + CSR for '$NAME'"
openssl genrsa -out "client-${NAME}.key" 4096 2>/dev/null
chmod 600 "client-${NAME}.key"
openssl req -new -key "client-${NAME}.key" -out "client-${NAME}.csr" \
-subj "/CN=${ORG} Client ${NAME}/O=${ORG}/C=${COUNTRY}"
echo "==> Signing with root CA"
openssl x509 -req -in "client-${NAME}.csr" \
-CA root-ca.pem -CAkey root-ca.key -CAcreateserial \
-out "client-${NAME}.crt" -days "$DAYS" 2>/dev/null
rm -f "client-${NAME}.csr"
echo "==> Bundling PKCS#12 (modern + legacy)"
openssl pkcs12 -export \
-out "client-${NAME}.p12" \
-inkey "client-${NAME}.key" \
-in "client-${NAME}.crt" \
-certfile root-ca.pem \
-name "${ORG} Client (${NAME})" \
-passout pass:
chmod 600 "client-${NAME}.p12"
openssl pkcs12 -legacy -export \
-out "client-${NAME}-legacy.p12" \
-inkey "client-${NAME}.key" \
-in "client-${NAME}.crt" \
-certfile root-ca.pem \
-name "${ORG} Client (${NAME}, legacy)" \
-passout pass:
chmod 600 "client-${NAME}-legacy.p12"
echo
echo "==> Done. Files in $CERT_DIR:"
ls -la "client-${NAME}".* | sed 's/^/ /'
echo
echo "To install on a pi client (run there):"
echo " mkdir -p ~/.pi/agent/certs"
echo " scp $(whoami)@$(hostname -f):${CERT_DIR}/client-${NAME}.crt ~/.pi/agent/certs/client.pem"
echo " scp $(whoami)@$(hostname -f):${CERT_DIR}/client-${NAME}.key ~/.pi/agent/certs/client-key.pem"
echo " scp $(whoami)@$(hostname -f):${CERT_DIR}/root-ca.pem ~/.pi/agent/certs/"
echo
echo "To import in a browser, fetch client-${NAME}-legacy.p12 and pass it to"
echo "scripts/install-browser-certs.sh on the client."