Skip to content

Instantly share code, notes, and snippets.

@maelvls
Last active December 4, 2025 19:14
Show Gist options
  • Select an option

  • Save maelvls/9dac51852e5208f6d8b16fd02b1e83fb to your computer and use it in GitHub Desktop.

Select an option

Save maelvls/9dac51852e5208f6d8b16fd02b1e83fb to your computer and use it in GitHub Desktop.
A fake acme server that doesn't ask you to solve challenges
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"log"
"math/big"
"net/http"
"strings"
"sync"
"time"
)
// --------------------------------------------------------------------
// Types
// --------------------------------------------------------------------
type Order struct {
ID string `json:"id"`
Status string `json:"status"`
Authorizations []string `json:"authorizations"`
Finalize string `json:"finalize"`
Certificate string `json:"certificate,omitempty"` // URL
cert string // internal only
}
type Account struct {
ID string `json:"id"`
Status string `json:"status"`
Contact []string `json:"contact,omitempty"`
Orders string `json:"orders,omitempty"`
}
// Minimal request shape for new-account (we ignore most fields)
type newAccountRequest struct {
Contact []string `json:"contact"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
}
// --------------------------------------------------------------------
// Globals (super simple, single-order / single-authz server)
// --------------------------------------------------------------------
var (
orderID = "1234"
authzID = "5678"
certID = "abcd"
accountID = "acct1"
mu sync.Mutex
orders = map[string]*Order{}
accounts = map[string]*Account{}
certs = map[string]string{} // certID -> PEM string
baseURL string
caCert *x509.Certificate
caKey *ecdsa.PrivateKey
)
// --------------------------------------------------------------------
// main
// --------------------------------------------------------------------
func main() {
port := flag.String("port", "4000", "Port to listen on")
baseurlFlag := flag.String("baseurl", "", "Base URL for ACME server (e.g. http://localhost:4000). If not set, defaults to http://localhost:<port>")
flag.Parse()
if *baseurlFlag != "" {
baseURL = *baseurlFlag
} else {
baseURL = fmt.Sprintf("http://localhost:%s", *port)
}
var err error
caCert, caKey, err = selfSignedCA()
if err != nil {
log.Fatalf("Error creating self-signed CA: %v", err)
}
http.HandleFunc("/directory", handleDirectory)
http.HandleFunc("/acme/new-nonce", handleNewNonce)
http.HandleFunc("/acme/new-account", handleNewAccount)
http.HandleFunc("/acme/new-order", handleNewOrder)
http.HandleFunc("/acme/authz/", handleAuthz)
http.HandleFunc("/acme/order/", handleOrder)
http.HandleFunc("/acme/cert/", handleCert)
addr := ":" + *port
log.Printf("Fake ACME server listening on %s\n", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
// --------------------------------------------------------------------
// /directory (ACME directory)
// --------------------------------------------------------------------
//
// GET /directory
//
// Returns the URLs for newNonce, newAccount, newOrder.
func handleDirectory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "GET only", http.StatusMethodNotAllowed)
return
}
resp := map[string]any{
"newNonce": baseURL + "/acme/new-nonce",
"newAccount": baseURL + "/acme/new-account",
"newOrder": baseURL + "/acme/new-order",
"meta": map[string]any{
"termsOfService": baseURL + "/terms",
},
}
_ = json.NewEncoder(w).Encode(resp)
}
// --------------------------------------------------------------------
// /acme/new-nonce (Replay-Nonce endpoint)
// --------------------------------------------------------------------
//
// HEAD /acme/new-nonce
//
// Returns a Replay-Nonce header. We don't actually validate it.
func handleNewNonce(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead && r.Method != http.MethodGet {
http.Error(w, "HEAD or GET only", http.StatusMethodNotAllowed)
return
}
nonce := randomNonce()
w.Header().Set("Replay-Nonce", nonce)
w.WriteHeader(http.StatusOK)
}
// --------------------------------------------------------------------
// /acme/new-account (Create fake account)
// --------------------------------------------------------------------
//
// POST /acme/new-account
//
// Body: plain JSON, not JWS. In real ACME it's a JWS-wrapped payload.
func handleNewAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
var req newAccountRequest
_ = json.NewDecoder(r.Body).Decode(&req)
mu.Lock()
defer mu.Unlock()
// Single account, overwrite if called again.
acct := &Account{
ID: accountID,
Status: "valid",
Contact: req.Contact,
Orders: baseURL + "/acme/order/" + orderID,
}
accounts[accountID] = acct
w.Header().Set("Location", baseURL+"/acme/account/"+accountID)
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(acct)
}
// --------------------------------------------------------------------
// /acme/new-order (Create fake order)
// --------------------------------------------------------------------
//
// POST /acme/new-order
//
// Ignores body, always returns a single pending order with one authz.
func handleNewOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
mu.Lock()
defer mu.Unlock()
o := &Order{
ID: orderID,
Status: "pending",
Authorizations: []string{
baseURL + "/acme/authz/" + authzID,
},
Finalize: baseURL + "/acme/order/" + orderID + "/finalize",
}
orders[orderID] = o
w.Header().Set("Location", baseURL+"/acme/order/"+orderID)
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(o)
}
// --------------------------------------------------------------------
// /acme/authz/<id> (Authorization always valid, no challenges)
// --------------------------------------------------------------------
//
// GET /acme/authz/5678
//
// Returns Example 1: status=valid, challenges: [].
func handleAuthz(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(w, "GET or POST only", http.StatusMethodNotAllowed)
return
}
id := r.URL.Path[len("/acme/authz/"):]
if id != authzID {
http.NotFound(w, r)
return
}
resp := map[string]any{
"identifier": map[string]string{
"type": "dns",
"value": "example.com",
},
"status": "valid",
"expires": "2030-01-01T00:00:00Z",
"challenges": []any{},
}
_ = json.NewEncoder(w).Encode(resp)
}
// --------------------------------------------------------------------
// /acme/order/<id>[/finalize]
// --------------------------------------------------------------------
//
// GET /acme/order/1234 -> returns the order, already valid on purpose
// POST /acme/order/1234/finalize -> sets certificate URL
func handleOrder(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path[len("/acme/order/"):]
if path == "" {
http.NotFound(w, r)
return
}
// finalize endpoint
if path == orderID+"/finalize" {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
mu.Lock()
defer mu.Unlock()
o := orders[orderID]
if o == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("finalize: order " + orderID + " not found"))
return
}
var hdr struct {
Payload string `json:"payload"`
}
_ = json.NewDecoder(r.Body).Decode(&hdr)
if hdr.Payload == "" {
http.Error(w, "finalize: missing protected header", http.StatusBadRequest)
return
}
payload, err := base64.RawURLEncoding.DecodeString(hdr.Payload)
if err != nil {
http.Error(w, "finalize: invalid base64 payload: "+err.Error(), http.StatusBadRequest)
return
}
var body struct {
Csr string `json:"csr"`
}
_ = json.NewDecoder(strings.NewReader(string(payload))).Decode(&body)
if body.Csr == "" {
http.Error(w, "finalize: missing csr in payload", http.StatusBadRequest)
return
}
decoded, err := base64.RawURLEncoding.DecodeString(body.Csr)
if err != nil {
http.Error(w, "finalize: invalid base64 csr: "+err.Error(), http.StatusBadRequest)
return
}
csr, err := x509.ParseCertificateRequest(decoded)
if err != nil {
http.Error(w, "finalize: invalid csr: "+err.Error(), http.StatusBadRequest)
return
}
// Sign using the caCert and caKey.
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: csr.Subject,
Issuer: caCert.Subject,
SignatureAlgorithm: x509.ECDSAWithSHA256,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: csr.DNSNames,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, csr.PublicKey, caKey)
if err != nil {
http.Error(w, fmt.Sprintf("error creating self-signed certificate: %v", err), http.StatusInternalServerError)
return
}
certs[certID] = fmt.Sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n",
base64.StdEncoding.EncodeToString(certDER))
o.Status = "valid"
o.Certificate = baseURL + "/acme/cert/" + certID
_ = json.NewEncoder(w).Encode(o)
return
}
// GET /acme/order/1234
if path == orderID {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(w, "GET or POST only", http.StatusMethodNotAllowed)
return
}
mu.Lock()
defer mu.Unlock()
o := orders[orderID]
if o == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("order " + orderID + " not found"))
return
}
// No challenges, let's pretend the CA has some out-of-band way to
// authorize the domain.
if o.Status == "pending" {
o.Status = "ready"
}
_ = json.NewEncoder(w).Encode(o)
return
}
http.NotFound(w, r)
}
// --------------------------------------------------------------------
// /acme/cert/<id> (Return fake PEM certificate)
// --------------------------------------------------------------------
func handleCert(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(w, "GET and POST-as-GET only", http.StatusMethodNotAllowed)
return
}
id := r.URL.Path[len("/acme/cert/"):]
if id != certID {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/pem-certificate-chain")
mu.Lock()
defer mu.Unlock()
c := certs[id]
if c == "" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("cert " + id + " not found"))
return
}
fmt.Fprint(w, c)
}
// --------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------
func randomNonce() string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 24)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
panic(err)
}
b[i] = letters[n.Int64()]
}
return string(b)
}
func selfSignedCA() (*x509.Certificate, *ecdsa.PrivateKey, error) {
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Fake ACME CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
}
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("error generating private key: %v", err)
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, fmt.Errorf("error creating self-signed certificate: %v", err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, fmt.Errorf("error parsing self-signed certificate: %v", err)
}
return cert, priv, nil
}
@maelvls
Copy link
Author

maelvls commented Dec 4, 2025

To test this out:

mkdir -p /tmp/fakeacme
cd /tmp/fakeacme
curl -LO https://gist.githubusercontent.com/maelvls/9dac51852e5208f6d8b16fd02b1e83fb/raw/c0ff1cd9c9f08e6acee754332a2485a1afabe8b6/main.go
go run main.go

clone the cert-manager project and run:

helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.19.0 \
  --set crds.enabled=true --set crds.keep=false
kubectl scale -n cert-manager deploy cert-manager --replicas 0
go run ./cmd/controller --v=8 --cluster-resource-namespace=cert-manager --leader-election-namespace=kube-system --acme-http01-solver-image=quay.io/jetstack/cert-manager-acmesolver:v1.19.0 --max-concurrent-challenges=60 --kubeconfig ~/.kube/config 2>&1
kubectl apply -f- <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: fake-acme
  namespace: default
spec:
  acme:
    server: http://localhost:4000/directory
    privateKeySecretRef:
      name: fake-acme-account-key
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: fake-cert
  namespace: default
spec:
  secretName: fake-cert-tls
  issuerRef:
    name: fake-acme
    kind: Issuer
  commonName: example.com
  dnsNames:
    - example.com
EOF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment