diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 0000000..f3f5dc1
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,35 @@
+changelog:
+ exclude:
+ labels:
+ - ignore-for-release
+ categories:
+ - title: Breaking Changes
+ labels:
+ - breaking-change
+ - title: New Features
+ labels:
+ - enhancement
+ - feature
+ - title: Bug Fixes
+ labels:
+ - bug
+ - fix
+ - title: Security
+ labels:
+ - security
+ - title: Documentation
+ labels:
+ - documentation
+ - docs
+ - title: CI / Build
+ labels:
+ - cicd
+ - build
+ - chore
+ - title: Dependencies
+ labels:
+ - dependencies
+ - go-deps
+ - title: Other Changes
+ labels:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d501737..a43c793 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,8 +30,10 @@ jobs:
sudo apt-get update
sudo apt-get install -y libpcsclite-dev
- - name: Setup dev environment
- run: make dev
+ - name: Check go.mod and go.sum are tidy
+ run: |
+ go mod tidy
+ git diff --exit-code go.mod go.sum
- name: Check formatting
run: |
@@ -68,9 +70,15 @@ jobs:
sudo apt-get install -y libpcsclite-dev
- name: Run tests
+ # Tests scoped to externalcas: the top-level package is a step-ca plugin entry
+ # point with no independently testable logic.
run: |
- make dev
- go test -v -race ./...
+ go test -v -race -coverprofile=coverage.txt ./externalcas
+
+ - name: Upload coverage results to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
security-scan:
name: Security Scan
@@ -89,9 +97,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y libpcsclite-dev
- - name: Setup dev environment
- run: make dev
-
- name: Run gosec
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
@@ -100,7 +105,7 @@ jobs:
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck -C ./externalcas ./...
+ govulncheck ./...
build:
name: Build (${{ matrix.os }}_${{ matrix.arch }})
@@ -133,6 +138,10 @@ jobs:
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.goarch }}
+ # CGO is disabled for arm64 because the runner is x86 and cross-compiling
+ # with CGO requires a cross-compiler toolchain. The arm64 binary is therefore
+ # built without CGO. If a CGO dependency is ever added, the arm64 build will
+ # fail here and the matrix must be updated to use a native arm64 runner.
CGO_ENABLED: ${{ matrix.arch == 'arm64' && '0' || '1' }}
MATRIX_OS: ${{ matrix.os }}
MATRIX_ARCH: ${{ matrix.arch }}
@@ -252,6 +261,7 @@ jobs:
generate_release_notes: true
files: |
dist/step-ca_linux_*
+ dist/*.bundle
dist/SHA256SUMS
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f2754f8..2448d87 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [main]
paths:
- - 'docs/**'
+ - 'docs/**'
workflow_dispatch:
permissions:
@@ -22,32 +22,32 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- - name: Checkout
- uses: actions/checkout@v7
- with:
- submodules: true # pulls in docs/themes/hugo-book
- fetch-depth: 0 # required for enableGitInfo = true
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ submodules: true # pulls in docs/themes/hugo-book
+ fetch-depth: 0 # required for enableGitInfo = true
- - name: Setup Hugo
- uses: peaceiris/actions-hugo@v3.2.1
- with:
- hugo-version: 'latest'
- extended: true # required for SCSS compilation in hugo-book theme
+ - name: Setup Hugo
+ uses: peaceiris/actions-hugo@v3.2.1
+ with:
+ hugo-version: '0.164.0'
+ extended: true # required for SCSS compilation in hugo-book theme
- - name: Configure Pages
- id: pages
- uses: actions/configure-pages@v6
+ - name: Configure Pages
+ id: pages
+ uses: actions/configure-pages@v6
- - name: Build
- env:
- BASE_URL: ${{ steps.pages.outputs.base_url }}
- run: hugo --environment public --minify --baseURL "$BASE_URL/"
- working-directory: docs
+ - name: Build
+ env:
+ BASE_URL: ${{ steps.pages.outputs.base_url }}
+ run: hugo --environment public --minify --baseURL "$BASE_URL/"
+ working-directory: docs
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v5
- with:
- path: docs/public
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: docs/public
deploy:
name: Deploy
@@ -60,6 +60,6 @@ jobs:
pages: write
id-token: write
steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index baf0726..e5e8e24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,6 @@ COMPARISON.md
.python-version
.ansible
pyproject.toml
+.claude
+.zed
+dcv-dev.json
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 59005f7..4410a7c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,24 +1,44 @@
---
exclude: ^docs/
repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.2.0
- hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
- - id: check-added-large-files
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.2.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+
+- repo: local
+ hooks:
+ - id: go-test
+ name: go test
+ entry: go test -race ./externalcas
+ language: system
+ pass_filenames: false
+ types: [go]
- - repo: local
+- repo: https://github.com/gitleaks/gitleaks
+ rev: v8.30.1
hooks:
- - id: go-test
- name: go test
- entry: go test -race ./externalcas
- language: system
- pass_filenames: false
- types: [go]
+ - id: gitleaks
+
+- repo: local
+ hooks:
+ - id: govulncheck
+ name: govulncheck
+ entry: govulncheck
+ args: ["-C", "./externalcas", "./..."]
+ language: system
+ types: [go]
+ pass_filenames: false
- - repo: https://github.com/gitleaks/gitleaks
- rev: v8.30.1
- hooks:
- - id: gitleaks
+- repo: local
+ hooks:
+ - id: staticcheck
+ name: staticcheck
+ entry: staticcheck
+ args: ["./externalcas"]
+ language: system
+ types: [go]
+ pass_filenames: false
diff --git a/Makefile b/Makefile
index 83d8f14..4720e94 100644
--- a/Makefile
+++ b/Makefile
@@ -10,17 +10,20 @@ BUILD_TIME ?= $(shell date -u '+%Y-%m-%d %H:%M UTC')
default: build
+test:
+ go test -race ./externalcas
+
clean:
- @echo "π§Ή Cleaning build cache..."
+ @echo "Cleaning build cache..."
go clean -cache
rm -f step-ca
rm -rf db
check-deps:
- @echo "π Checking for libpcsclite-dev dependency..."
+ @echo "Checking for libpcsclite-dev dependency..."
@OS_NAME=$$(uname -s); \
if [ "$$OS_NAME" = "Darwin" ]; then \
- echo "π macOS - Skipping dependency check"; \
+ echo "π MacOS - Skipping dependency check"; \
elif [ -f /etc/os-release ]; then \
. /etc/os-release; \
if echo "$$ID" | grep -Eqi 'ubuntu|debian'; then \
@@ -42,13 +45,13 @@ check-deps:
fi
dev: clean check-deps
- @echo "β¬οΈ Downloading dependencies to create dev environment..."
+ @echo "Setting up dependencies for development..."
mkdir db
go mod tidy
- @echo "β
Ready"
+ @echo "β OK"
build: clean check-deps
- @echo "βοΈ Building ACME proxy with Step CA..."
+ @echo "Building acme-proxy with step-ca..."
mkdir db
go build -ldflags="-s -w -X main.Version=$(VERSION) -X 'main.BuildTime=$(BUILD_TIME)'" -v -o $(APP_NAME) .
- @echo "β
Done"
+ @echo "β OK"
diff --git a/README.md b/README.md
index 61d9d65..ab3c03d 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,60 @@
# About
-`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.
+`acme-proxy` allows users to get certificates from any certificate authority that supports ACME protocol (such as LetsEncrypt, Sectigo, Digicert etc.) without opening http/80 to the internet _or_ distributing api keys for your DNS server! This 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.
## Documentation
Checkout our [documentation site](https://software.es.net/acme-proxy) for detailed examples on user guide, installation instructions, configuration etc.
-## How It Works
-
-`acme-proxy` runs as an ACME server inside your trusted network, acting as an intermediary between your internal infrastructure and an external certificate authority service (such as Sectigo). 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`.
-
-**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
-
-
-
-**Note:** LetsEncrypt does not support EAB. However, commercial CAs such as Sectigo and ZeroSSL do.
-
## Use Cases
This architecture addresses typical enterprise constraints that prevent direct certificate issuance from LetsEncrypt:
**HTTP-01 Challenge Limitations:**
-- Security policies prohibit exposing port 80 to the public internet
+- Security policies prohibit exposing port 80 to the public internet.
**DNS-01 Challenge Limitations:**
- 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
+- Security policies restrict distribution of API tokens or TSIG keys for large DNS zones
-For more information on security considerations when using DNS-01 challenge:
+For more information on security considerations when using DNS-01 TXT challenge:
- [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)
+
**How acme-proxy mitigates security risks**
+
+1. HTTP/80 exposure is limited to a trusted internal host rather than the global internet which is the case when using LetsEncrypt.
+2. No distributing DNS related api key or tsig key to ACME clients.
+
+
+## How It Works
+
+`acme-proxy` runs as an ACME server inside your trusted network, acting as an intermediary between your internal infrastructure and an upstream certificate authority which signs the certificate.
+
+1. Your internal server (behind a firewall perimeter) requests a certificate from `acme-proxy` using standard ACME client such as 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 an external certificate authority for signing
+4. `acme-proxy` retrieves the signed certificate bundle and returns it to your server
+
+
To get signed certificates from an external CA `acme-proxy` supports two modes of operations:
+
+### 1. External Account Binding (EAB)
+
+Some commercial certificate authorities allow their customers to validate their apex domain (example.com) once and issue an EAB key. Using this key customers can get certs for *.example.com without having to validate every domain or subdomain (say foo.example.com) individually.
+
+
+
+**Note:** LetsEncrypt does not support EAB. However, commercial CAs such as Sectigo, ZeroSSL, DigiCert do.
+
+### 2. DNS01-TXT
+
+[Lego](https://go-acme.github.io/lego/) is a well known ACME client which supports over 200 DNS providers to solve DNS01-TXT challenge. Using one of the Lego providers, acme-proxy authenticates with your DNS server and temporarily places a TXT record which the external CA can verify before issuing a signed certificate. The key benefit of using this mode is that your DNS server's API key or TSIG key lives only on acme-proxy and thus circumvents the need for distributing those credentials to all your servers.
+
+
+
## Quick Start
```sh
@@ -91,10 +106,11 @@ Review and update configuration options in [ca.json](./ca.json) before starting
vim ca.json
```
-Checkout our [official docs](https://software.es.net/acme-proxy/install/#configuration) for full set of configuration options. For quick start the most relevant config bits are:
+Refer to the [documentation](https://software.es.net/acme-proxy/configuration) for full set of configuration options. For a quick start the most relevant config bits are:
```json
- "dnsNames": ["acmeproxy.example.com"],
+{
+ "dnsNames": ["acme-proxy.example.com"],
...
"authority": {
"type": "externalcas",
@@ -103,26 +119,48 @@ Checkout our [official docs](https://software.es.net/acme-proxy/install/#configu
"account_email": "",
"eab_kid": "",
"eab_hmac_key": "",
- "metrics": {
- "enabled": true,
- "port": 9234,
- "dataSource": "/opt/acme-proxy/db/metrics"
+ "dns01_txt":{
+ "providers": "",
+ "dns_servers":"",
+ "env_vars": {}
}
- },
+ }
+ },
...
- "commonName": "acmeproxy.example.com"
- }
+ "commonName": "acme-proxy.example.com"
+}
```
-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. To get signed certs from InCommon use `https://acme.sectigo.com/v2/InCommonRSAOV` as shown below
+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. To get signed certs from CertiNext/InCommon use `https://acme-us.certinext.io/v1/directory` as shown below
```json
- "ca_url": "https://acme.sectigo.com/v2/InCommonRSAOV"
+ "ca_url": "https://acme-us.certinext.io/v1/directory"
"account_email": "certadmin@example.com",
"eab_kid": "",
"eab_hmac_key": "",
```
+To get certificates signed from LetsEncrypt use the following config options
+
+```json
+ "ca_url": "https://acme-v02.api.letsencrypt.org/directory"
+ "account_email": "certadmin@example.com",
+ "dns01_txt": {
+ "provider": "lego-dns-provider-code",
+ "dns_servers": ["8.8.8,8", "1.1.1.1", "2606:4700:4700::1111"],
+ "env_vars": {
+ "LEGO_PROVIDER_API_KEY": "xxxxxxx",
+ }
+ }
+```
+
+| Field | Description |
+|-------------------------|------------------------------------------------------------------------------|
+| `dns01_txt.provider` | [Lego Provider](https://go-acme.github.io/lego/dns/index.html) CLI Flag name |
+| `dns01_txt.dns_servers` | Use your authoritative DNS server's addresses to avoid caching/TTL problems |
+| `dns01_txt.env_vars` | Environment variables specific to your Lego DNS Provider for authentication |
+
+
### Starting acme-proxy
After configuring `ca.json` file simply start the systemd service
@@ -220,7 +258,6 @@ oSlzzVurgu0CIFeUruafCMHm2SzuP1eUCgAcMBHtTiugiduq+726bxcw2ln0noLE
[Tue 15 Jul 22:41:13 CDT 2025] Your cert key is in: /root/.acme.sh/myserver.example.com_ecc/myserver.example.com.key
[Tue 15 Jul 22:41:13 CDT 2025] The intermediate CA cert is in: /root/.acme.sh/myserver.example.com_ecc/ca.cer
[Tue 15 Jul 22:41:13 CDT 2025] And the full-chain cert is in: /root/.acme.sh/myserver.example.com_ecc/fullchain.cer
-
```
### Verify
diff --git a/ca.json b/ca.json
index 75716ab..ae43fc6 100644
--- a/ca.json
+++ b/ca.json
@@ -15,10 +15,15 @@
"account_email": "",
"eab_kid": "",
"eab_hmac_key": "",
+ "dns01_txt":{
+ "provider":"",
+ "dns_servers":[],
+ "env_vars":{
+ }
+ },
"metrics": {
- "enabled": true,
"port": 9234,
- "dataSource": "db/metrics"
+ "dataSource": "/opt/acme-proxy/db/metrics"
}
},
"provisioners": [
diff --git a/docs/assets/dns01-txt.png b/docs/assets/dns01-txt.png
new file mode 100644
index 0000000..a958e19
Binary files /dev/null and b/docs/assets/dns01-txt.png differ
diff --git a/docs/assets/sequence.png b/docs/assets/sequence.png
index 8397dbb..5f02b9a 100644
Binary files a/docs/assets/sequence.png and b/docs/assets/sequence.png differ
diff --git a/docs/content/_index.md b/docs/content/_index.md
index 9df87e9..8aa1cd4 100644
--- a/docs/content/_index.md
+++ b/docs/content/_index.md
@@ -4,25 +4,42 @@ title = 'acme-proxy'
## What is acme-proxy?
-`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 runs as a standalone server inside your enterprise environment, acting as an intermediary between your internal infrastructure and an external certificate authority service (such as Sectigo, DigiCert or ZeroSSL). It accepts certificate orders and validates certificate requests using the ACME protocol (RFC 8555), but does **NOT** sign certificates or store private keys.
+`acme-proxy` allows users to get certificates from any certificate authority that supports ACME protocol (such as LetsEncrypt, Sectigo, Digicert etc.) without opening http/80 to the internet _or_ distributing api keys for your DNS server! This 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.
-{{< image src="/assets/highlevel-flow.png" alt="sequence" >}}
+## Certificate Issuance Flow
-## Certificate issuance flow
+`acme-proxy` runs as an ACME server inside your trusted network, acting as an intermediary between your internal infrastructure and an upstream certificate authority which signs the certificate.
-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
+1. Your internal server (behind a firewall perimeter) requests a certificate from `acme-proxy` using a standard ACME client such as 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 an external certificate authority for signing.
+4. `acme-proxy` retrieves the signed certificate bundle and returns it to your server.
{{< image src="/assets/sequence.png" alt="sequence" >}}
+
To get signed certificates from an external CA `acme-proxy` supports two modes of operations:
+
+### 1. External Account Binding (EAB)
+
+Some commercial certificate authorities allow their customers to do a one time validation for their apex domain (example.com) and issue a key associated with their account called as external account binding key (EAB). Using this key customers may be able to get certs for *.example.com without having to perform validation for every domain or subdomain (say foo.example.com) individually.
+
+{{< image src="/assets/highlevel-flow.png" alt="sequence" >}}
+
+**Note:** LetsEncrypt does not support EAB. However, commercial CAs such as Sectigo, ZeroSSL, DigiCert do.
+
+### 2. DNS01-TXT
+
+`acme-proxy` carries [Lego](https://go-acme.github.io/lego/) as a Go dependency which is a well known ACME client that supports over 200 DNS providers to solve ACME challenges. Using one of the Lego providers, acme-proxy authenticates with your DNS server and temporarily places a TXT record which the external CA can verify before issuing a signed certificate. The key benefit of using this mode is that your DNS server's API key or TSIG key lives only on acme-proxy and thus circumvents the need for distributing and rotating those credentials across your infrastructure.
+
+
+
+**Note:** Using this mode also allows users to get signed certificates from LetsEncrypt!
+
## Connectivity Requirements
For the ACME certificate request issuance, renewal flow to work correctly, make sure your any internal firewalls, ACLs, IPtables rules permit the following traffic.
-**Client to acme-proxy (HTTPS/443)**
+### Client to acme-proxy (HTTPS/443)
Your servers running certbot must be able to connect to acme-proxy over HTTPS.
@@ -33,7 +50,7 @@ Protocol https (443)
Action allow
```
-**acme-proxy to Client (HTTP/80)**
+### acme-proxy to Client (HTTP/80)
`acme-proxy` validates HTTP-01 challenges by connecting to your servers directly on port 80. Your servers must allow inbound HTTP/80 from acme-proxy's IP β not from the public internet. This is the key security benefit: HTTP/80 exposure is limited to a trusted internal host rather than the global internet which is the case when using LetsEncrypt.
diff --git a/docs/content/client.md b/docs/content/client.md
index a4dce6e..dc24e35 100644
--- a/docs/content/client.md
+++ b/docs/content/client.md
@@ -1,6 +1,6 @@
+++
title = 'ACME Clients'
-weight = 30
+weight = 40
BookToC = true
+++
diff --git a/docs/content/configuration.md b/docs/content/configuration.md
new file mode 100644
index 0000000..ef161c5
--- /dev/null
+++ b/docs/content/configuration.md
@@ -0,0 +1,98 @@
++++
+title = 'Configure'
+weight = 30
+BookToC = true
++++
+
+# Required Configuration
+
+Since acme-proxy uses step-ca as the ACME server much of the required configuration options are set by step-ca itself. `acme-proxy` supports two modes - **eab** and **dns01_txt**. To get a certificate signed from an upstream CA, you must configure atleast _one of_ `eab` or `dns01_txt` modes. Depending on the upstream CA, you may need to configure both.
+
+```json {hl_lines=["13-22"]}
+{
+ "address": ":443",
+ "dnsNames": ["acmeproxy.example.com"],
+ "logger": {
+ "format": "json"
+ },
+ "db": {
+ "type": "bbolt",
+ "dataSource": "/opt/acme-proxy/db/bbolt"
+ },
+ "authority": {
+ "type": "externalcas",
+ "config": {
+ "ca_url": "",
+ "account_email": "",
+ "eab_kid": "",
+ "eab_hmac_key": "",
+ "dns01_txt": {
+ "provider": "",
+ "dns_servers": [],
+ "env_vars": {}
+ },
+ "metrics": {
+ "port": 9234,
+ "dataSource": "/opt/acme-proxy/db/metrics"
+ }
+ },
+ "provisioners": [
+ {
+ "type": "ACME",
+ "name": "acme",
+ "claims": {
+ "enableSSHCA": false,
+ "disableRenewal": false,
+ "allowRenewalAfterExpiry": false,
+ "disableSmallstepExtensions": true
+ }
+ }
+ ],
+ "backdate": "1m0s"
+ },
+ "tls": {
+ "minVersion": 1.2,
+ "maxVersion": 1.3,
+ "renegotiation": false
+ },
+ "commonName": "acmeproxy.example.com"
+}
+```
+
+
+## Field Reference
+
+Fields under `authority.config` are specific to acme-proxy.
+
+| Field | Required | Description |
+|-------|----------|-------------|
+| `address` | Yes | Listen address. `:443` binds all interfaces on port 443. |
+| `dnsNames` | Yes | Hostname(s) that this proxy is reachable at. acme-proxy requests a TLS cert for itself using these names on first start. |
+| `db.type` | Yes | Persistent KV data source to store ACME challenge state information |
+| `db.dataSource` | Yes | Path to the bbolt KV store directory. Must be writable by the service user. |
+| `authority.config.ca_url` | Yes | ACME directory URL of your upstream certificate authority. |
+| `authority.config.account_email` | Yes | Email registered with the upstream CA. |
+| `authority.config.certlifetime` | No | Request certificate with a max lifetime period if supported by upstream CA |
+| `authority.config.eab_kid` | Yes | External Account Binding Key ID, obtained from your CA's account portal. |
+| `authority.config.eab_hmac_key` | Yes | External Account Binding HMAC key, obtained from your CA's account portal. |
+| `authority.config.dns01_txt.provider` | Yes | [Lego Provider](https://go-acme.github.io/lego/dns/index.html) CLI Flag |
+| `authority.config.dns01_txt.dns_servers` | No | Use your authoritative DNS server's addresses to avoid caching/TTL problems |
+| `authority.config.dns01_txt.env_vars` | Yes | Environment variables specific to your Lego DNS Provider for authentication |
+| `authority.config.metrics.port` | No | Metrics port. Default: `9234`. |
+| `authority.config.metrics.datasource` | No | Prometheus metrics datastore. Default: `/opt/acme-proxy/db/metrics`. |
+| `commonName` | Yes | Common name for the proxy's own TLS certificate. Should match `dnsNames[0]`. |
+
+### Upstream CA URLs
+
+These are some commonly used certificate authorities which provide an ACME endpoint.
+
+| CA | ACME URL |
+|----|----------|
+| CertiNext | https://acme-us.certinext.io/v1/directory
+| Sectigo OV | `https://acme.sectigo.com/v2/OV` |
+| LetsEncrypt | `https://acme-v02.api.letsencrypt.org/directory` |
+| ZeroSSL | `https://acme.zerossl.com/v2/DV90` |
+
+# Step-CA
+
+step-ca is a swiss army knife of PKI. To see a full set of supported features and configuration options from `step-ca` please see to their [official documentation](https://smallstep.com/docs/step-ca/configuration/)
diff --git a/docs/content/install.md b/docs/content/install.md
index a03c799..de5a19c 100644
--- a/docs/content/install.md
+++ b/docs/content/install.md
@@ -6,14 +6,13 @@ BookToC = true
# Install
-Three methods are available. The install script is recommended for most deployments.
+There are a few methods for produciton installation. The install script is recommended for most deployments due to it's simplicity.
| Method | Best for |
|--------|---------|
| [Install script](#install-script-recommended) | Standard Linux servers, systemd environments |
-| [Pre-built binary](#pre-built-binary) | Environments where curl-pipe-to-shell is prohibited |
| [Build from source](#build-from-source) | Development, or architectures not covered by releases |
-| [Docker](#docker) | Container-based deployments |
+| [Docker](#docker) | Standalone container-based deployments
---
@@ -54,71 +53,7 @@ curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh |
| `$DB_DIR/bbolt` | bbolt KV store for ACME account state |
| `/etc/systemd/system/acme-proxy.service` | Systemd service unit |
-The service is **enabled but not started**. [Configure `ca.json`](#configuration) before starting.
-
----
-
-## Pre-built Binary
-
-Download the release binary directly from the [GitHub releases page](https://github.com/esnet/acme-proxy/releases), verify the checksum, and install manually.
-
-```sh
-VERSION=1.0.0 # replace with the current release
-
-# Download binary and checksum
-curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/v${VERSION}/step-ca_linux_amd64"
-curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/v${VERSION}/step-ca_linux_amd64.sha256"
-
-# Verify
-sha256sum -c step-ca_linux_amd64.sha256
-
-# Install
-sudo install -o root -g root -m 0755 step-ca_linux_amd64 /opt/acme-proxy/step-ca
-```
-
-> For arm64, substitute `amd64` with `arm64` in the filename.
-
-After placing the binary, create the config directory and set up `ca.json` manually (see [Configuration](#configuration)), then create a systemd service unit:
-
-```sh
-sudo mkdir -p /opt/acme-proxy/db
-
-sudo tee /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
-User=acme-proxy
-Group=acme-proxy
-ExecStart=/opt/acme-proxy/step-ca /opt/acme-proxy/ca.json
-WorkingDirectory=/opt/acme-proxy
-Restart=on-failure
-RestartSec=5
-NoNewPrivileges=yes
-ProtectSystem=strict
-PrivateTmp=yes
-AmbientCapabilities=CAP_NET_BIND_SERVICE
-CapabilityBoundingSet=CAP_NET_BIND_SERVICE
-ReadWritePaths=/opt/acme-proxy
-StandardOutput=journal
-StandardError=journal
-SyslogIdentifier=acme-proxy
-
-[Install]
-WantedBy=multi-user.target
-EOF
-
-sudo useradd -r -s /sbin/nologin acme-proxy
-sudo chown -R acme-proxy:acme-proxy /opt/acme-proxy
-sudo systemctl daemon-reload
-sudo systemctl enable acme-proxy
-```
+The service is **enabled but not started**. [Configure](configuration.md) `ca.json` file before starting.
---
@@ -140,16 +75,17 @@ make
The build produces a `step-ca` binary in the current directory. Copy it to your install location:
```sh
-sudo install -o root -g root -m 0755 step-ca /opt/acme-proxy/step-ca
+# configure `ca.json` file before starting step-ca
+./step-ca ca.json
```
-Then follow the [pre-built binary](#pre-built-binary) instructions to create the service unit.
+Use the [installer script](https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh) as a reference to complete the setup with systemd service unit, service account user, permissions etc.
---
## Install using Docker
-Before starting the container on a linux distro
+Run the following commands before starting the container on a linux host
```sh
mkdir -p /opt/acme-proxy/db
@@ -191,89 +127,6 @@ services:
---
-## Configuration
-
-All install methods use the same `ca.json` configuration format. The install script creates a template β five fields require customization before the service can start.
-
-### Minimal required configuration
-
-```json
-{
- "address": ":443",
- "dnsNames": ["acmeproxy.example.com"],
- "logger": {
- "format": "json"
- },
- "db": {
- "type": "bbolt",
- "dataSource": "/opt/acme-proxy/db/bbolt"
- },
- "authority": {
- "type": "externalcas",
- "config": {
- "ca_url": "https://acme.sectigo.com/v2/InCommonRSAOV",
- "account_email": "certadmin@example.com",
- "eab_kid": "your-eab-key-id",
- "eab_hmac_key": "your-eab-hmac-key",
- "certlifetime": 30,
- "metrics": {
- "enabled": true,
- "port": 9234,
- "dataSource": "/opt/acme-proxy/db/metrics"
- }
- },
- "provisioners": [
- {
- "type": "ACME",
- "name": "acme",
- "claims": {
- "enableSSHCA": false,
- "disableRenewal": false,
- "allowRenewalAfterExpiry": false,
- "disableSmallstepExtensions": true
- }
- }
- ],
- "backdate": "1m0s"
- },
- "tls": {
- "minVersion": 1.2,
- "maxVersion": 1.3,
- "renegotiation": false
- },
- "commonName": "acmeproxy.example.com"
-}
-```
-
-### Field reference
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `address` | Yes | Listen address. `:443` binds all interfaces on port 443. |
-| `dnsNames` | Yes | Hostname(s) that this proxy is reachable at. acme-proxy requests a TLS cert for itself using these names on first start. |
-| `authority.config.ca_url` | Yes | ACME directory URL of your upstream certificate authority. |
-| `authority.config.account_email` | Yes | Email registered with the upstream CA. |
-| `authority.config.eab_kid` | Yes | External Account Binding Key ID, obtained from your CA's account portal. |
-| `authority.config.eab_hmac_key` | Yes | External Account Binding HMAC key, obtained from your CA's account portal. |
-| `authority.config.certlifetime` | No | Request certificate with a max lifetime period if supported by upstream CA |
-| `authority.config.metrics.enabled` | No | Expose Prometheus metrics. Default: `true`. |
-| `authority.config.metrics.port` | No | Metrics port. Default: `9234`. |
-| `db.dataSource` | Yes | Path to the bbolt KV store directory. Must be writable by the service user. |
-| `commonName` | Yes | Common name for the proxy's own TLS certificate. Should match `dnsNames[0]`. |
-
-### Upstream CA URLs
-
-| CA | ACME URL |
-|----|----------|
-| Sectigo / InCommon RSA OV | `https://acme.sectigo.com/v2/InCommonRSAOV` |
-| ZeroSSL | `https://acme.zerossl.com/v2/DV90` |
-
->[!IMPORTANT]
->**Note**
-LetsEncrypt as a public certificate authority does not support ACME accounts via External Account Binding and hence it cannot be used as an upstream CA with acme-proxy.
-
----
-
## Starting the Service
### Systemd
@@ -331,3 +184,46 @@ curl -s https://acmeproxy.example.com/acme/acme/directory | jq .
```
A JSON object with `newNonce`, `newAccount`, `newOrder` keys confirms the ACME server is running and accepting requests.
+
+---
+
+## Verifying Release Binaries
+
+Each release binary is accompanied by a SHA256 checksum file and a [cosign](https://docs.sigstore.dev/cosign/overview/) signature bundle for supply-chain verification.
+
+### Checksum
+
+```sh
+VERSION=v1.0.0 # replace with the release version
+
+curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64"
+curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/SHA256SUMS"
+
+sha256sum --check --ignore-missing SHA256SUMS
+```
+
+### Signature
+
+Binaries are signed using [keyless signing](https://docs.sigstore.dev/cosign/signing/overview/) via GitHub Actions OIDC β no long-lived signing key exists. Verification requires the `.bundle` file published alongside each binary.
+
+```sh
+# Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/
+VERSION=v1.0.0 # replace with the release version
+
+curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64"
+curl -fsSLO "https://github.com/esnet/acme-proxy/releases/download/${VERSION}/step-ca_linux_amd64.bundle"
+
+cosign verify-blob \
+ --bundle step-ca_linux_amd64.bundle \
+ --certificate-identity "https://github.com/esnet/acme-proxy/.github/workflows/ci.yml@refs/tags/${VERSION}" \
+ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
+ step-ca_linux_amd64
+```
+
+A successful verification prints:
+
+```
+Verified OK
+```
+
+Substitute `amd64` with `arm64` for the ARM binary.
diff --git a/docs/content/quickstart.md b/docs/content/quickstart.md
index e554171..25a168c 100644
--- a/docs/content/quickstart.md
+++ b/docs/content/quickstart.md
@@ -38,34 +38,43 @@ curl -fsSL https://raw.githubusercontent.com/esnet/acme-proxy/main/install.sh |
## Step 2 β Configure
-Open the config file:
+`acme-proxy` supports two modes to get a certificate signed from an external CA.
+
+1. External Account Binding (EAB)
+2. DNS01-TXT
+
+Depending on your upstream CA, you may need to configure either one or both modes.
```sh
sudo vim /opt/acme-proxy/ca.json
```
-Set these five fields β everything else can stay at its default:
+Set the following fields first
| Field | Where to find it |
|-------|-----------------|
-| `dnsNames` | Your acme-proxy hostname, e.g. `["acmeproxy.example.com"]` |
+| `dnsNames` | Your acme-proxy hostname, e.g. `["acme-proxy.example.com"]` |
| `ca_url` | Your upstream CA's ACME directory URL (see table below) |
| `account_email` | Contact email registered with the upstream CA |
-| `eab_kid` | EAB Key ID from your CA's account portal |
-| `eab_hmac_key` | EAB HMAC key from your CA's account portal |
-**Common upstream CA URLs:**
+**Common ACME enabled CA URLs:**
| CA | URL |
|----|-----|
-| Sectigo / InCommon RSA OV | `https://acme.sectigo.com/v2/InCommonRSAOV` |
-| ZeroSSL | `https://acme.zerossl.com/v2/DV90` |
+| LetsEncrypt | `https://acme-v02.api.letsencrypt.org/directory` |
+| CertiNext | `https://acme-us.certinext.io/v1/directory` |
+| Sectigo OV | `https://acme.sectigo.com/v2/OV` |
-> LetsEncrypt does not support EAB and cannot be used as an upstream CA with acme-proxy.
-**Minimal working config:**
+### 1. External Account Binding (EAB)
-```json
+| Field | Where to find it |
+|-------|------------------|
+| `eab_kid` | EAB Key ID from your CA's account portal |
+| `eab_hmac_key` | EAB HMAC key from your CA's account portal |
+
+
+```json {hl_lines=["9-10"]}
{
"address": ":443",
"dnsNames": ["acme-proxy.example.com"],
@@ -82,6 +91,38 @@ Set these five fields β everything else can stay at its default:
}
```
+### 2. DNS01-TXT
+
+LetsEncrypt does not support EAB. To get certificates signed from LetsEncrypt you must use the `dns01_txt` mode
+
+| Field | Description |
+|-------------------------|------------------------------------------------------------------------------|
+| `dns01_txt.provider` | [Lego Provider](https://go-acme.github.io/lego/dns/index.html) CLI Flag name |
+| `dns01_txt.dns_servers` | Use your authoritative DNS server's addresses to avoid caching/TTL problems |
+| `dns01_txt.env_vars` | Environment variables specific to your Lego DNS Provider for authentication |
+
+
+```json {hl_lines=["9-15"]}
+{
+ "address": ":443",
+ "dnsNames": ["acme-proxy.example.com"],
+ "authority": {
+ "type": "externalcas",
+ "config": {
+ "ca_url": "https://acme-v02.api.letsencrypt.org/directory",
+ "account_email": "certadmin@example.com",
+ "dns01_txt": {
+ "provider": "lego-dns-provider-code",
+ "dns_servers": ["8.8.8,8", "1.1.1.1", "2606:4700:4700::1111"],
+ "env_vars": {
+ "LEGO_PROVIDER_API_KEY": "xxxxxxx",
+ }
+ }
+ },
+ "commonName": "acme-proxy.example.com"
+}
+```
+
---
## Step 3 β Start
@@ -103,23 +144,23 @@ The service is ready when logs show:
2025/07/15 22:12:25 Initializing ACME client...
2025/07/15 22:12:25 [INFO] acme: Registering account for admin@example.com
2025/07/15 22:12:26 ACME client initialized successfully
-2025/07/15 22:12:26 Processing certificate request for domains: [proxy.example.com]
-2025/07/15 22:12:26 Starting certificate request processing for domains: [proxy.example.com]
-2025/07/15 22:12:26 [INFO] [proxy.example.com] acme: Obtaining bundled SAN certificate given a CSR
-2025/07/15 22:12:27 [INFO] [proxy.example.com] AuthURL: https://acme.sectigo.com/v2/InCommonRSAOV/authz/sx4qvINAdWw2IjplmyH6kg
-2025/07/15 22:12:27 [INFO] [proxy.example.com] acme: authorization already valid; skipping challenge
-2025/07/15 22:12:27 [INFO] [proxy.example.com] acme: Validations succeeded; requesting certificates
+2025/07/15 22:12:26 Processing certificate request for domains: [acme-proxy.example.com]
+2025/07/15 22:12:26 Starting certificate request processing for domains: [acme-proxy.example.com]
+2025/07/15 22:12:26 [INFO] [acme-proxy.example.com] acme: Obtaining bundled SAN certificate given a CSR
+2025/07/15 22:12:27 [INFO] [acme-proxy.example.com] AuthURL: https://acme.sectigo.com/v2/InCommonRSAOV/authz/sx4qvINAdWw2IjplmyH6kg
+2025/07/15 22:12:27 [INFO] [acme-proxy.example.com] acme: authorization already valid; skipping challenge
+2025/07/15 22:12:27 [INFO] [acme-proxy.example.com] acme: Validations succeeded; requesting certificates
2025/07/15 22:12:27 [INFO] Wait for certificate [timeout: 30s, interval: 500ms]
-2025/07/15 22:12:33 [INFO] [proxy.example.com] Server responded with a certificate.
-2025/07/15 22:12:33 Successfully obtained certificate from InCommon for domains: [proxy.example.com]
+2025/07/15 22:12:33 [INFO] [acme-proxy.example.com] Server responded with a certificate.
+2025/07/15 22:12:33 Successfully obtained certificate from InCommon for domains: [acme-proxy.example.com]
2025/07/15 22:12:33 Starting Smallstep CA/0000000-dev (linux/amd64)
2025/07/15 22:12:33 Documentation: https://u.step.sm/docs/ca
2025/07/15 22:12:33 Community Discord: https://u.step.sm/discord
2025/07/15 22:12:33 Config file: ca.json
-2025/07/15 22:12:33 The primary server URL is https://acmeproxy.example.com:443
-2025/07/15 22:12:33 Root certificates are available at https://acmeproxy.example.com:443/roots.pem
+2025/07/15 22:12:33 The primary server URL is https://acme-proxy.example.com:443
+2025/07/15 22:12:33 Root certificates are available at https://acme-proxy.example.com:443/roots.pem
2025/07/15 22:12:33 X.509 Root Fingerprint: a6cf64dbb4c8d5fd19ce48896068db03b533a8d1336c6256a87d00cbb3def3ea
-2025/07/15 22:12:33 Serving HTTPS on proxy.example.com:443 ...
+2025/07/15 22:12:33 Serving HTTPS on acme-proxy.example.com:443 ...
```
---
@@ -127,37 +168,42 @@ The service is ready when logs show:
## Step 4 β Verify
```sh
-curl -s https://acmeproxy.example.com/acme/acme/directory | jq .
+curl -s https://acme-proxy.example.com/acme/acme/directory | jq .
```
Expected:
```json
{
- "newNonce": "https://acmeproxy.example.com/acme/acme/new-nonce",
- "newAccount": "https://acmeproxy.example.com/acme/acme/new-account",
- "newOrder": "https://acmeproxy.example.com/acme/acme/new-order",
- "revokeCert": "https://acmeproxy.example.com/acme/acme/revoke-cert",
- "keyChange": "https://acmeproxy.example.com/acme/acme/key-change"
+ "newNonce": "https://acme-proxy.example.com/acme/acme/new-nonce",
+ "newAccount": "https://acme-proxy.example.com/acme/acme/new-account",
+ "newOrder": "https://acme-proxy.example.com/acme/acme/new-order",
+ "revokeCert": "https://acme-proxy.example.com/acme/acme/revoke-cert",
+ "keyChange": "https://acme-proxy.example.com/acme/acme/key-change"
}
```
+Verify [connectivity requirements](index.md/#connectivity-requirements) have been met before proceeding to the next step.
+
---
-## Step 5 β Issue a Test Certificate
+## Step 5 β Issue a Certificate
-Install acme.sh if not already present:
+On a server running in your network, install `acme.sh` if not already present:
```sh
-sudo apt-get install -y acme.sh socat # Debian / Ubuntu
-sudo dnf install -y epel-release acme.sh socat # RHEL / Rocky
+# Debian / Ubuntu
+sudo apt-get install -y acme.sh socat
+
+# RHEL / Rocky / Alma
+sudo dnf install -y epel-release acme.sh socat
```
-Issue a certificate in standalone mode (temporarily binds port 80 for the HTTP-01 challenge):
+Issue a certificate in standalone mode (temporarily binds port 80 for the HTTP-01 challenge). `myserver.example.com` must have a valid A/AAAA record which `acme-proxy.example.com` should be able to resolve
```sh
acme.sh --issue \
- --server https://acmeproxy.example.com/acme/acme/directory \
+ --server https://acme-proxy.example.com/acme/acme/directory \
--domain myserver.example.com \
--standalone
```
@@ -169,11 +215,3 @@ openssl x509 \
-in ~/.acme.sh/myserver.example.com_ecc/myserver.example.com.cer \
-noout -issuer -dates
```
-
----
-
-## Next Steps
-
-- **Set up ACME clients system-wide with auto-renewal** β [Client Guide](client.md)
-- **Issue certificates for NGINX, Apache, Docker workloads** β [User Guide](user.md)
-- **Alternative install methods** (binary, source, Docker, full config reference) β [Install](install.md)
diff --git a/docs/content/user.md b/docs/content/user.md
index f151033..127a505 100644
--- a/docs/content/user.md
+++ b/docs/content/user.md
@@ -1,6 +1,6 @@
+++
title = 'User Guide'
-weight = 40
+weight = 50
BookToC = true
+++
diff --git a/docs/public/404.html b/docs/public/404.html
index 0d68af3..4f5c12f 100644
--- a/docs/public/404.html
+++ b/docs/public/404.html
@@ -1,6 +1,6 @@
-