Home Certificate Authority with HSM & ACME

Blah blah.

Key risks: I forget how to use it. I repurpose it. Unauthorized access.

Root and manual use intermediate certs and keys stored on self-encrypting USB sticks, intermediate CA for ACME use in a HSM with step-ca (getting pkcs11 stuff to play nice on Alpine was… “fun”).

Conflict and Dread

PKI without OpenSSL is šŸ’–

copied all that along with instructions on how to use it to the USB sticks and wrote down instructions in a little notebook too. put all that in a safe since i shouldn’t need the root for a while. The ACME stuff is done by step-ca with a different intermediate key signed by root. That key was generated in (and never leaves) the HSM so even if someone hacks the online CA, they can sign their own certs while it’s running, but they can’t extract the private key and go sign their own certs where I can’t catch them or audit it.

{
        "subject": {{ toJson .Subject }},
        "keyUsage": ["certSign", "crlSign"],
        "basicConstraints": {
                "isCA": true,
                "maxPathLen": 0
        },
        {{/* All fields are optional, and all but "critical" can be a string or an array of strings */}}
        "nameConstraints": {
                "critical": true,
                "permittedDNSDomains": ["0xbad.coffee", "jade.wtf", "trashwitch.dev", "sigyn.systems", "pb3.pw"]
        }
}

step-ca as ACME CA with HSM on APU2

basically doing a slightly more involved version of https://smallstep.com/blog/build-a-tiny-ca-with-raspberry-pi-yubikey/ (and generated key in the HSM instead of needing to gen it locally and then move into a yubikey-as-hsm)

The other twist is that I’m using name constraints on the intermediate CA certs so they can’t issue certs for arbitrary domains. Did this because otherwise someone could, if they got access to online CA, use it generate certs to MITM my own traffic (assuming website wasn’t using cert pinning).

This is questionable because Apple (at some point, not sure if still true) didn’t support certs with that extension, so they’d treat them as invalid, but at the moment none of my apple stuff participates in the mTLS rodeo.

jda@tangent:~/Projects/tls-infra$ cat jade_intermediate_tmpl.json
{
        "subject": {{ toJson .Subject }},
        "keyUsage": ["certSign", "crlSign"],
        "basicConstraints": {
                "isCA": true,
                "maxPathLen": 0
        },
        {{/* All fields are optional, and all but "critical" can be a string or an array of strings */}}
        "nameConstraints": {
                "critical": true,
                "permittedDNSDomains": ["0xbad.coffee", "jade.wtf", "trashwitch.dev"]
        }
}

Alpine install and system prep

oh, and if you use an apu2, the hardware random number generate is broken (not sure if CPU bug or coreboot not initiating it correctly), so it is very not random (I think all zeros). The HSM uses it’s own hardware RNG for onboard ops and I flipped Linux to use the TPM’s RNG (which is not super fast, but fast enough) for normal things.

HSM Init

# HSM init
sc-hsm-tool --create-dkek-share dkek-share-1.pbe
openssl base64 -in dkek-share-1.pbe
sc-hsm-tool --initialize --so-pin ${SO_PIN} --pin ${PIN} --dkek-shares 1
sc-hsm-tool --import-dkek-share dkek-share-1.pbe

# make new cert in HSM
pkcs11-tool -l --pin ${PIN} --keypairgen --key-type EC:secp256r1 --label "Jade Intermediate HSM ACME"

# find key ID (the pkcs11:blahblah part) to use with certtool
p11tool --provider /usr/lib/opensc-pkcs11.so --list-privkeys --login

