Files
RustProxy/integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
T
2026-10-11 22:12:27 +02:00

138 lines
4.8 KiB
Bash
Executable File

#!/bin/sh
set -eu
# Deploy an externally issued OPNsense/os-acme-client certificate to RustProxy 0.23.x.
# Configuration is kept separately so the API token does not need to live in this script.
CONFIG_FILE="${RUSTPROXY_ACME_CONFIG:-/usr/local/etc/rustproxy-acme.conf}"
log() {
logger -t rustproxy-acme -- "$*" 2>/dev/null || true
printf '%s\n' "$*"
}
fail() {
log "ERROR: $*"
exit 1
}
[ -r "$CONFIG_FILE" ] || fail "Cannot read config: $CONFIG_FILE"
# shellcheck disable=SC1090
. "$CONFIG_FILE"
: "${RUSTPROXY_URL:?Set RUSTPROXY_URL in $CONFIG_FILE}"
: "${RUSTPROXY_KEY_ID:?Set RUSTPROXY_KEY_ID in $CONFIG_FILE}"
: "${RUSTPROXY_TOKEN:?Set RUSTPROXY_TOKEN in $CONFIG_FILE}"
: "${CERTIFICATE_ID:?Set CERTIFICATE_ID in $CONFIG_FILE}"
: "${CERT_FILE:?Set CERT_FILE in $CONFIG_FILE}"
: "${KEY_FILE:?Set KEY_FILE in $CONFIG_FILE}"
CURL="${CURL:-$(command -v curl || true)}"
OPENSSL="${OPENSSL:-$(command -v openssl || true)}"
[ -n "$CURL" ] || fail "curl not found"
[ -n "$OPENSSL" ] || fail "openssl not found"
case "$CERTIFICATE_ID" in
''|*[!A-Za-z0-9_-]*) fail "CERTIFICATE_ID may contain only A-Z, a-z, 0-9, _ and -" ;;
esac
[ "${#CERTIFICATE_ID}" -le 64 ] || fail "CERTIFICATE_ID is longer than 64 characters"
[ -r "$CERT_FILE" ] || fail "Certificate not readable: $CERT_FILE"
[ -r "$KEY_FILE" ] || fail "Private key not readable: $KEY_FILE"
# Validate certificate syntax and make sure the certificate and private key belong together.
"$OPENSSL" x509 -in "$CERT_FILE" -noout >/dev/null 2>&1 \
|| fail "Invalid certificate PEM: $CERT_FILE"
"$OPENSSL" pkey -in "$KEY_FILE" -noout >/dev/null 2>&1 \
|| fail "Invalid private key PEM: $KEY_FILE"
CERT_PUB=$(
"$OPENSSL" x509 -in "$CERT_FILE" -pubkey -noout 2>/dev/null \
| "$OPENSSL" pkey -pubin -outform DER 2>/dev/null \
| "$OPENSSL" dgst -sha256 2>/dev/null
)
KEY_PUB=$(
"$OPENSSL" pkey -in "$KEY_FILE" -pubout -outform DER 2>/dev/null \
| "$OPENSSL" dgst -sha256 2>/dev/null
)
[ -n "$CERT_PUB" ] && [ "$CERT_PUB" = "$KEY_PUB" ] \
|| fail "Certificate and private key do not match"
# Do not publish an already-expired certificate.
"$OPENSSL" x509 -in "$CERT_FILE" -checkend 0 -noout >/dev/null 2>&1 \
|| fail "Certificate is already expired"
# Always reconcile with RustProxy. The deploy endpoint is idempotent: an identical
# RustProxy-owned generation is a server-side no-op, while a missing certificate id
# is created and a changed manual certificate is rotated atomically. This avoids
# trusting stale local state when the RustProxy configuration was changed separately.
umask 077
TMP_JSON=$(mktemp "${TMPDIR:-/tmp}/rustproxy-cert.XXXXXX") || fail "mktemp failed"
TMP_CURL=$(mktemp "${TMPDIR:-/tmp}/rustproxy-curl.XXXXXX") || { rm -f "$TMP_JSON"; fail "mktemp failed"; }
TMP_RESPONSE=$(mktemp "${TMPDIR:-/tmp}/rustproxy-response.XXXXXX") || { rm -f "$TMP_JSON" "$TMP_CURL"; fail "mktemp failed"; }
trap 'rm -f "$TMP_JSON" "$TMP_CURL" "$TMP_RESPONSE"' EXIT HUP INT TERM
# PEM is line-oriented Base64 plus BEGIN/END markers. Escape it as a JSON string
# without needing jq/python on OPNsense.
pem_json() {
awk '{
gsub(/\\/, "\\\\");
gsub(/\"/, "\\\"");
sub(/\r$/, "");
printf "%s\\n", $0;
}' "$1"
}
{
printf '{"certificate_id":"%s","cert_pem":"' "$CERTIFICATE_ID"
pem_json "$CERT_FILE"
printf '","key_pem":"'
pem_json "$KEY_FILE"
printf '"}\n'
} > "$TMP_JSON"
# Keep the Bearer token out of curl's process arguments by using a mode-0600 config file.
cat > "$TMP_CURL" <<EOF_CURL
silent
show-error
connect-timeout = 10
max-time = 45
request = "POST"
url = "${RUSTPROXY_URL%/}/api/certificates/deploy"
header = "Authorization: Bearer ${RUSTPROXY_TOKEN}"
header = "X-RustProxy-Certificate-Key: ${RUSTPROXY_KEY_ID}"
header = "Content-Type: application/json"
data-binary = "@${TMP_JSON}"
output = "${TMP_RESPONSE}"
write-out = "%{http_code}"
EOF_CURL
# Optional trust anchor for an internal/self-signed administration certificate.
if [ -n "${RUSTPROXY_CACERT:-}" ]; then
[ -r "$RUSTPROXY_CACERT" ] || fail "CA file not readable: $RUSTPROXY_CACERT"
printf 'cacert = "%s"\n' "$RUSTPROXY_CACERT" >> "$TMP_CURL"
fi
# Optional curl --resolve equivalent, e.g. rustproxy.lan:8443:10.10.10.20.
# This lets the URL use the certificate DNS name while traffic goes to a fixed LAN IP.
if [ -n "${RUSTPROXY_RESOLVE:-}" ]; then
printf 'resolve = "%s"\n' "$RUSTPROXY_RESOLVE" >> "$TMP_CURL"
fi
HTTP_CODE=$("$CURL" --config "$TMP_CURL") || fail "HTTPS request to RustProxy failed"
case "$HTTP_CODE" in
2??)
log "RustProxy certificate reconcile succeeded (HTTP $HTTP_CODE, id=$CERTIFICATE_ID)"
cat "$TMP_RESPONSE"
printf '\n'
;;
*)
log "RustProxy certificate deploy failed (HTTP $HTTP_CODE)"
cat "$TMP_RESPONSE" >&2
printf '\n' >&2
exit 1
;;
esac