v0.27.3
This commit is contained in:
1 parent
e84df445c6
commit
e87a382a7e
109 files changed
+1092
-2925
No files matched your search
@@ -0,0 +1,107 @@
|
||||
# OPNsense os-acme-client -> RustProxy
|
||||
|
||||
A simple mechanism for automatically sending a certificate generated by `os-acme-client` on OPNsense to RustProxy via API.
|
||||
|
||||
## Files
|
||||
|
||||
- `opnsense-rustproxy-cert-deploy.sh` - sends the certificate and private key to RustProxy.
|
||||
- `rustproxy-acme.conf` - connection settings, API token, and certificate file paths.
|
||||
- `actions_rustproxy_cert.conf` - optional `configd` action that allows the script to be executed from Cron in OPNsense.
|
||||
|
||||
## 1. Installation
|
||||
|
||||
```sh
|
||||
cp opnsense-rustproxy-cert-deploy.sh /usr/local/sbin/
|
||||
chmod 750 /usr/local/sbin/opnsense-rustproxy-cert-deploy.sh
|
||||
|
||||
cp rustproxy-acme.conf.example /usr/local/etc/rustproxy-acme.conf
|
||||
chmod 600 /usr/local/etc/rustproxy-acme.conf
|
||||
```
|
||||
|
||||
## 2. Configuration
|
||||
|
||||
Edit:
|
||||
|
||||
```sh
|
||||
vi /usr/local/etc/rustproxy-acme.conf
|
||||
```
|
||||
|
||||
Important parameters:
|
||||
|
||||
```sh
|
||||
RUSTPROXY_URL='https://127.0.0.1:9090'
|
||||
RUSTPROXY_KEY_ID='opnsense-acme'
|
||||
RUSTPROXY_TOKEN='RUSTPROXY_TOKEN'
|
||||
|
||||
CERTIFICATE_ID='shared-cert'
|
||||
|
||||
CERT_FILE='/var/etc/acme-client/certs/CERT_UUID/fullchain.pem'
|
||||
KEY_FILE='/var/etc/acme-client/keys/CERT_UUID/private.key'
|
||||
```
|
||||
|
||||
If `os-acme-client` exports the certificate to a custom directory, simply set the correct paths:
|
||||
|
||||
```sh
|
||||
CERT_FILE='/path/to/fullchain.pem'
|
||||
KEY_FILE='/path/to/private.key'
|
||||
```
|
||||
|
||||
`CERTIFICATE_ID` is the stable external certificate id. If it does not exist, RustProxy creates a manual certificate entry. If it already exists as a manual certificate, RustProxy rotates it in place. RustProxy-managed ACME ids are rejected to prevent two renewal owners for one lineage.
|
||||
|
||||
## 3. RustProxy API
|
||||
|
||||
Create a certificate API key in RustProxy and enter its values in the configuration:
|
||||
|
||||
```text
|
||||
RUSTPROXY_KEY_ID
|
||||
RUSTPROXY_TOKEN
|
||||
```
|
||||
|
||||
The script sends the certificate to:
|
||||
|
||||
```text
|
||||
POST /api/certificates/deploy
|
||||
```
|
||||
|
||||
The connection from OPNsense to RustProxy should use HTTPS.
|
||||
|
||||
## 4. Test
|
||||
|
||||
Run the certificate reconciliation:
|
||||
|
||||
```sh
|
||||
/usr/local/sbin/opnsense-rustproxy-cert-deploy.sh
|
||||
```
|
||||
|
||||
On success, you should see a message similar to:
|
||||
|
||||
```text
|
||||
RustProxy certificate reconcile succeeded (HTTP 200, id=shared-cert)
|
||||
```
|
||||
|
||||
## 5. OPNsense Automation
|
||||
|
||||
Optionally install the `configd` action:
|
||||
|
||||
```sh
|
||||
cp actions_rustproxy_cert.conf /usr/local/opnsense/service/conf/actions.d/
|
||||
service configd restart
|
||||
```
|
||||
|
||||
Then go to **System -> Settings -> Cron** and add a scheduled execution of the `Deploy ACME certificate to RustProxy` action.
|
||||
|
||||
The script deliberately reconciles on every run instead of trusting a local cache. The server is the source of truth: an identical RustProxy-owned certificate generation is a no-op, a missing id is created, and changed material is rotated atomically. This also repairs drift when a certificate entry or generation was removed outside OPNsense.
|
||||
|
||||
## Additional Options
|
||||
|
||||
If RustProxy uses a private CA:
|
||||
|
||||
```sh
|
||||
RUSTPROXY_CACERT='/usr/local/etc/ssl/rustproxy-ca.pem'
|
||||
```
|
||||
|
||||
If you want to connect to a specific LAN address while keeping proper TLS hostname verification:
|
||||
|
||||
```sh
|
||||
RUSTPROXY_RESOLVE='rustproxy.lan:8443:192.168.1.20'
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/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
|
||||
Reference in new issue
Block a user