Quickstart · Windows + YubiKey

The honest Windows path.

Two facts before any command, because they decide everything on this page.

Request a download link

Fact 1 — there is no Windows service today

The Windows all-in-one shape (embedded PostgreSQL under one Windows Service) is designed, not built: no Windows Service, no installer. Roadmap On Windows you run goca in containers — which is what this page does.

Fact 2 — the CNG device half is Windows-unverified

goca has a Windows CNG/KSP bridge (how Windows exposes its machine TPM), but the shipped Linux image never compiles or runs it — every non-Windows build links a stub that honestly returns "unavailable". The Windows-host lane that would verify it has not run green, so this release does not claim a cng key can sign. Roadmap The verified hardware path on this machine class is PKCS#11 — which is exactly what the YubiKey half of this page uses.

Path A — Docker Desktop (the stack)

Install Docker Desktop with the WSL2 backend, then follow the Compose quickstart unchanged — same commands, same fifteen minutes, same five green checks. One Windows-specific note: for the "host reboot restarts everything automatically" behavior, Docker Desktop must itself start with the machine (Settings → General → "Start Docker Desktop when you sign in").

Path A gives you software custody. If that is your evaluation, stop here — the rest of this page is about putting a real hardware token under the keys.

Path B — the YubiKey, into WSL2

The PKCS#11 bridge is cgo, and Docker Desktop's own docker-desktop distro is not a place to build or attach hardware. The path that works — as actually run and verified on real hardware (2026-08-01, YubiKey 5, firmware 5.43) — is a real WSL2 distro plus USB passthrough. Sequence below, gotchas included.

1 · A real Ubuntu distro

# PowerShell. --no-launch matters: without it, first run drops into an
# interactive username/password prompt that a non-interactive shell cannot answer.
wsl --install -d Ubuntu --no-launch
wsl -d Ubuntu -u root -- bash -c "id -un"     # root, no user account needed

2 · USB passthrough, Windows side

# The MSI needs admin (winget self-elevates).
winget install --exact --id dorssel.usbipd-win
usbipd list                                   # find your BUSID (VID:PID 1050:xxxx)
usbipd bind --busid <busid>                   # ADMIN REQUIRED. One-time; persists.

bind is the only step that needs elevation, and it is one-time — the share survives reboots. attach does not need admin.

3 · Attach — with the distro running

# The distro must be RUNNING before attach, or you get
# "The selected WSL distribution is not running". WSL shuts idle distros
# down, so hold one open:
Start-Process wsl -ArgumentList '-d','Ubuntu','-u','root','--','sleep','1800'
usbipd attach --wsl Ubuntu --busid <busid>    # → STATE: Attached

The YubiKey disappears from Windows

While attached, the YubiKey is gone from the Windows side — including FIDO2 for browser logins. usbipd detach --busid <busid> gives it back.

4 · Inside Ubuntu

# Note ykcs11 is its OWN package: installing yubico-piv-tool
# does NOT give you libykcs11.so.
apt-get install -y pcscd pcsc-tools opensc yubico-piv-tool ykcs11 build-essential
pcscd --foreground --auto-exit &              # no systemd in WSL by default
opensc-tool --list-readers                    # → Yubico YubiKey OTP+FIDO+CCID
pkcs11-tool --module /usr/lib/x86_64-linux-gnu/libykcs11.so -T
pkcs11-tool --module /usr/lib/x86_64-linux-gnu/libykcs11.so -M

Two apt traps. usbip is not a package on recent Ubuntu and is not needed (usbipd-win 4+ drives the attach itself). And apt-get install aborts the entire transaction on one unavailable package — a single bad name can install nothing while a script reports success. Install one package per invocation, or check each name first.

Do not chase USB into Docker Desktop

Passing the token into a container on Docker Desktop means attaching to the docker-desktop distro and bind-mounting /dev/bus/usb. It is fragile, and the Ubuntu distro above is needed anyway for the cgo toolchain. Run the hardware work where the hardware is.

5 · Point goca at it

From here the standard module-mount pattern applies: the vendor .so goes in the module directory mounted read-only into the container running keysvc (GOCA_HSM_MODULE_DIR), the PIN goes in a 0400 file owned by uid 65532 in the secret directory (GOCA_HSM_SECRET_DIR) — never in an environment variable — and a Pkcs11Module document names the module by its container path with a pinRef: file:… reference. A smartcard is exclusive-access, so the process loading libykcs11.so must be able to reach the running pcscd. Do not create a CA onto the module until its status is ready and the algorithm you want appears — a configuration document alone is never evidence that a token is reachable.

What a YubiKey honestly gives you

A YubiKey 5 is not a YubiHSM 2. Its PIV applet gives you a PKCS#11 token via libykcs11 with:

  • Strict pkcs11 custody only. The token has no symmetric-key store — pkcs11-wrapped is not a mode it can offer at all. The private key never leaves the token; every signature is one C_Sign, at the token's rate. The smartcard tier validates only the stronger custody mode.
  • No PQC. No ML-DSA, no SLH-DSA, no composite — goca's post-quantum algorithms cannot be exercised here at all.
  • ~4 usable key slots (9a auth, 9c signature, 9d key management, 9e card auth) plus 20 "retired" slots. Enough for a CA hierarchy in a test, nowhere near an issuing CA's working set.
  • Genuinely non-extractable keys in hardware, real signing latency, and a smartcard's exclusive-access model — the combination that makes it worth doing at all. RSA 1024/2048, ECDSA P-256/P-384 (including combined ECDSA-SHA256/384/512), plus Ed25519 and X25519 keygen, as measured on firmware 5.43 — ask the token, not the changelog.
  • Key generation happens out of band. Generation needs the security-officer role with the management key, which goca deliberately never presents (a signing oracle that could log in as the SO could re-initialize the token holding the CA key). Provision with ykman piv keys generate … and adopt the existing key — a path the product supports.

Verified on real hardware: YubiKey 5, firmware 5.43, 2026-08-01 — goca's own probe runs against it green.

Hazards — read before plugging it in

  • Three wrong PINs blocks the applet. Three wrong PUKs after that and the PIV applet can only be reset, which destroys every PIV key on it. An automated loop with a stale PIN will do this in under a second. Pin the PIN in one place; never let a retry loop near it.
  • A PIV reset is destructive to whatever else uses PIV — certificate-based Windows logon, SSH via PIV, S/MIME. FIDO2, OTP and OpenPGP live in separate applets and are unaffected — but confirm before resetting. Use a spare key if you have one.
  • Exclusive access. Windows' SCardSvr and Linux's pcscd both claim the reader; whichever holds it, the other side's PKCS#11 module gets CKR_TOKEN_NOT_PRESENT.
  • A touch policy is unattended-signing poison. A key provisioned --touch-policy ALWAYS blocks inside C_Sign for about 15 seconds and then fails — fine for an attended root, fatal for an issuing CA or scheduled CRL generation. goca says so up front: presenceRequired on the module document makes keysvc refuse unattended callers rather than time out confusingly.

Get the bundle

Free — download on request.

Request a download link