acme-proxy 0.2.0

An ACME (RFC 8555) server that issues from a local CA, relays to an upstream CA, or delegates to a script
Documentation
# Custom Plugins Examples

This section provides complete examples of custom plugin scripts that can be
integrated into `acme-proxy`. These scripts must be marked as executable (`chmod
+x`).

## Custom signer script

A custom signer that passes the CSR to a fictional internal API to obtain a
certificate.

```bash
#!/bin/bash
# /etc/acme-proxy/signer/internal-pki.sh
set -e

# We only handle the "issue" hook in this example
if [ "$ACME_SIGNER_HOOK" = "issue" ]; then
    # Read the JSON payload from stdin
    PAYLOAD=$(cat)

    # Extract the base64 encoded CSR
    CSR_B64=$(echo "$PAYLOAD" | jq -r '.csr_der_base64')

    # Call internal PKI API
    # The API is expected to return a JSON with a 'certificate_pem' field.
    RESPONSE=$(curl -s -X POST https://pki.internal.company.com/api/sign \
        -H "Content-Type: application/json" \
        -d "{\"csr\": \"$CSR_B64\", \"order_id\": \"$ACME_SIGNER_ORDER_ID\"}")

    # Extract PEM from response
    PEM=$(echo "$RESPONSE" | jq -r '.certificate_pem')

    if [ -n "$PEM" ] && [ "$PEM" != "null" ]; then
        # Output the PEM chain to stdout (leaf first, then issuers)
        echo "$PEM"
        exit 0
    else
        # Exit 1 (any non-zero other than 3) = internal failure -> the client
        # gets a 500 and the order is marked invalid.
        #
        # Exit 3 is RESERVED for "this CSR is bad" -> the client gets a 400
        # badCSR and the order stays "ready" so it can retry with a corrected
        # CSR. Only use 3 when the PKI rejected the CSR itself, never for an
        # API outage like this one.
        echo "internal PKI API did not return a certificate" >&2
        exit 1
    fi
fi

# Hooks this example does not implement. `revoke` must succeed or the proxy
# leaves the order un-revoked, so returning non-zero here would be wrong for a
# real deployment;
# implement it, or set supports_crl/supports_renewal_info = false (the default)
# so those hooks are never invoked at all.
exit 1
```

## Custom filter script

A custom filter that checks the client IP against a threat intelligence feed
before allowing the connection.

```bash
#!/bin/bash
# /etc/acme-proxy/filters/threat-intel.sh

if [ "$ACME_FILTER_HOOK" = "connection" ]; then
    # Skip checking local IPs
    if [[ "$ACME_FILTER_CLIENT_IP" == 10.* ]] || [[ "$ACME_FILTER_CLIENT_IP" == 192.168.* ]]; then
        exit 0
    fi

    # Query threat intel API
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://threat.internal/api/check?ip=$ACME_FILTER_CLIENT_IP")

    if [ "$STATUS" = "200" ]; then
        # IP is clean
        exit 0
    else
        # IP is flagged, deny the request
        echo "Client IP $ACME_FILTER_CLIENT_IP is flagged in Threat Intel"
        exit 1
    fi
fi

exit 0
```

## Custom notification script

A custom notification script that sends a Slack message when a certificate is
revoked.

```bash
#!/bin/bash
# /etc/acme-proxy/notify/slack.sh

WEBHOOK_URL="https://hooks.slack.com/services/AAAAAAAAA/BBBBBBBBB/XXXXXXXXXXXXXXXXXXXXXXXX"

# The JSON payload is always on stdin; read it before anything else. For
# certificate_revoked it carries the reason code, which has no env var.
PAYLOAD=$(cat)

if [ "$ACME_NOTIFY_HOOK" = "certificate_revoked" ]; then
    # ACME_NOTIFY_IDENTIFIERS is only populated for certificate_issued, so it
    # would render empty here — take the order id from the environment and the
    # reason from the payload instead.
    REASON=$(echo "$PAYLOAD" | jq -r '.reason // "unspecified"')
    MESSAGE="🚨 Certificate Revoked! Serial: \`$ACME_NOTIFY_CERT_SERIAL\` | Order: \`$ACME_NOTIFY_ORDER_ID\` | Reason: \`$REASON\`"

    curl -s -X POST -H 'Content-type: application/json' \
        --data "{\"text\": \"$MESSAGE\"}" \
        "$WEBHOOK_URL"
fi

exit 0
```

> A notification script's exit code is only logged — it can never fail the ACME
> request that triggered it.