diff --git a/Dockerfile b/Dockerfile index bd69532..0285efb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Stage 1: Build step-ca with acmeproxy plugin +# Stage 1: Build step-ca with acme-proxy plugin FROM golang:1.25.5-trixie AS build @@ -12,19 +12,19 @@ RUN make FROM chainguard/wolfi-base:latest -WORKDIR /acmeproxy -RUN chown -R nonroot:nonroot /acmeproxy/ +WORKDIR /acme-proxy +RUN chown -R nonroot:nonroot /acme-proxy/ COPY --from=build --chown=nonroot:nonroot /build/step-ca . # KV store mount point -RUN mkdir /acmeproxy/db && chown nonroot:nonroot /acmeproxy/db +RUN mkdir /acme-proxy/db && chown nonroot:nonroot /acme-proxy/db # ca.json mount point -RUN mkdir /acmeproxy/config && chown nonroot:nonroot /acmeproxy/config +RUN mkdir /acme-proxy/config && chown nonroot:nonroot /acme-proxy/config USER nonroot EXPOSE 443 -ENTRYPOINT [ "/acmeproxy/step-ca" ] -CMD [ "/acmeproxy/config/ca.json" ] +ENTRYPOINT [ "/acme-proxy/step-ca" ] +CMD [ "/acme-proxy/config/ca.json" ] diff --git a/README.md b/README.md index d9f597d..6fe2f8e 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,84 @@ -# ACME server as a Registration Authority +# ACME Server as a Registration Authority -Using [step-ca](https://github.com/smallstep/certificates) as a stand alone ACME server in [registration authority (RA)](https://smallstep.com/docs/registration-authorities/) mode, `acme-proxy` accepts certificate orders, and authenticates certificate requests over ACME protocol (RFC 8555). It does **NOT** sign certificates or store any private key. Instead, once acme-proxy validates that the ACME client succesfully solved challenge, certificate signing requests are then passed to an external certificate authority (e.g: Sectigo, ZeroSSL) using External Account Binding to sign and catalog. +`acme-proxy` is a standalone ACME server built on [step-ca](https://github.com/smallstep/certificates) that operates in [registration authority (RA)](https://smallstep.com/docs/registration-authorities/) mode. It accepts certificate orders and validates certificate requests using the ACME protocol (RFC 8555), but does **NOT** sign certificates or store private keys. -This approach is particularly useful for enterprise environments that cannot solve ACME challenges to get certificates issued by LetsEncrypt. Some of the possible reasons are listed below: +## How It Works -- Security policy prohibits opening http/80 to the global internet hence they cannot solve HTTP-01 challenge from LetsEncrypt. +When a client successfully completes an ACME challenge, `acme-proxy` forwards the certificate signing request to an external certificate authority (CA) that supports External Account Binding (EAB). The external CA signs the certificate and returns it to the client through `acme-proxy`. -- Can't use the DNS challenge due to a number of possible reasons: - - DNS management solution does not expose an api or there is no native integration with any standard ACME clients. - - Security policy prohibits from distributing api tokens or TSIG keys for large zones like candy. +**Note:** LetsEncrypt does not support EAB. However, commercial CAs such as Sectigo and ZeroSSL do. - Reference: - - [EFF blog post](https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation) - - [LetsEncrypt docs]() +## Use Cases -![How does acme-proxy work?](docs/sequence.png) +This architecture addresses typical enterprise constraints that prevent direct certificate issuance from LetsEncrypt: -## WARNING ⚠️ +**HTTP-01 Challenge Limitations:** -This is a work in progress. Not quite ready for production but will be soon. +- Security policies prohibit exposing port 80 to the public internet -**TODO** +**DNS-01 Challenge Limitations:** -- [x] Move config bits from env vars to `ca.json` -- [x] Implement Revoke method -- [x] Re-assess if `GetCertificateAuthority` is a requirement or not -- [x] Write unit tests -- [ ] Prometheus metrics -- [ ] Write admin docs -- [ ] Write user docs -- [ ] Write Helm chart +- Legacy DNS infrastructure lacks REST API support or ACME client integration +- Security policies restrict distribution of API tokens or TSIG keys for large DNS zones + +For more information on DNS-01 security considerations: + +- [EFF: Technical Deep Dive on ACME DNS Challenge Validation](https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation) +- [LetsEncrypt: DNS-01 Challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) + +## Benefits + +Using ACME with commercial CAs in enterprise environments provides several advantages: + +**Trusted Certificates:** + +- Certificates are signed by publicly trusted CAs are already in system trust stores +- Eliminates the operational burden of distributing and maintaining custom root certificates across endpoints, servers, and client devices + +**Automation and Self-Service:** + +- Leverage standard ACME clients (Certbot, acme.sh, cert-manager.io) for certificate issuance, automatic renewals. +- Enable self-service certificate requests for development teams + +## ACME Proxy Workflow + +`acme-proxy` runs as an ACME server inside your enterprise environment, acting as an intermediary between your internal infrastructure and an external certificate authority service (such as Sectigo). + +**Certificate Request Flow:** + +1. Your internal server (behind a firewall perimeter) requests a certificate from `acme-proxy` using standard ACME clients like certbot, acme.sh or cert-manager.io if you're using Kubernetes. +2. `acme-proxy` presents cryptographic challenges to verify domain ownership +3. Once validation succeeds, `acme-proxy` forwards the certificate signing request to your external CA using External Account Binding (EAB) +4. The external CA signs the certificate +5. `acme-proxy` retrieves the certificate bundle and returns it to your server + +![sequence diagram](docs/sequence.png) ## Quick Start -### Installer script +```sh +curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh | sudo sh +``` + +The script installs acme-proxy as a systemd service with sensible defaults, all of which can be overridden with environment variables: ```sh -curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/refs/heads/main/install.sh | sudo sh +# Default values (all overridable) +INSTALL_DIR="${INSTALL_DIR:-/opt/acme-proxy}" +DB_DIR="${DB_DIR:-${INSTALL_DIR}/db}" +CONFIG_FILE="${CONFIG_FILE:-${INSTALL_DIR}/ca.json}" +SERVICE_USER="${SERVICE_USER:-acme-proxy}" +SERVICE_GROUP="${SERVICE_GROUP:-acme-proxy}" ``` -### Build from source (Optional) +**Example with custom install directory:** + +```sh +curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh | \ + sudo INSTALL_DIR=/usr/local/acme-proxy SERVICE_USER=acmeservice sh +``` + +### Build from source (optional) Requirements: Go >= 1.25 @@ -50,7 +89,7 @@ Requirements: Go >= 1.25 ## Usage -Review and update configuration options in ca.json before starting the acme-proxy server. +Review and update configuration options in [ca.json](./ca.json) before starting the acme-proxy server. ```sh vim ca.json @@ -81,7 +120,7 @@ The most important parts of the config are - `ca_url` : ACME directory URL of external certificate authority. To get signed certs from InCommon use `https://acme.sectigo.com/v2/InCommonRSAOV` -Most commercial certificate authorities (such as Sectigo, ZeroSSL) support ACME over external account binding (EAB). You will need to to get EAB credentials i.e HMAC Key and Key ID associated with your account. +Most commercial certificate authorities (such as Sectigo) support certificate issuance over external account binding. You will need to get EAB credentials i.e HMAC Key and Key ID associated with your account. ```json "account_email": "certadmin@example.com", @@ -207,7 +246,7 @@ We have our certificate signed by InCommon 🎉 ### Renewing a certificate -Issuing a certificate is _generally_ not a problem in enterprise environments. But the ability to reliably renew certificates and reload services gracefully post renewal is. I am using the `--force` flag for renewal only because the default configuration in ACME clients only performs automatic renewal `1 < N < 30` number of days before certificate expiration. +Issuing a certificate is *generally* not a problem in enterprise environments. But the ability to reliably renew certificates and reload services gracefully post renewal is. I am using the `--force` flag for renewal only because the default configuration in ACME clients only performs automatic renewal `1 < N < 30` number of days before certificate expiration. ```sh $ ./acme.sh --renew --domain myserver.example.com --force @@ -263,20 +302,17 @@ N+c9XyDLAiEAkbrRKBsYc8YSgYviREF9u+gz7jK5JY2dsaRatEfb8Eg= Cert renewal was a success! ✨ -## Upstream docs - -- Step CA full configuration options - +### WARNING ⚠️ -- Certificate issuance policy configuration - +This is a work in progress. Not quite ready for production but will be very soon. -- Step CA Registration Authority (RA) - +#### TODO -- Step CA github repo - - -- Registration Authority (RA) related discussions - - - - +- [x] Move config bits from env vars to `ca.json` +- [x] Implement Revoke method +- [x] Re-assess if `GetCertificateAuthority` is a requirement or not +- [x] Write unit tests +- [ ] Prometheus metrics +- [ ] Write admin docs +- [ ] Write user docs +- [ ] Write Helm chart diff --git a/acmeproxy.service b/acmeproxy.service deleted file mode 100644 index 9b4bd57..0000000 --- a/acmeproxy.service +++ /dev/null @@ -1,47 +0,0 @@ -[Unit] -Description=ACME Proxy Server (step-ca) -Documentation=https://github.com/esnet/acme-proxy -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -#User=acmeproxy -#Group=acmeproxy - -# Paths -ExecStart=/opt/acmeproxy/step-ca /opt/acmeproxy/ca.json -WorkingDirectory=/opt/acmeproxy - -# Restart behavior -Restart=on-failure -RestartSec=5 -StartLimitIntervalSec=60 -StartLimitBurst=3 - -# Security hardening -#NoNewPrivileges=yes -#ProtectSystem=strict -#ProtectHome=yes -#PrivateTmp=yes -#PrivateDevices=yes -#ProtectKernelTunables=yes -#ProtectKernelModules=yes -#ProtectControlGroups=yes -#RestrictSUIDSGID=yes -#RestrictNamespaces=yes - -# Allow binding to privileged ports (443) -AmbientCapabilities=CAP_NET_BIND_SERVICE -CapabilityBoundingSet=CAP_NET_BIND_SERVICE - -# Allow write access to config and database directories -ReadWritePaths=/opt/acmeproxy - -# Logging -StandardOutput=journal -StandardError=journal -SyslogIdentifier=acmeproxy - -[Install] -WantedBy=multi-user.target diff --git a/ca.json b/ca.json index 8fc615d..09c3a97 100644 --- a/ca.json +++ b/ca.json @@ -6,7 +6,7 @@ }, "db": { "type": "bbolt", - "dataSource": "/opt/acmeproxy/db/bbolt" + "dataSource": "/opt/acme-proxy/db/bbolt" }, "authority": { "type": "externalcas", diff --git a/docs/develop.md b/docs/contribute.md similarity index 93% rename from docs/develop.md rename to docs/contribute.md index e12b304..ad87168 100644 --- a/docs/develop.md +++ b/docs/contribute.md @@ -7,7 +7,7 @@ ## Contribute -1. Fork this repo +1. Fork the repo 2. In your fork, create a new branch for your work 3. Commit & push changes to your fork 4. Submit a pull request diff --git a/docs/codebase.md b/docs/development.md similarity index 65% rename from docs/codebase.md rename to docs/development.md index 1f31ce8..abc9f12 100644 --- a/docs/codebase.md +++ b/docs/development.md @@ -1,10 +1,10 @@ -# Intro +# ACME server as Registration Authority -`acmeproxy` is based on [smallstep/certificates](https://github.com/smallstep/certificates). That go module contains a collection of packages although the most important one for this project is certificate authority service (cas). +See [upstream docs](#upstream-docs) section for more background on what registration authority, CAS are and how those concepts fits into step-ca architecture. ## Certificate Authority Service (CAS) -CAS provides a plugin based architecture that allows Step CA to delegate certificate signing to different backends - whether that's Google Cloud, HashiCorp Vault, or in our case, external certificate authorities like Sectigo or ZeroSSL. +CAS provides a plugin based architecture that allows Step CA to delegate certificate signing to different backends - whether that's Google Cloud, HashiCorp Vault, or in our case, external certificate authorities like Sectigo or ZeroSSL. ACME proxy can be run as a standalone ACME server in Registraiton Authority mode ### ExternalCAS @@ -36,3 +36,22 @@ const ( ExternalCAS = "externalcas" ) ``` + +## Upstream docs + +**Step CA github repo** + + +**Step CA Registration Authority (RA)** + + +**RA related github discussions** + +- +- + +**Step CA full configuration options** + + +**Certificate issuance policy configuration** + diff --git a/go.mod b/go.mod index bb5eca0..4dca766 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/esnet/acmeproxy +module github.com/esnet/acme-proxy go 1.25 diff --git a/install.sh b/install.sh index b3a2b09..6c477e6 100755 --- a/install.sh +++ b/install.sh @@ -2,11 +2,11 @@ set -e REPO="esnet/acme-proxy" -INSTALL_DIR="/opt/acmeproxy" -DB_DIR="${INSTALL_DIR}/db" -CONFIG_FILE="${INSTALL_DIR}/ca.json" -SERVICE_USER="${SERVICE_USER:-acmeproxy}" -SERVICE_GROUP="${SERVICE_GROUP:-acmeproxy}" +INSTALL_DIR="${INSTALL_DIR:-/opt/acme-proxy}" +DB_DIR="${DB_DIR:-${INSTALL_DIR}/db}" +CONFIG_FILE="${CONFIG_FILE:-${INSTALL_DIR}/ca.json}" +SERVICE_USER="${SERVICE_USER:-acme-proxy}" +SERVICE_GROUP="${SERVICE_GROUP:-acme-proxy}" # Detect OS and architecture OS=$(uname -s | tr '[:upper:]' '[:lower:]') @@ -51,7 +51,7 @@ mkdir -p "$INSTALL_DIR" mkdir -p "$DB_DIR" echo "Creating ca.json configuration file..." -cat > "$CONFIG_FILE" << 'EOF' +cat > "$CONFIG_FILE" << EOF { "address": ":443", "dnsNames": ["acmeproxy.example.com"], @@ -60,7 +60,7 @@ cat > "$CONFIG_FILE" << 'EOF' }, "db": { "type": "bbolt", - "dataSource": "/opt/acmeproxy/db/bbolt" + "dataSource": "${DB_DIR}/bbolt" }, "authority": { "type": "externalcas", @@ -128,12 +128,14 @@ echo "Setting ownership of installation directory..." chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "$INSTALL_DIR" echo "Installing systemd service..." -cat > /etc/systemd/system/acmeproxy.service << EOF +cat > /etc/systemd/system/acme-proxy.service << EOF [Unit] Description=ACME Proxy Server (step-ca) Documentation=https://github.com/esnet/acme-proxy After=network-online.target Wants=network-online.target +StartLimitIntervalSec=60 +StartLimitBurst=3 [Service] Type=simple @@ -141,38 +143,36 @@ User=${SERVICE_USER} Group=${SERVICE_GROUP} # Paths -ExecStart=/opt/acmeproxy/step-ca /opt/acmeproxy/ca.json -WorkingDirectory=/opt/acmeproxy +ExecStart=${INSTALL_DIR}/step-ca ${CONFIG_FILE} +WorkingDirectory=${INSTALL_DIR} # Restart behavior Restart=on-failure RestartSec=5 -StartLimitIntervalSec=60 -StartLimitBurst=3 # Security hardening -# NoNewPrivileges=yes -# ProtectSystem=strict -# ProtectHome=yes -# PrivateTmp=yes -# PrivateDevices=yes -# ProtectKernelTunables=yes -# ProtectKernelModules=yes -# ProtectControlGroups=yes -# RestrictSUIDSGID=yes -# RestrictNamespaces=yes +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=no +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RestrictNamespaces=yes # Allow binding to privileged ports (443) AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE # Allow write access to config and database directories -ReadWritePaths=/opt/acmeproxy +ReadWritePaths=${INSTALL_DIR} # Logging StandardOutput=journal StandardError=journal -SyslogIdentifier=acmeproxy +SyslogIdentifier=acme-proxy [Install] WantedBy=multi-user.target @@ -181,8 +181,8 @@ EOF echo "Reloading systemd daemon..." systemctl daemon-reload -echo "Enabling acmeproxy service..." -systemctl enable acmeproxy +echo "Enabling acme-proxy service..." +systemctl enable acme-proxy echo "" echo "Installation complete!" @@ -196,9 +196,9 @@ echo " - eab_kid: External Account Binding Key ID" echo " - eab_hmac_key: External Account Binding HMAC key" echo "" echo " 2. Start the service:" -echo " sudo systemctl start acmeproxy" +echo " sudo systemctl start acme-proxy" echo "" echo " 3. Check status:" -echo " sudo systemctl status acmeproxy" -echo " sudo journalctl -u acmeproxy -f" +echo " sudo systemctl status acme-proxy" +echo " sudo journalctl -u acme-proxy -f" echo "" diff --git a/main.go b/main.go index 7390a99..44a7df2 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,7 @@ import ( _ "go.step.sm/crypto/kms/yubikey" // Enabled cas interfaces. - _ "github.com/esnet/acmeproxy/externalcas" + _ "github.com/esnet/acme-proxy/externalcas" _ "github.com/smallstep/certificates/cas/cloudcas" _ "github.com/smallstep/certificates/cas/softcas" _ "github.com/smallstep/certificates/cas/stepcas"