Built for the KeycloakCon Japan talk "Binding human identity to artifact signatures."
A container image gets signed with a certificate that is cryptographically bound to a real Keycloak identity — not a GitHub Actions issuer, not Google, not a static keypair sitting in a secrets manager. Your own IdP is the root of trust for "who signed this." The signed app deploys and shows that identity live on its own page — signer, issuer, sign time, pod name — so the payoff is visible, not just a terminal message.
Flow:
Developer --login--> Keycloak --ID token--> Fulcio --short-lived cert--> Cosign (signs image)
|
v
ttl.sh (image + signature)
|
v
cosign verify (checks issuer = Keycloak)
|
v
App deploys, shows signer identity live
This is proven working end-to-end.
app/templates/index.html.template isn't a static page — nginx's built-in envsubst
templating renders it at container start using real env vars set by
05-deploy-app.sh (SIGNER_IDENTITY, SIGNER_ISSUER, SIGNED_TIME, and POD_NAME
via the Kubernetes downward API). What's on screen when you open the app is the actual
identity that signed the image, not a hardcoded placeholder — worth calling out on
camera.
The talk's actual point is the Keycloak → Fulcio identity binding. Rekor (transparency log) and CTLog (certificate transparency) both need a Trillian + MySQL backend, and none of them are required to demonstrate that binding — they add a public audit trail, which is valuable in production but orthogonal to what this demo needs to prove. Dropping them entirely keeps the setup light and fast.
Signing uses --new-bundle-format with --signing-config (Fulcio only, no Rekor
entry) and --trusted-root (Fulcio's current root cert, fetched fresh every run —
see below for why). Verification runs offline with --insecure-ignore-tlog and
--insecure-ignore-sct — both flags exist specifically for setups like this one that
intentionally don't run a transparency log.
Fulcio's fileca mode generates a brand-new, ephemeral self-signed root CA every time
its pod restarts — it is not persisted. Caching a copy from setup time and reusing it
later breaks the moment Fulcio restarts for any reason. 03-build-sign.sh fetches it
live from Fulcio's own /api/v1/rootCert endpoint immediately before every sign, so
it's always current.
The Bitnami Helm chart for Keycloak pulls in a Postgres subchart pinned to a specific
versioned image tag. Bitnami restructured its container catalog in 2025 — most
versioned tags were pulled from the free docker.io/bitnami repo, so charts pinned
to an old tag now fail with ImagePullBackOff / "not found." This demo runs
Keycloak's own official image (quay.io/keycloak/keycloak) in dev mode with its
built-in in-memory database instead — no Postgres, no third-party catalog dependency.
The scaffold umbrella chart hardcodes Fulcio's trusted OIDC issuer to a Kubernetes
service-account issuer, for its own internal CI/e2e testing — there's no values
override to point it at an external IdP like Keycloak. The standalone sigstore/fulcio
chart exposes the real config path (config.contents.OIDCIssuers) needed for this.
ttl.sh is a free, public, ephemeral container registry — no account, no auth, images
just expire after the TTL you give them. If your cluster's DNS ever breaks (a kind +
Ubuntu + systemd-resolved issue, unrelated to this demo — see CoreDNS's Corefile
forward directive if ttl.sh stops resolving), that's a cluster networking problem
to fix directly rather than a reason to avoid ttl.sh.
| File | Purpose |
|---|---|
01-setup-cluster.sh |
kind cluster + Keycloak (official image, dev mode) + Fulcio (standalone chart, trusts Keycloak) |
keycloak-realm-sigstore-demo.json |
Realm export — client, user (developer), mappers, pre-configured for import |
fulcio-values.yaml |
Helm values for the standalone Fulcio chart — this is where Fulcio trusts Keycloak |
app/ |
The demo web app — nginx + a live-templated page showing the real signer identity |
03-build-sign.sh |
Builds the app, pushes both tags to ttl.sh, gets a Keycloak token, signs the :signed tag |
04-verify.sh |
Verifies the signature offline against Fulcio's current root — the demo's strongest moment |
05-deploy-app.sh |
Deploys the signed app with real signing data injected into the live page |
06-destroy.sh |
Tears everything down — kind cluster, temp files |
DEMO_SCRIPT.md |
Recording runbook — talking points, timing, retake notes |
kind,kubectl,helm,cosign(v2.x),jq,curl, Docker running locally- Internet connection (to pull charts/images and push to ttl.sh)
- ~10 min lead time before recording to bring the cluster up
./01-setup-cluster.sh
./03-build-sign.sh
./04-verify.sh
./05-deploy-app.sh
# open http://localhost:8081 to see the live signer-identity page
echo '127.0.0.1 keycloak.keycloak.svc.cluster.local' | sudo tee -a /etc/hosts -> Access Keycloak UI on browser
# when you're done:
./06-destroy.sh