First run
++ No operator can sign in yet. Register your operator CA and get the first admin + certificate. +
+-
+ {STEPS.map((s) => (
+
- + {s.label} + + ))} +
+ The first-run session ends after 15 minutes without a call. If it ends while you are at the CA, + start again with the newest token from the log, confirm the CA's fingerprint again, and + continue from your key backup. +
+); + +const certFrom = (text: string): Uint8Array => + readCertificateDer(new TextEncoder().encode(text.trim())); + +// PathA: the browser makes the admin's P-384 key and CSR, with the key backup +// saved first. The CA signs the CSR out of band; the signed certificate comes +// back here, the manager checks and records it, and the browser builds the +// PKCS#12 with the backup's passphrase. +const PathA = ({ + adminExtfile, + anchorDer, + onDone, +}: { + adminExtfile: string; + anchorDer: null | Uint8Array; + onDone: () => void; +}) => { + const [fullName, setFullName] = useState(""); + const [email, setEmail] = useState(""); + const [mode, setMode] = useState<"details" | "make" | "upload">("details"); + const [backup, setBackup] = useState+ Take the CSR to your operator CA and sign it with the admin section above (add it to the + CA's OpenSSL config; never copy extensions from the CSR). Bring back the signed + certificate. +
++ {w} +
+ ))} + {error ? ( ++ {error} +
+ ) : null} ++ Install the PKCS#12 yourself. Your first sign-in with it records the certificate as the + first admin and closes first run. +
+Optional pre-flight
++ Paste the signed certificate to have the Fleet Manager check it now. Only the certificate + is sent. +
+ + + {error ? ( ++ {error} +
+ ) : null} + {result ? ( ++ Recorded {result.email}, serial {result.serialHex}. + {result.warnings.length > 0 ? ` ${result.warnings.join(" ")}` : ""} +
+ ) : null} + ++ Looking for a registered operator CA… +
+ ); + } + + if (current !== "none" && !replace) { + const ca = current.operatorCa; + const matches = normalizeFingerprint(pasted) === ca.sha256; + const reconfirm = async () => { + setPending(true); + setError(null); + try { + const result = await registerOperatorCABootstrap({ + ...EMPTY_FORM, + confirmSha256: ca.sha256, + }); + onRegistered(result.confirmed ? result : { ...current, confirmed: true }, null); + } catch (error_: unknown) { + setError(fleetErrorMessage(error_, "Confirming the operator CA failed.")); + } finally { + setPending(false); + } + }; + return ( ++ An operator CA is already registered: {ca.subject}. + Confirm it is yours before you continue. +
+
+ {colonFingerprint(ca.sha256)}
+
+
+ On the CA machine, run{" "}
+
+ openssl x509 -in operator-ca.crt -noout -fingerprint -sha256
+ {" "}
+ and paste the output. If it doesn't match, register your own CA: that retires this
+ one immediately.
+
+ {error} +
+ ) : null} ++ Import the PKCS#12 with its passphrase (the key backup's), then reload this page and + choose the FleetOS certificate when the browser asks. If it doesn't ask, quit the browser + fully and start it again: Chrome remembers a "no certificate" answer per site. Your + first sign-in as admin closes first run. +
++ Keep the key backup somewhere safe, or delete it once the PKCS#12 is imported. +
+ ++ No operator can sign in yet. Register your operator CA and get the first admin + certificate. +
+
+ {children}
+
+);
+
+// TokenStep starts the first-run session with the bootstrap token from the
+// manager's log. The fingerprint check comes first: the server certificate is
+// self-signed, so comparing its SHA-256 with the log is the only defence
+// against someone in the middle capturing the token.
+export const TokenStep = ({
+ inProgress,
+ onStarted,
+ tokenExpiresAt,
+}: {
+ inProgress: boolean;
+ onStarted: () => void;
+ tokenExpiresAt: string;
+}) => {
+ const [token, setToken] = useState("");
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useStateCheck the fingerprint first
+
+ This Fleet Manager serves a self-signed certificate until you give it one. Before you type
+ the token, open the certificate details from the browser's address bar and compare
+ its SHA-256 fingerprint with the{" "}
+ server certificate SHA-256 line in the Fleet
+ Manager's log. If they differ, stop: someone may be intercepting the connection, and
+ the token would be theirs.
+
+ First run was already started. If that wasn't you, continue with the newest token + from the log. That ends the earlier session, and replacing the registered CA retires + anything it trusted. +
+ ) : null} + +Before you start
++ You need an external operator CA: an OpenSSL CA, or an enterprise or offline CA that can + issue the operator certificate profile. Have its CA certificate file, and a way to get a + CSR signed by it. A CryptOS node can't be the operator CA. +
+Find the bootstrap token
++ The Fleet Manager prints a single-use token in its log at start, and a new one each time + one is used or expires. Take the newest. +
+Linux / macOS
+Windows (PowerShell)
++ The current token expires at {tokenExpiresAt}. +
+ ) : null} ++ {error} +
+ ) : null} +first-run wizard
; + }, +})); +vi.mock("@/components/layout/diagnostics-copy", () => ({ DiagnosticsCopy: () => null })); + +const status = (state: BootstrapState, reasonCode = ErrorReason.UNSPECIFIED) => ({ + reasonCode, + state, + tokenExpiresAt: "2026-09-30T13:00:00Z", +}); + +const renderGate = () => + render( +auth gate
+auth gate
+auth gate
++ {UNAVAILABLE_COPY[bootstrap.reasonCode] ?? + "First run is unavailable on this Fleet Manager, and no operator CA is configured."} +
+ {children} + > + ); + } + + return <>{children}>; +}; diff --git a/src/components/operator-ca-register-form.tsx b/src/components/operator-ca-register-form.tsx index 30c66a1..e3591d8 100644 --- a/src/components/operator-ca-register-form.tsx +++ b/src/components/operator-ca-register-form.tsx @@ -172,7 +172,7 @@ export const OperatorCARegisterForm = ({ onRegistered, submit, }: { - onRegistered: (result: RegisterResult) => void; + onRegistered: (result: RegisterResult, form: RegisterForm) => void; submit: (form: RegisterForm) => Promise