A lightweight enterprise ACME Certificate Authority service with device attestation support. It provides just the HTTP handlers needed to implement ACME, it is intended to be integrated into nanomdm or another service of your choosing. Storage, signing, authorization, and logging are implemented as pluggable interfaces to integrate into a wide variety of environments.
import (
"github.com/brandonweeks/nanoca"
"github.com/brandonweeks/nanoca/authorizers/null"
"github.com/brandonweeks/nanoca/issuers/inprocess"
"github.com/brandonweeks/nanoca/signers/file"
"github.com/brandonweeks/nanoca/storage/badger"
)
signer, _ := file.LoadSigner("rootCA.key")
storage, _ := badger.New(badger.Options{InMemory: true})
ca, _ := nanoca.New(
inprocess.New(signer),
null.New(),
storage,
"https://localhost:8443",
nanoca.WithPrefix("/acme"),
)
defer ca.Close()
mux := http.NewServeMux()
mux.Handle("/", ca.Handler())cmd/mpc-server/ wraps nanoca as a standalone ACME server with Docker support.
The image bakes in a demo CA (deploy/demo-ca/), so it deploys with no CA
config. The demo CA is disposable and its key is public. Never use it for real
certificates.
To use your own CA, generate one and pass it in:
scripts/gen-ca.sh # writes PKCS#8 rootCA.key + rootCA.crt to secrets/, prints PEM for RenderThe key must be PKCS#8 (a PRIVATE KEY block). gen-ca.sh uses
openssl genpkey; the older openssl genrsa emits PKCS#1, which the file signer
rejects. Set the subject with CN, O, C, DAYS, BITS. On Render, paste
the printed PEM into CA_CERT_PEM and CA_KEY_PEM.
| Var | Default | Purpose |
|---|---|---|
PORT |
10000 |
Listen port |
BASE_URL |
(required) | Base URL in the ACME directory |
CA_CERT_PEM |
(unset) | Root CA cert as PEM |
CA_KEY_PEM |
(unset) | Root CA key as PEM |
CA_CERT |
/etc/secrets/rootCA.crt |
Root CA cert file |
CA_KEY |
/etc/secrets/rootCA.key |
Root CA key file (PKCS#8) |
BADGER_DIR |
/data/badger |
Storage path |
TRUST_FORWARDED_PROTO |
true |
Trust X-Forwarded-Proto from a proxy |
CA precedence: *_PEM > file paths > baked-in demo CA. The PEM parser accepts
PKCS#8, PKCS#1 RSA, and SEC1 EC keys.
Some reverse proxies (e.g., Cloudflare) terminate TLS and forward over plain HTTP, so
r.TLS is nil and nanoca's RFC 8555 URL check rejects every signed POST with
"HTTPS is required" (HTTP 400). The wrapper reads X-Forwarded-Proto and
restores the HTTPS state. Set TRUST_FORWARDED_PROTO=false only when the process
terminates TLS itself.
deploy/profiles/ holds two macOS profiles that run the attestation flow against
a nanoca ACME server. They carry no secrets and point at the public test CA.
Apply them through Fleet or another MDM.
| Profile | Payload | Purpose |
|---|---|---|
nanoca-root-ca-trust.mobileconfig |
com.apple.security.root |
Trusts the test root CA so issued certs validate |
nanoca-acme.mobileconfig |
com.apple.security.acme |
Requests an identity cert: hardware bound, Secure Enclave attestation, EC P-384 |
The ACME profile sets the client identifier and subject CN from
$FLEET_VAR_HOST_HARDWARE_SERIAL, a Fleet variable. With another MDM, use its
per-host serial variable or a static value. Update the ACME profile's server URL
to match your deployment and see deploy/profiles/README.md to verify issuance.
POC only. The server uses the null authorizer, so any attested device gets a
cert. It is not linked to Apple Business Manager tokens, so ABM membership is not
checked. The demo CA key is public. Use a real authorizer and a real CA for
production.