# generate cert signing request for cert/key in HSM
certtool --provider /usr/lib/opensc-pkcs11.so --generate-request --load-privkey "pkcs11:mode=[...]" --outfile cert.csr
sc-hsm-tool --create-dkek-share dkek-share-1.pbe
openssl base64 -in dkek-share-1.pbe
sc-hsm-tool --initialize --so-pin ${SO_PIN} --pin ${PIN} --dkek-shares 1
sc-hsm-tool --import-dkek-share dkek-share-1.pbe
pkcs11-tool -l --pin ${PIN} --keypairgen --key-type EC:secp256r1 --label "Jade Intermediate HSM ACME"
olca:~# pkcs11-tool -O
Using slot 0 with a present token (0x0)
Public Key Object; EC  EC_POINT 256 bits
  EC_POINT:   044104d8f5a77ebf48cbeb18db789fc597052136455f81d04332453de3a0babe463429cf1cfc76f9e68186159b4f4ad65755deafb3bd515386d81315dce8a6777bec56
  EC_PARAMS:  06082a8648ce3d030107
  label:      Jade Intermediate HSM ACME
  ID:         b0e6b6cdb60590ad9ac69c8691b32ac60ba59a15
  Usage:      verify
  Access:     none
Profile object 2159993760
  profile_id:          '4'
p11tool --provider /usr/lib/opensc-pkcs11.so --list-privkeys --login
certtool --provider /usr/lib/opensc-pkcs11.so --generate-request --load-privkey "pkcs11:mode=[...]" --outfile cert.csr

step-ca

Traefik with private ACME CA

Case sensitivity issues

version: "3"

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - host
      - proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_data:/data
    environment:
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA=true"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_CASERVER=https://olca.home.0xbad.coffee/acme/acme/directory"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_CERTIFICATESDURATION=24"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_EMAIL=jade@trashwitch.dev"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_TLSCHALLENGE=true"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_STORAGE=/data/acme_olca.json"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_HTTPCHALLENGE=true"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_HTTPCHALLENGE_ENTRYPOINT=web"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_KEYTYPE=EC256"
      - "TRAEFIK_CERTIFICATESRESOLVERS_OLCA_ACME_DNSCHALLENGE=false"
      - "TRAEFIK_PROVIDERS_FILE_DIRECTORY=/data/configurations/"
      - "TRAEFIK_ENTRYPOINTS_web=true"
      - "TRAEFIK_ENTRYPOINTS_web_ADDRESS=:80"
      - "TRAEFIK_ENTRYPOINTS_web_HTTP_REDIRECTIONS_ENTRYPOINT_TO=websecure"
      - "TRAEFIK_ENTRYPOINTS_websecure=true"
      - "TRAEFIK_ENTRYPOINTS_websecure_ADDRESS=:443"
      - "TRAEFIK_ENTRYPOINTS_websecure_HTTP_TLS=true"
      - "TRAEFIK_ENTRYPOINTS_websecure_HTTP_TLS_CERTRESOLVER=olca"
      - "TRAEFIK_ENTRYPOINTS_websecure_HTTP_TLS_DOMAINS=svc.home.0xbad.coffee"
      - "TRAEFIK_PROVIDERS_FILE_DEBUGLOGGENERATEDTEMPLATE=true"
      - "TRAEFIK_API=true"
      - "LEGO_CA_CERTIFICATES=/data/ahi_root.pem"
      - "TRAEFIK_PROVIDERS_DOCKER=true"
      - "TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false"
      #- "TRAEFIK_LOG_LEVEL=debug"
      #- "TRAEFIK_LOG=true"
      #- "TRAEFIK_LOG_FILEPATH=/data/system.log"
      #- "TRAEFIK_ACCESSLOG=true"
      #- "TRAEFIK_ACCESSLOG_FILEPATH=/data/access.log"
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.traefik-secure.entrypoints=websecure"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.svc.home.0xbad.coffee`)"
      #- "traefik.http.routers.traefik-secure.middlewares=user-auth@file"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      #- "traefik.http.routers.traefik.tls.certresolver=OLCA"
      - "traefik.http.routers.traefik.priority=1"

  portainer2:
    image: portainer/portainer-ce:latest
    container_name: portainer2
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - portainer_data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.portainer-secure.entrypoints=websecure"
      - "traefik.http.routers.portainer-secure.rule=Host(`portainer.svc.home.0xbad.coffee`)"
      - "traefik.http.routers.portainer-secure.service=portainer2"
      - "traefik.http.services.portainer2.loadbalancer.server.port=9000"
Want to keep reading? / go foward