VDI console access via Kerbside signed tokens¶
Prompt¶
Before responding to questions or discussion points in this
document, explore the shakenfist codebase thoroughly. Read
relevant source files, understand existing patterns (the REST
API decorator stack in shakenfist/external_api/base.py, JWT
issuance in shakenfist/util/access_tokens.py, the
cluster_config table and load_cluster_config() in
shakenfist/config.py and shakenfist/mariadb.py, the
eventlog audit pattern, and the deployer PKI roles under
deploy/ansible/roles/pki_internal_ca/), and ground your
answers in what the code actually does today. Do not
speculate about the codebase when you could read it instead.
This is a cross-repository plan. The affected repositories and their working branches are:
| Repository | Branch | Role |
|---|---|---|
| shakenfist/shakenfist | vdi-console-tokens |
Token minting, signing key, pubkey publication |
| shakenfist/client-python | vdi-console-tokens-client |
New API client methods, CLI surface, viewer launch |
| shakenfist/kerbside | sf-vdi-tokens |
Token exchange endpoint, scrape changes |
| shakenfist/ryll | vdi-console-tokens |
pip-installable viewer so the CLI can launch a session |
The master plan (this document) lives in shakenfist's
docs/plans/. Phase plan files live in the repository whose
code they change, so that each plan travels with the PR that
implements it. Kerbside's docs/plans/index.md carries a
cross-reference entry pointing back here.
For the kerbside side, consult kerbside's .claude/CLAUDE.md
and docs/proxy-architecture.md. Key kerbside references:
kerbside/api.py (the NovaToken resource is the pattern the
new exchange endpoint mirrors), kerbside/consoletoken.py,
kerbside/sources/shakenfist.py, and kerbside/main.py (the
maintenance-loop reapers).
When we get to detailed planning, I prefer a separate plan
file per detailed phase, named with -phase-NN-descriptive
appended, in the repository the phase changes.
I prefer one commit per logical change, and at minimum one commit per phase. Each commit should be self-contained: it should build, pass tests, and have a clear commit message explaining what changed and why.
Situation¶
Kerbside is a SPICE VDI protocol proxy that provides remote console access to VMs across Shaken Fist, OpenStack, and oVirt clouds. Ironically, Shaken Fist itself now has the worst Kerbside integration of the three:
- OpenStack (Nova 2025.1+ with spice-direct consoles) has
a per-instance authorisation story: when a user requests a
console, Nova returns a URL pointing at Kerbside with an
authentication token. Kerbside validates that token against
Nova's
/os-console-auth-tokens/API and only then issues its own session credential. Authorisation is enforced by the cloud that knows who owns the instance. - Shaken Fist is merely scraped: kerbside's
ShakenFistSourcepolls one configured namespace once a minute (kerbside/sources/shakenfist.py) and lists the consoles in kerbside's admin interface. There is no per-instance access control — anyone in kerbside's Keystone access group can fetch a.vvfor any scraped console. - Shaken Fist does already have the authorisation primitive:
InstanceVDIConsoleHelperEndpoint(shakenfist/external_api/instance.py:1442) hands out a.vvfile gated by@requires_instance_ownership(external_api/base.py:328) — but that.vvpoints the client directly at the hypervisor, bypassing Kerbside (and therefore bypassing TLS termination at the proxy, the SPICE protocol firewall, session auditing, and session termination).
Because we control both codebases, Shaken Fist can do
something Nova cannot: sign the console grant asymmetrically
so that Kerbside can validate it offline, with no
callback to the Shaken Fist API on console open. Nova's model
requires Kerbside to consult Nova at .vv-fetch time; a
signed-token model has no such availability coupling.
Relevant existing machinery, confirmed by code inspection:
- SF's REST API JWTs are HS256 via
flask_jwt_extendedwith a single cluster-wideAUTH_SECRET_SEED(external_api/app.py:59,config.py:148). The proxy token needs a different, asymmetric signing path (Ed25519 via raw PyJWT) so that kerbside holds only a public key. - SF already has PyJWT 2.13.0 as a direct dependency
(
pyproject.toml:116), but notcryptography, which PyJWT requires for EdDSA. Kerbside already depends on bothcryptographyand PyJWT (viaflask-jwt-extended). - The post-etcd cluster-wide key/value store is the
cluster_configtable (mariadb.py:1530), read intoSHAKENFIST_*environment variables at process start byload_cluster_config()(config.py:27).AUTH_SECRET_SEEDis already shared across API nodes exactly this way — the signing keypair follows the same pattern. - The pattern for publishing cluster material to kerbside
exists:
GET /admin/cacert(external_api/admin.py:47, any authenticated namespace) returns the SPICE CA cert, advertised via thecluster-cacertcapability string in the root page HTML (external_api/app.py:201), and consumed by kerbside'sShakenFistSource.__init__at source-init time. @redirect_instance_request(external_api/base.py:273) proxies vdiconsole requests to the instance's hypervisor because the direct.vvneeds node-local state (the SPICE CA file and the node's ports). A proxy-token endpoint needs none of that, so it can answer from any API node and must not carry that decorator.- The Python client (
shakenfist_client, repo shakenfist/client-python) follows HTTP redirects silently (apiclient.py:208-236), so the new endpoint must return the kerbside URL in a JSON body, not as a 3xx redirect — otherwise clients transparently fetch the.vvand the URL is never surfaced to tooling that wants it. - Kerbside's exchange machinery exists for Nova: the
NovaTokenresource (kerbside/api.py:516, route/nova-console.vv) validates an external token, callsdb.add_console(...), mints a kerbsideconsoletoken(a 48-char random string sized for the SPICE password field,kerbside/consoletoken.py:19) and returns a.vv. The SF exchange endpoint mirrors this shape, minus the callback. - The CLI's viewer launch already exists, but is virt-viewer
shaped:
sf-client instance vdiconsolewrites the.vvto a temp file and runsremote-viewervia subprocess (shakenfist_client/commandline/instance.py:636-663). Ryll — our own SPICE client, which understands kerbside's.vvextensions includinghost-subject— accepts either--file <path>or--url <URL to fetch a .vv from>(ryll README, "Connect using a .vv configuration file"). The--urlform means the CLI can hand ryll the kerbside exchange URL directly: ryll performs the one-shot token exchange itself and the JWT never touches disk. Ryll is not currently pip-installable; the precedent for shipping a Rust binary via pip is kerbside-proxy's maturin bin wheel (kerbsiderust/kerbside-proxy/, built in Docker). - Hypervisor SPICE TLS certs are provisioned by the deployer:
one cluster CA, per-node server certs with
cn = hostname(deploy/ansible/roles/pki_internal_ca/tasks/host_certificate.yml). The cert subject is not currently recorded anywhere the API exposes. Kerbside's scrape yieldshost_subject: Nonefor SF consoles today, so the proxy's host-subject enforcement (which kerbside ships as of v0.4.0) protects only static/oVirt paths, not SF.
Mission and problem statement¶
Give Shaken Fist the best Kerbside integration of the three supported clouds:
- Per-instance authorisation, enforced by Shaken Fist. A
user may open a proxied console for an instance if and only
if they hold a namespace credential that
requires_instance_ownershipaccepts for that instance. - No connect-time coupling. Kerbside must be able to authorise a console open without calling the Shaken Fist API: token validation is an offline signature check plus a local database lookup.
- Defense in depth. Kerbside takes backend connection details (hypervisor IP, ports, host subject) only from its own scraped database, never from token claims — a forged or replayed token cannot point the proxy at an arbitrary backend.
- Seamless client UX.
sf-client instance vdiconsolelands the user in a running viewer session with all of this happening under the hood: mint the token, hand ryll the exchange URL, session opens. The token plumbing — and ideally the viewer installation itself, via a pip-installable ryll — is invisible. - The existing direct-to-hypervisor
.vvpath keeps working for clusters with no Kerbside deployed.
Out of scope for this plan (deferred to a later deployment
phase, see Future work): ansible-collection deployer support,
including a dedicated kerbside infrastructure group modelled
on the database tier.
Token flow (the design in one place)¶
sf-client Shaken Fist API Kerbside
| | |
|-- GET vdiconsoleproxy --->| |
| (namespace JWT) | ownership check |
| | mint Ed25519 JWT |
|<-- {url, expires_at} -----| (aud=kerbside, sub= |
| | instance, jti, exp) |
| | |
|-- GET /sf-console.vv?token=<jwt> ------------------->|
| | verify sig (cached |
| | pubkey), check aud/ |
| | exp, jti unused, |
| | console in scrape DB |
|<-- .vv (password = kerbside consoletoken) -----------|
| | |
|== SPICE connect to kerbside, existing machinery ====>|
The JWT is a bearer capability for one HTTP exchange, not
a session credential. It never appears in the .vv (the SPICE
password field cannot hold a ~250-char JWT; kerbside's
existing 48-char consoletoken fills that role). Kerbside
persists only the token's jti for replay rejection until
exp; Shaken Fist stores nothing per-token and mints-and-
forgets, recording only an audit event.
Token claims (settled in phase 0, strawman here):
iss: the SF cluster'sconfig.ZONE(matches existing JWT practice,util/access_tokens.py).aud: the kerbside deployment's public base URL (from the newKERBSIDE_URLcluster config). Kerbside rejects tokens whoseaudis not itself.sub: the instance UUID.sf:namespace: the owning namespace (audit/display only — kerbside must not make authorisation decisions from it).iat,exp: TTL default of a few minutes (KERBSIDE_TOKEN_DURATION, phase 0 decides the default).jti: UUID4, kerbside's replay-rejection handle.- Header
kid: which cluster signing key signed this, so rotation is possible without a flag day.
Alternatives considered¶
Nova-parity callback validation¶
Kerbside receives an opaque token and calls back to a new SF "validate console token" admin API, exactly as it does for Nova. Rejected: it reintroduces the availability coupling (SF API down means no new consoles), requires SF to persist issued tokens, and buys nothing — we control both sides, so offline signature verification is available to us in a way it was not for Nova.
Push model¶
SF calls a kerbside API to register each issued token, so kerbside validates locally. Rejected: inverts the dependency (SF must hold kerbside credentials and reach its API), requires new kerbside API surface, and fails when kerbside's control plane is briefly unreachable even though its data plane is fine. Signed tokens achieve the same local validation with no push channel.
SF builds the proxy .vv itself¶
The vdiconsoleproxy endpoint could skip the kerbside HTTP
exchange and emit a .vv pointing at kerbside directly, with
the JWT... somewhere. Rejected on two grounds: the SPICE
password field cannot carry a JWT (48-char kerbside tokens
exist precisely because the field is tiny), and SF would need
to know kerbside's public CA cert, ports, and proxy
host-subject to author the .vv — smearing kerbside's TLS
identity into SF's config. Kerbside stays the author of its
own .vv, as it is for Nova.
Kerbside-side ACLs on scraped consoles¶
Keep pure scraping and grow a per-console ACL model inside kerbside, mapping kerbside (Keystone) users to consoles. Rejected: duplicates SF's identity model in a second database that must be kept in sync, and kerbside's identity layer is a single coarse Keystone group with no relationship to SF namespaces. Authorisation belongs to the cloud that owns the instance.
Open questions¶
These are settled in phase 0 before implementation starts. Recommendations are recorded inline.
- Signing key custody and bootstrap. Recommendation:
store the keypair in
cluster_config(the same custody asAUTH_SECRET_SEED, which gates strictly more power), as a JSON value holding{kid, private_pem, public_pem, created}. Generate lazily on first use with an atomicINSERTon thekey_nameprimary key (loser of the race re-reads), plus ansf-ctlsubcommand for explicit pre-generation and rotation. Rotation model: publish current + previous public keys, sign with current only. - Token TTL default. Recommendation: 300 seconds. Long enough for a human to click through a saved URL, short enough that the replay-cache table stays trivial. The deeper revocation question is bounded by scrape freshness anyway (a deleted instance vanishes from kerbside's console table within a minute, and live sessions are governed by kerbside's existing termination machinery, not the JWT).
- Scrape scope. Today
ShakenFistSourcescrapes exactly one namespace (sources/shakenfist.py:62), which would make the token flow work only for instances in that namespace. Recommendation: when the source's credential is thesystemnamespace, scrape cluster-wide viaget_instances(all=True)(already supported by client and server). Phase 0 should decide whether per-namespace scraping survives as a filter option. - How kerbside learns which SF source a token belongs to.
Recommendation: mirror
NovaToken— iterate configuredshakenfistsources and attempt verification against each source's cached public keys;audmust equal kerbside's ownPUBLIC_FQDN-derived URL. On an unknownkid, refetch the source's published keys once before rejecting (this is also the rotation path). - Fate of the direct-to-hypervisor
.vvendpoint. It is ownership-gated and remains correct for proxyless clusters. Recommendation: keep it unconditionally for now; add a cluster config knob to disable it only if operators ask. Decide in phase 0 whether the client should prefer the proxy path automatically when the capability is advertised. - Kerbside admin-UI exposure of SF consoles. Anyone in
kerbside's Keystone access group can currently mint a
session for any scraped console via
/console/proxy/<source>/<uuid>/console.vv. With cluster-wide scraping this surface grows to every SF instance. Recommendation: keep the admin path (it is the operator break-glass and is audited), but record the decision explicitly; a follow-on could restrict.vvissuance for SF-sourced consoles to an admin subset of the access group. - host_subject provenance. The deployer provisions
per-node SPICE certs with
cn = hostname, but nothing exposes the subject via the API. Options: (a) kerbside synthesisesCN=<node hostname>per a per-source config flag; (b) each SF node records its actualserver-cert.pemsubject into node attributes at daemon startup, and the API exposes it for the scrape. Recommendation: (b) — it is accurate for custom PKI too and keeps kerbside free of deployment assumptions; (a) is acceptable as an interim if (b) grows too large, but note host-subject verification is exact-match on the full subject, so synthesis is brittle against deployer changes. - Endpoint and capability naming. Strawman used below:
route
GET /instances/<ref>/vdiconsoleproxy, capability stringvdi-console-proxy, pubkey routeGET /admin/vditokenpubkey, kerbside route/sf-console.vv. The kerbside route name is decided (operator preference, 2026-07-19):sf-consolerather than the fullshakenfist-console, accepting the asymmetry with the existing/nova-console.vvfor brevity. Phase 0 confirms or amends the rest. - Ryll distribution mechanism. A maturin bin wheel (the
kerbside-proxy pattern) is the strawman, published as a
standalone
shakenfist-ryllpackage that client users opt into via an extra (pip install shakenfist-client[vdi]). The risk is manylinux compliance: unlike kerbside-proxy (a headless network daemon), ryll's GUI build links winit's X11/Wayland/xkb surface, cpal's ALSA, and rfd's dialog backend — some of which are outside the manylinux allowed-library set. Phase 3 must prototype the wheel build early and, if auditwheel cannot be satisfied, fall back to an installer that fetches a ryll release binary (GitHub releases) into a user-local path, keeping thesf-clientUX identical. Decide in phase 0 only the shape (standalone package + extra, plus a PATH/remote-viewer fallback chain); let the phase-3 prototype pick the mechanism. - Viewer selection order in the CLI. Recommendation:
packaged ryll if importable/installed, else
ryllon$PATH, elseremote-viewer(which cannot follow the exchange URL, so the CLI fetches the.vvto a temp file for it exactly as today), with a--vieweroverride.remote-viewermust remain a working path — the.vvwe emit is standard virt-viewer format.
Execution¶
| Phase | Repo | Plan | Status |
|---|---|---|---|
| 0. Decisions and token format | shakenfist | PLAN-kerbside-vdi-tokens-phase-00-decisions.md | Not started |
| 1. Cluster signing key + pubkey publication | shakenfist | PLAN-kerbside-vdi-tokens-phase-01-signing-key.md | Done |
| 2. vdiconsoleproxy endpoint | shakenfist | PLAN-kerbside-vdi-tokens-phase-02-proxy-endpoint.md | Done |
| 3. Pip-installable ryll | ryll | PLAN-pip-distribution.md (in ryll's docs/plans/) | Done |
| 4. Client, CLI, and viewer launch | client-python | PLAN-vdi-console-tokens.md (in that repo, branch vdi-console-tokens-client) |
Done |
| 5. Kerbside exchange endpoint | kerbside | PLAN-kerbside-vdi-tokens-phase-05-exchange.md (in kerbside, branch sf-vdi-tokens) |
Done |
| 6. Cluster-wide scrape and host_subject | kerbside + shakenfist | PLAN-kerbside-vdi-tokens-phase-06-scrape.md (in kerbside) | Done |
| 7. Functional test: SF mint path | shakenfist | PLAN-kerbside-vdi-tokens-phase-07-ci.md (in kerbside) | Done |
| 8. Documentation | all | PLAN-kerbside-vdi-tokens-phase-08-docs.md | Done |
| 9. Full cross-repo end-to-end + kerbside exchange lane (post-merge, real SF) | all | PLAN-kerbside-vdi-tokens-phase-09-e2e.md (in kerbside) | Done |
Phase 0: Decisions and token format¶
Settle the Open questions above and write the token format down as a short normative spec (claims, algorithm, key storage, rotation, error responses) inside the phase plan. Everything downstream cites that spec. Plan at high effort — this is where cross-repo consistency is cheapest to buy.
Phase 1: Cluster signing key and pubkey publication (SF)¶
- Add
cryptographyas a direct dependency (PyJWT needs it for EdDSA; today SF has PyJWT but not cryptography). - Key generation and storage per phase 0:
cluster_configrow(s), atomic lazy creation,sf-ctlmanagement subcommand. GET /admin/vditokenpubkey: JSON list of{kid, alg, public_pem}. Auth mirrors/admin/cacert(@verify_token+@log_token_use, any authenticated namespace — the material is public-key only).- Capability string in the root-page HTML so clients and kerbside can probe for support.
- Unit tests: generation atomicity (two racing creators converge), endpoint shape, no private material in the response or in any log/event.
Phase 2: vdiconsoleproxy endpoint (SF)¶
- New
KERBSIDE_URLconfig field (empty default = feature off), populated cluster-wide viacluster_configlikeDNS_SERVERis today. - New
InstanceVDIProxyConsoleHelperEndpoint: decorator stack@verify_token/@arg_is_instance_ref/@requires_instance_ownership/@log_token_use— deliberately without@redirect_instance_request, since minting needs no node-local state and must work from any API node. - Guards: 404-equivalent when
KERBSIDE_URLis unset; the same instance-state andvideo.vdichecks the scrape applies (state == created, vdi type starts withspice). - Mint the JWT per the phase-0 spec; respond
{url: '<KERBSIDE_URL>/sf-console.vv?token=<jwt>', expires_at: ...}as JSON (no redirect — the client follows redirects silently and the URL must be surfaceable). - Audit event on the instance (
EVENT_TYPE_AUDIT) recordingjtiandkidbut never the token itself. - Unit tests: ownership enforcement (namespace A cannot mint for namespace B's instance), disabled-feature path, claim correctness, TTL.
Phase 3: Pip-installable ryll (ryll)¶
The goal: pip install shakenfist-ryll (typically via the
shakenfist-client[vdi] extra) puts a working ryll binary
on the venv's PATH, the way kerbside's pip install ships
kerbside-proxy.
- Prototype a maturin bin wheel early, mirroring
kerbside-proxy's Docker-wrapped build (ryll builds in
Docker already; no native toolchain on dev hosts). The
open risk is manylinux compliance for a GUI binary — see
open question 9. If auditwheel cannot be satisfied, pivot
to the fallback there (a thin
shakenfist-ryllpackage that fetches the matching GitHub release binary at install or first run) without changing the client-facing contract: either way the package exposes aryllexecutable. - CI: build the wheel in ryll's existing rust.yml lane; release automation alongside ryll's crate/binary releases.
- This phase deliberately contains all the packaging
weirdness so that phase 4 sees only "a
ryllbinary is installed or it is not".
Phase 4: Client, CLI, and viewer launch (client-python)¶
get_vdi_console_proxy(instance_ref)returning the parsed JSON, and a convenience that fetches the kerbside URL and returns.vvtext (plainrequests.get— kerbside's endpoint takes no SF auth).get_vdi_token_public_keys()for kerbside's source driver.- CLI:
sf-client instance vdiconsolebecomes the seamless path — when thevdi-console-proxycapability is advertised, mint the token and launch the viewer; with ryll, pass the exchange URL viaryll --url(no temp file, token never on disk); withremote-viewer, fetch the.vvto a temp file as today. Viewer selection per open question 10 (packaged ryll → PATH ryll → remote-viewer,--viewer/--directoverrides).vdiconsolefilekeeps working for both paths. - Packaging:
vdiextra depending onshakenfist-ryll(from phase 3). - Release note: kerbside's phase 6 consumes
get_vdi_token_public_keys(), so a client release precedes it (kerbside pinsshakenfist-client>=).
Phase 5: Kerbside exchange endpoint (kerbside)¶
- New
SfTokenresource at/sf-console.vv, mirroringNovaToken(api.py:516) with validation replaced by: signature verification against cached per-source public keys (EdDSA; refetch-once on unknownkid),aud/expchecks, and single-usejtienforcement. - Alembic migration: a
sf_token_jtisreplay table (jtiprimary key,expiry); reject exchanges whosejtiis present; reaper in the maintenance loop mirroring_reap_expired_console_tokens(main.py:185). - Console lookup strictly from the scraped
consolestable by(source, sub)— claims are never used for backend addressing. 404 when the console is not (yet) scraped. - Issue the kerbside
consoletokenand.vvexactly as the Nova path does; audit events for accepted and rejected exchanges. - Unit tests: replay rejection, expired token, wrong
aud, unknownkid(with and without successful refetch), forged signature, unscraped console.
Phase 6: Cluster-wide scrape and host_subject (kerbside + SF)¶
- Scrape scope per phase 0:
get_instances(all=True)under a system credential; public-key fetch added toShakenFistSource.__init__besideget_cluster_cacert(), with periodic refresh. - host_subject per phase 0's decision: SF-side node attribute
publication and/or kerbside-side synthesis, then populate
host_subjectin the scrape yield (sources/shakenfist.py:85) so the proxy's existing enforcement covers SF backends. - The SF half of this phase (node cert-subject publication) lands on the SF branch with its own phase plan section.
Phase 7: Functional test — SF mint path¶
Narrowed to the SF-side mint test; the kerbside functional lane moved to phase 9 (see below and the phase-7 plan in kerbside for the grounding).
- SF: a
cluster_ci_testsfunctional test (deploy/shakenfist_ci/cluster_ci_tests/test_vdi_tokens.py) asserting the minting path (ownership gate, JSON shape, verifiable signature against the published pubkey) without needing a kerbside deployment. Calls the REST endpoints via_request_urlso it is independent of the client PR's merge order, and skips cleanly whenKERBSIDE_URLis unprovisioned (it is process-cached at SF start, so it is set for real by phase 9's deployment; the test then activates).
Phase 8: Documentation¶
Done. The detailed plan of record for this phase is
PLAN-kerbside-vdi-tokens-phase-08-docs.md (authored in the kerbside repo
alongside phases 5–7). The four repos' doc PRs are independent.
- SF: user guide (how to open a proxied console), operator
guide (enabling
KERBSIDE_URL, key rotation runbook),ARCHITECTURE.md/AGENTS.mdtouch-ups. - Kerbside: rewrite the Shaken Fist section of
docs/console-sources.md(it currently documents only the scrape model), updatedocs/proxy-architecture.mdand the plan index. - client-python: README/CLI help, including the viewer
selection chain and the
[vdi]extra. - ryll: README install section (pip alongside the existing build-from-source instructions).
Phase 9: Full cross-repo end-to-end (post-merge, real SF)¶
Runs once the four PRs are on develop, so kerbside CI can
install SF and the client at HEAD. Absorbs the kerbside
exchange/proxy lane that phase 7 originally intended: a
direct-qemu-style exchange against a static console is
infeasible because kerbside only trusts type: shakenfist
sources for token verification and the maintenance loop reaps
any console a live scrape did not yield — so a real SF (not a
static or hand-seeded console) is required.
- Deploy a single-node SF in CI (as realized: the
build-smoke-clusteraction, not getsf), provisionKERBSIDE_URL(deploy-time, since it is process-cached) and a signing key, point a kerbside at it, and drive the full flow: mint token via client, exchange for.vv, connect through the proxy, assert session audit/teardown. - Adversarial coverage against the live app: replayed token, expired token, wrong audience, unknown kid, cross-namespace mint attempt.
- The SF mint path (phase 7's
test_vdi_tokens.py) activates here too, onceKERBSIDE_URLis provisioned.
Dependencies on other plans¶
None hard. This plan is independent of the BYO-MariaDB /
remove-primary thread — it uses cluster_config as it exists
today. Sequencing within this plan: phases 1→2 are ordered on
the SF side; phase 3 (ryll packaging) is independent and can
start immediately — it is also the highest-uncertainty item,
so starting its prototype early buys information cheaply;
phase 4 needs phases 2 and 3 (and degrades gracefully if 3
slips, via the remote-viewer fallback); phase 5 needs only
the phase-0 spec plus phase 1 (a pubkey endpoint to fetch in
tests can be faked until then); phase 6 needs a released
client containing phase 4's get_vdi_token_public_keys(). A
kerbside release (v0.5.0) ships phases 5–6; the
lockstep-release discipline from the rust-proxy transition
applies if any proxy-contract change sneaks in (none is
expected — this plan is entirely control-plane).
Agent guidance¶
Execution model¶
All implementation work is done by sub-agents, never in the management session. The management session is reserved for planning, review, and decision-making. The workflow is:
- Plan at high effort in the management session.
- Spawn a sub-agent for each implementation step with the brief from the phase plan, at the recommended effort level and model.
- Review the sub-agent's output in the management session. Check the actual files — the sub-agent's summary describes what it intended, not necessarily what it did.
- Fix or retry if the output is wrong. Diagnose whether the brief was insufficient (improve it) or the model was too light (upgrade it), then re-run.
- Commit once the management session is satisfied.
Cross-repo caution: a sub-agent works in exactly one repository per step. Steps that "need" to touch two repos are a sign the phase plan split a seam wrongly — go back to the plan.
Planning effort¶
The master plan and phase 0 are high-effort work. Phases 1, 2 and 5 involve key custody, authorisation gates, and replay semantics — plan those at high effort too. Phase 4 (client methods mirroring existing ones, launcher chain) and phase 8 (docs) can be planned at medium effort. Two items carry the plan's schedule risk and deserve design-document treatment rather than checklists: phase 3's manylinux question (spike it first) and phase 7's SF-lane-in-kerbside-CI step.
Step-level guidance¶
Each phase plan should include the step table from
PLAN-TEMPLATE.md (| Step | Effort | Model | Isolation |
Brief for sub-agent |). Front-load research into briefs: cite
file and line for the pattern each step mirrors (e.g. "mirror
AdminClusterCaCertificateEndpoint at
external_api/admin.py:47", "mirror the NovaToken resource
at kerbside/api.py:516"). Security-sensitive steps (key
generation, signature verification, replay cache) should
recommend opus and include the adversarial test list in the
brief.
Management session review checklist¶
Per PLAN-TEMPLATE.md, plus specific to this plan:
- No private key material in API responses, logs, or
events (grep the diff for
privatenear logging calls). - Kerbside never reads backend addresses from JWT claims.
- Every new endpoint carries the intended decorator stack
— in particular
requires_instance_ownershipon the minting endpoint and noredirect_instance_request. -
pre-commit run --all-filespasses in whichever repo changed (SF's runs flake8, stestr, mypy; kerbside's runs its tox envs).
Administration and logistics¶
Success criteria¶
We will know this plan is complete when:
- A user holding only a namespace key can run
sf-client instance vdiconsole <instance>against a Kerbside-enabled SF cluster and land in a running ryll session for their own instance with no other manual steps (givenpip install shakenfist-client[vdi]) — and cannot get a console for an instance in another namespace. - The same command still works with only
remote-viewerinstalled, and still works direct-to-hypervisor on clusters with noKERBSIDE_URLconfigured. - Kerbside opens that console with zero Shaken Fist API calls on the exchange path (signature check + local DB only).
- A replayed exchange URL is rejected; an expired one is rejected; both rejections are audited on the kerbside side, and every mint is audited on the SF side.
- SF-sourced consoles carry a
host_subjectand the proxy enforces it, closing the gap where enforcement covered only static/oVirt sources. - CI proves the end-to-end flow (kerbside SF lane) and the minting authorisation gate (SF cluster_ci), and the pre-push checks pass in all four repos.
- Documentation in all four repos reflects the feature, and the plan index status tables are current.
Future work¶
- Deployer support (explicitly deferred phase). Teach the
ansible collection to deploy Kerbside alongside SF: a
dedicated
kerbsideinfrastructure group (modelled on the database tier from the BYO-MariaDB work, not "every node"), renderingsources.yamlwith a system credential, writingKERBSIDE_URLintocluster_config, and wiring kerbside's own TLS material. This lands as its own plan once the code support here has proven itself. - Kerbside admin-auth pluggability: kerbside's own login is
Keystone-only (
kerbside/api.py:200), which is an odd requirement for an SF-only deployment. Possibly converges with SF's OIDC plan (PLAN-oidc-authentication.md) and the auth-federation branch. - Restricting kerbside's operator break-glass
.vvissuance for SF-sourced consoles to an admin subset (open question 6 records the current decision). - Signing-key rotation automation (scheduled rotation rather
than operator-initiated
sf-ctl).
Bugs fixed during this work¶
(To be filled in as encountered. A scan of the shakenfist and kerbside issue trackers for related open bugs should happen during phase 0 and be recorded here.)
Documentation index maintenance¶
On creation of this plan: add a Plan Status row to
docs/plans/index.md and an entry to docs/plans/order.yml
in the shakenfist repo, and a cross-reference entry to
kerbside's docs/plans/index.md. As phases complete, update
the Execution table above and both indexes.
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.