Quickstart · Windows + YubiKey
The honest Windows path.
Two facts before any command, because they decide everything on this page.
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 needed2 · 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: AttachedThe 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
pkcs11custody only. The token has no symmetric-key store —pkcs11-wrappedis not a mode it can offer at all. The private key never leaves the token; every signature is oneC_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 (
9aauth,9csignature,9dkey management,9ecard 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'
SCardSvrand Linux'spcscdboth claim the reader; whichever holds it, the other side's PKCS#11 module getsCKR_TOKEN_NOT_PRESENT. - A touch policy is unattended-signing poison. A key
provisioned
--touch-policy ALWAYSblocks insideC_Signfor 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:presenceRequiredon the module document makes keysvc refuse unattended callers rather than time out confusingly.