
ACME Mini‑CA – Documentation
This repository contains a minimal‑viable ACME Certificate Authority written in Go.
It is not a full replacement for production CAs such as Boulder/PEBBLE, but it demonstrates every building‑block you need to issue X.509 certificates via RFC 8555:
- ACME endpoints (
newAccount
,newOrder
,challenge
,finalize
,certificate
,revokeCert
) - Challenge types:
http‑01
(default) anddns‑01
(optional) - JWS request verification – supports both JWK and KID modes
- OCSP responder and CRL generation (optional)
- MySQL persistence for all entities (accounts, orders, nonces, certs …)
- Pluggable DNS provider API to publish/remove
_acme‑challenge.
TXT records
1 Architecture
┌─────────────┐ HTTP ┌──────────────┐
│ ACME Client│ ──────────────►│ ACME Server │
└─────────────┘ │ (this repo) │
├─── MySQL │
├─── OCSP / CRL │
└──────────────┘
▲
│ REST
┌───────┴────────┐
│ DNS Provider │ (optional)
└────────────────┘
The server may sit behind a reverse‑proxy for TLS termination; however http‑01
challenges are exposed directly by the embedded autocert.Manager
.
2 Build & Run
# clone & build
$ go build -o acme-ca .
# set mandatory secrets
export CA_CERT_PATH="./ca_cert.pem"
export CA_KEY_PATH="./ca_key.pem"
# run with sensible defaults
$ ./acme-ca
2.1 Environment variables
Variable | Default | Description |
---|---|---|
PORT |
8080 |
HTTP listener port |
MYSQL_DSN |
root:root@tcp(localhost:3306)/acme?parseTime=true&multiStatements=true |
DSN in Go SQL format |
ACME_ALLOWED_DOMAIN |
example.com |
Hostname whitelist for autocert HTTP handler |
DNS01_ENABLED |
false |
When true the server issues additional dns‑01 challenges |
DNS_PROVIDER_URL |
(empty) | Base URL of your DNS provider’s TXT API |
DNS_PROVIDER_TOKEN |
(empty) | Bearer token used in Authorization header |
OCSP_ENABLED |
false |
Expose GET /ocsp/{serial} endpoint |
CRL_ENABLED |
false |
Expose GET /crl endpoint |
JWS_VERIFY_ENABLED |
true |
Toggle signature verification (disable for local dev) |
Note
All optional modules can be enabled/disabled independently.
2.2 Database schema
init()
automatically executes the following DDL (simplified):
nonces(id PK, created)
accounts(id PK, jwk JSON)
orders(id PK, payload JSON, status, finalize_url)
authzs(id PK, order_id FK, identifier, status, payload JSON)
challenges(id PK, authz_id FK, type, token, status, payload JSON)
certs(id PK, serial, der LONGBLOB, revoked_at)
Add indices as required (e.g. serial
, token
).
3 ACME API surface
Method / Path | Purpose |
---|---|
HEAD /acme/new-nonce |
Issue fresh nonce (always) |
POST /acme/new-account |
Register account – payload must be JWS‑signed |
POST /acme/new-order |
Create order → returns authz /finalize URLs |
GET /acme/authz/{id} |
Poll authorization status |
POST /acme/challenge/{id} |
Client signals that challenge token is ready |
POST /acme/finalize/{order} |
Upload CSR (base64url DER) |
GET /acme/certificate/{id} |
Retrieve PEM certificate |
POST /acme/revoke-cert |
Revoke certificate (base64url DER) |
3.1 Challenge flow
- Server → Client
newOrder
response contains anauthz
URL. - Client → Server
GET /acme/authz/{id}
to fetch challenge details. - Client publishes token:
- http‑01:
http://<DOMAIN>/.well-known/acme-challenge/{token}
= key‑auth - dns‑01: TXT
_acme-challenge.<DOMAIN>
= base64url(sha256(token)) - When
DNS01_ENABLED=true
the server automatically calls DNS Provider API.
- http‑01:
- Client → Server
POST /acme/challenge/{id}
signals readiness. - Server validates (dns‑01 simply trusts provider) and sets valid.
- Client uploads CSR via
finalize
, server issues certificate.
3.2 DNS Provider contract
POST <DNS_PROVIDER_URL>
DELETE <DNS_PROVIDER_URL>
Authorization: Bearer <DNS_PROVIDER_TOKEN>
Content-Type: application/json
{
"fqdn": "_acme-challenge.example.com",
"token": "<txt contents>"
}
Return 200
to acknowledge; all other codes are logged only (non‑blocking).
4 OCSP & CRL
- OCSP ↠
GET /ocsp/{serial}
– returns DERapplication/ocsp-response
.
Serial numbers are looked‑up incerts.revoked_at
. Status =Revoked
if timestamp present. - CRL ↠
GET /crl
– regenerates PEM‑CRL on each request.
Enable with the respective ENV flags.
5 JWS verification
When JWS_VERIFY_ENABLED=true
every ACME POST body must be a JOSE JWS JSON serialization.
- New‑Account: payload includes a
jwk
→ key is saved under newaccount.id
. - Subsequent requests: use
kid
header pointing toaccount
URL. - Nonce‑reuse or invalid signatures return
4xx
.
6 Security & Hardening checklist
Area | Recommendation |
---|---|
CA key | Store in HSM / KMS, never plaintext. |
TLS | Run behind reverse‑proxy (Nginx, Traefik) or enable tls.Config on http.Server . |
Rate‑limits | Use middleware (e.g. golang.org/x/time/rate ). |
Audit | Stream structured logs to SIEM; DB triggers for cert changes. |
Backup | Full MySQL dumps; optionally S3 for issued certs. |
Monitoring | Export Prometheus metrics: issued / revoked / OCSP hits. |
7 Testing
A quick smoke‑test with Pebble (Let’s Encrypt dev CA):
# run Pebble
$ docker run --rm -p 14000:14000 letsencrypt/pebble -config /test/config.json
# point Pebble at your Mini‑CA
PEBBLE\_ALTERNATE\_ROOTS=
export PEBBLE\_V2\_CA="http://localhost:8080/acme"
# use `lego` client
$ lego -m you@example.com --pem --server http://localhost:8080/acme --accept-tos -d sub.stadt-hilden.de run
8 Future improvements
- OCSP stapling & cache headers
- PostgreSQL + GORM migrations
- Automatic key‑rollover for CA
- ACME‑v3 STAR certificates (short‑term re‑use)
- Proper background validator for
dns‑01
viadig TXT
lookup
© 2025 — Released under the Apache‑2.0 License