Skip to content

Silo certificates tab - #3410

Draft
david-crespo wants to merge 7 commits into
mainfrom
silo-certificates-tab
Draft

david-crespo wants to merge 7 commits into
mainfrom
silo-certificates-tab

Conversation

@david-crespo

Copy link
Copy Markdown
Collaborator

Prototype to make sure oxidecomputer/omicron#11470 is solid. Needs polish but it works. I think for deletes we'll want to do the same thing we're discussing for deleting your own role, making the user type something to confirm it.

image image image image

@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
console Ready Ready Preview Oct 15, 2026 8:44pm UTC

Request Review

@david-crespo

david-crespo commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

@claude review

(testing this feature)

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 app/pages/system/silos/SiloCertificatesTab.tsx — After deleting a silo certificate, fleet admins get a toast naming the certificate's UUID rather than the name they clicked. The toast reads variables.path.certificate. The delete call at line 130 sets that value to cert.id, because the silo selector has to be left out when the cert is given by ID. Fix: take the name from the row that was deleted, not from the request path. For example, show cert.name in the toast from the doDelete closure, or pass the name to onSuccess some other way, and keep the ID-based delete call unchanged.

    Why this was flagged

    An admin opens /system/silos/maze-war/certificates and picks Delete from the row menu for maze-war-cert. The doDelete call at SiloCertificatesTab.tsx:539 is deleteCert({ path: { certificate: cert.id } }), so variables.path.certificate holds the UUID 7d36cbfc-9dba-4600-9816-323a22b4c719. The onSuccess handler at SiloCertificatesTab.tsx:527 shows that value, so the toast says "Certificate 7d36cbfc-... deleted" instead of the name the admin selected in the confirm modal. Every other delete toast in this flow, such as the create toast at silo-certificate-create.tsx:207, shows the resource name. The e2e test in test/e2e/silos.e2e.ts only closes the toast and never checks its text, so it doesn't catch this.

    Verification: nit. In app/pages/system/silos/SiloCertificatesTab.tsx, the onSuccess(_data, variables) handler on lines 114-118 shows Certificate <HL>{variables.path.certificate}</HL> deleted. Line 130 calls deleteCert({ path: { certificate: cert.id } }). So the toast shows the certificate's UUID, while the confirm modal shows the name (label: cert.name, line 131). Only the toast text is wrong.

Comment thread app/util/certificate.ts
Comment on lines +47 to +63
sha256Fingerprint: toHex(await cert.getThumbprint('SHA-256')).join(':'),
keyAlgorithm: describeKey(cert.publicKey.algorithm),
selfSigned: cert.subject === cert.issuer,
}
}

/**
* Parse every cert in a PEM chain, leaf first by convention. Returns null if
* there are no PEM blocks or any block fails to parse.
*/
export async function parseCertChain(pem: string): Promise<ParsedCert[] | null> {
const blocks = pem.match(PEM_BLOCKS_RE)
if (!blocks) return null
try {
return await Promise.all(blocks.map(parseOne))
} catch {
return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) If the console is opened over plain HTTP (not localhost), the certificate detail view shows "Couldn't parse certificate" and hides every parsed field, even for valid certs. The fingerprint at line 47 calls cert.getThumbprint('SHA-256'), which uses WebCrypto crypto.subtle. Browsers leave that undefined outside secure contexts, so the call throws. The catch at lines 62-63 then turns the whole chain into null. Fix: let the fingerprint fail on its own, e.g. catch around getThumbprint and leave the fingerprint unset, so domains, issuer, validity, serial and the chain still render. ChainList's key={c.sha256Fingerprint} then needs a fallback key.

Why this was flagged

The trigger is the console being served from an insecure origin, for example a rack or dev setup reached at http://. In that setup window.crypto.subtle is undefined. Opening /system/silos/:silo/certificates/:certificate runs parseCertChain(cert.cert) (silo-certificate-view.tsx). That function awaits Promise.all(blocks.map(parseOne)) (certificate.ts:61). Inside parseOne, line 47 awaits cert.getThumbprint('SHA-256'), which calls crypto.subtle.digest and throws. The bare catch { return null } at certificate.ts:62-63 throws away all the fields that parsed fine. The view then takes the !isPending && !leaf branch and shows the notice "Couldn't parse certificate" / "Expected one or more X.509 certificates in PEM format." for a valid PEM, with no Domains, Issuer, Expires or Chain section. This is new code, so the base has no equivalent. No safeguard isolates the one optional async field from the rest of the parse.

Verification: Mechanism: app/util/certificate.ts:47 computes sha256Fingerprint: toHex(await cert.getThumbprint('SHA-256')).join(':') inside parseOne. On an http non-localhost origin it is undefined, so the digest call throws and getThumbprint rejects. parseCertChain runs return await Promise.all(blocks.map(parseOne)) with catch { return null } (lines 60-64).

Comment thread mock-api/msw/handlers.ts
Comment on lines +2505 to +2509
siloCertificateList({ query, cookies }) {
requireFleetViewer(cookies)
const silo = lookup.silo({ silo: query.silo })
const certs = db.certificates
.filter((c) => c.siloId === silo.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) In the mock API, a silo created with TLS certificates shows an empty Certificates tab. The new tab reads only db.certificates. The existing siloCreate handler never writes body.tls_certificates to that table. Fix: in siloCreate (handlers.ts:1943), push each entry of tls_certificates into db.certificates with the new silo's siloId, dropping key. Also leave tls_certificates out of the stored silo object, so the private keys are not stored on it.

Why this was flagged

In dev or e2e against the mock, a user creates a silo through the silo-create form and adds a cert with the TLS certs field. siloCreate (mock-api/msw/handlers.ts:1943-1962) spreads ...body into the silo record, tls_certificates included. It never inserts anything into db.certificates. The new siloCertificateList handler filters db.certificates by siloId (handlers.ts:2505-2508). So /system/silos//certificates shows "No certificates", even though the silo was created with one. Real Nexus stores those certs as the silo's certificates. Before this change no UI read silo certificates, so the gap was invisible. The new tab makes it reachable, and it shows mock behaviour that contradicts the API.

Verification: siloCreate (mock-api/msw/handlers.ts:1943-1962) pushes to db.silos, siloQuotas, siloProvisioned and siloSettings, and never to db.certificates. The new siloCertificateList (handlers.ts:2505-2511) returns only db.certificates.filter((c) => c.siloId === silo.id). So every silo created in the mock shows "No certificates" on its tab, even though the user just added one.

@david-crespo

Copy link
Copy Markdown
Collaborator Author

Decent result, those are real issues. Not sure we care about the http thing, but we can do a fallback anyway.

@david-crespo david-crespo added this to the 24 milestone Oct 16, 2026

This branch was successfully deployed

1 active deployment
Preview — d2d16365 Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant