crates/gunnar-grpc/src/service/bootstrap.rs
//! `Bootstrap`: the one service gunnar serves without a credential, and only
//! while nothing has ever been enrolled.
//!
//! # The whole security boundary, in one sentence
//!
//! `Enroll` is refused unless
//! `gunnar_core::AuthTable::bootstrap_window_open` is true, and the check that
//! decides that is inside the same compare-and-swap that writes the first
//! identity — so there is no interval between deciding and acting for a second
//! caller to slip through.
//!
//! # Why this service is NOT behind [`crate::auth::AuthGate`]
//!
//! It cannot be. The gate's whole job is to look a fingerprint up in the ACL key
//! table, and on a server that has never been enrolled the key table is empty:
//! every credential, including a correct one, fails. Putting `Bootstrap` behind
//! the gate would make the first boot unreachable, which is the defect this
//! service exists to fix.
//!
//! So it is mounted **beside** the gate, and its own refusal is the door. Two
//! properties keep that honest:
//!
//! * [`crate::auth::UNAUTHENTICATED_METHODS`] names exactly these two RPCs, and
//! `tests/auth.rs` asserts that every other method in
//! [`crate::auth::METHODS`] is refused without a credential. A third
//! unauthenticated method cannot be added by accident; it has to be written
//! into that list, where it is visible.
//! * the refusal lives in `gunnar-core`, under the CAS, not here. A handler
//! that forgot to check would still be refused by the writer.
//! * `Restore` is outside the gate but NOT credential-free (h-v1-6a,
//! 2026-09-28): [`crate::auth::authenticate_restore`] checks the stream's
//! `gunnar-sshsig-v2` header against the keys the box was fed, and the
//! verified key is the owner the resume is held to.
//! * `BecomeTwin` and `InstallTwinCertificate` likewise (h-bootstrap-open,
//! 2026-09-28): [`crate::auth::authenticate_bootstrap`], a credential bound to
//! the method from a FED key — a twin follows whatever primary it is told and
//! pins the first issuer it is handed, so neither is anybody's to call.
//!
//! # What it says out loud
//!
//! Every call, allowed or refused, is one record on
//! [`crate::auth::AUDIT_TARGET`]. An enrolment is `warn!`, not `info!`: it
//! happens once in a server's life and it hands somebody administrative control
//! of the box. A refused enrolment is `warn!` too — on a server that has been
//! enrolled, somebody calling `Bootstrap.Enroll` is either a confused operator
//! or an attacker trying the obvious thing, and both are worth a line.
use gunnar_proto::bootstrap_server::Bootstrap;
use gunnar_proto::{
AssignTunnelAddressRequest, AssignTunnelAddressResponse, BecomeTwinRequest, BecomeTwinResponse,
BootstrapStatusRequest, BootstrapStatusResponse, EnrollRequest, EnrollResponse,
GrowStatusRequest, GrowStatusResponse, InstallTwinCertificateRequest,
InstallTwinCertificateResponse, LogChunk, RestoreResponse, TwinCsrRequest, TwinCsrResponse,
};
use tonic::{Request, Response, Status};
use crate::seam::{BootstrapState, ControlPlane};
use crate::service::BootstrapService;
/// The audited method name for enrolment.
pub const METHOD_ENROLL: &str = "Bootstrap.Enroll";
/// The audited method name for the status probe.
pub const METHOD_STATUS: &str = "Bootstrap.Status";
/// The audited method name for the runtime growth's credential-free status.
pub const METHOD_GROW_STATUS: &str = "Bootstrap.GrowStatus";
/// The audited method name for the twin door.
pub const METHOD_BECOME_TWIN: &str = "Bootstrap.BecomeTwin";
/// The audited method name for the restore door.
pub const METHOD_RESTORE: &str = "Bootstrap.Restore";
/// How long a `Bootstrap.Restore` stream waits for the box's intake while another stream still
/// holds it (an abandoned stream whose last apply has not returned).
pub const RESTORE_INTAKE_WAIT: std::time::Duration = std::time::Duration::from_secs(20);
/// How long a `Bootstrap.Restore` stream may go without a chunk before the box ends it (and
/// lets go of its intake): a sender whose connection died without a word.
pub const RESTORE_IDLE: std::time::Duration = std::time::Duration::from_secs(180);
/// The audited method name for the tunnel-address door.
pub const METHOD_ASSIGN_TUNNEL_ADDRESS: &str = "Bootstrap.AssignTunnelAddress";
/// The audited method name for the twin's certificate request (lane T9).
pub const METHOD_TWIN_CSR: &str = "Bootstrap.TwinCsr";
/// The audited method name for installing the twin's certificate (lane T9).
pub const METHOD_INSTALL_TWIN_CERTIFICATE: &str = "Bootstrap.InstallTwinCertificate";
/// One sentence naming the state, written once so the log line, the status
/// response and the start-up banner cannot describe the same server three
/// different ways (one implementation).
pub fn describe(state: &BootstrapState) -> String {
if state.window_open {
return "NO IDENTITY HAS EVER BEEN ENROLLED. Bootstrap.Enroll is open to any caller that \
can reach this control port, with no credential. It closes permanently on the \
first success."
.to_owned();
}
if state.bootstrapped && state.keys == 0 {
return format!(
"bootstrapped at {} by \"{}\", but the identity table now holds {} principals and 0 \
keys: nobody can authenticate against this server, and the enrolment window stays \
shut. Add a key with `gunnar key add` while the server is down.",
state.bootstrapped_at_unix_ms, state.bootstrapped_principal, state.principals
);
}
if state.bootstrapped {
return format!(
"bootstrapped at {} by \"{}\"; the enrolment window is closed permanently. {} \
principals, {} keys.",
state.bootstrapped_at_unix_ms,
state.bootstrapped_principal,
state.principals,
state.keys
);
}
format!(
"the enrolment window is closed: this identity table already holds {} principals and {} \
keys but carries no bootstrap record, so it was written before the record existed.",
state.principals, state.keys
)
}
#[tonic::async_trait]
impl Bootstrap for BootstrapService {
async fn enroll(
&self,
request: Request<EnrollRequest>,
) -> Result<Response<EnrollResponse>, Status> {
let request = request.into_inner();
if request.name.trim().is_empty() {
return Err(Status::invalid_argument(
"`name` is required: it is both the user's id and the account it owns, the \
<account> in <host>/<account>/<repo>",
));
}
if request.openssh_public_key.trim().is_empty() {
return Err(Status::invalid_argument(
"`openssh_public_key` is required: one authorized_keys line, e.g. \
\"ssh-ed25519 AAAAC3Nz… name@host\"",
));
}
let name = request.name.trim();
match self
.ctx
.control
.enroll_first_identity(name, request.openssh_public_key.trim())
{
Ok(enrolled) => {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_ENROLL,
principal = %enrolled.principal.id,
fingerprint = %enrolled.key.fingerprint,
account = %enrolled.account,
decision = "allow",
"THE UNAUTHENTICATED ENROLMENT WINDOW IS NOW CLOSED. This principal is \
gunnar-admin and is intrinsically admin of its own account. Nothing else \
on this server will ever be served without a credential."
);
let at = enrolled.bootstrapped_at_unix_ms;
Ok(Response::new(EnrollResponse {
principal: Some(enrolled.principal),
key: Some(enrolled.key),
account: enrolled.account,
bootstrapped_at_unix_ms: at,
client_cert_pem: enrolled.client_cert_pem,
client_key_pem: enrolled.client_key_pem,
issuer_cert_pem: enrolled.issuer_cert_pem,
}))
}
Err(err) => {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_ENROLL,
principal = %name,
decision = "deny",
reason = %err,
"Bootstrap.Enroll refused"
);
Err(Status::from(err))
}
}
}
async fn become_twin(
&self,
request: Request<BecomeTwinRequest>,
) -> Result<Response<BecomeTwinResponse>, Status> {
// **Who is making this box a twin, proved** (h-bootstrap-open, 2026-09-28): a
// credential bound to this method, from a key the box was FED — the same rule
// `Restore` keeps, because a twin follows whatever primary it is told to.
self.fed_caller(
request.metadata(),
crate::auth::BOOTSTRAP_BECOME_TWIN_METHOD,
METHOD_BECOME_TWIN,
)?;
let primary = request.into_inner().primary;
// Already a twin before this call: `become_twin` answers the same key for the same
// primary (and refuses any other), so nothing changes — said at INFO, not as news.
let already = self.ctx.control.is_twin();
match self.ctx.control.become_twin(primary.trim()) {
Ok(key) if already => {
let restart_required = self.ctx.control.twin_git_listener_needs_restart();
tracing::info!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_BECOME_TWIN,
primary = %primary,
restart_required,
decision = "allow",
"this box is already the twin of {}: asked again, it answered the same key (nothing changed)",
primary.trim()
);
Ok(Response::new(BecomeTwinResponse {
twin_public_key: key,
detail: format!(
"already a twin of {primary}; enrol this key on the primary in group `{}`",
crate::TWIN_GROUP
),
restart_required,
}))
}
Ok(key) => {
// **The socket that could not move** (2026-09-21, lane T4). The
// git bind is chosen once, at start-up, and a role conferred
// over the wire arrives after it. Nothing is served through it
// — every git service on a twin is refused by name at the door
// from this instant — but the socket is still on a primary's
// address, and only a restart moves it. Said here rather than
// left for somebody to find with `ss`.
let restart_required = self.ctx.control.twin_git_listener_needs_restart();
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_BECOME_TWIN,
primary = %primary,
restart_required,
decision = "allow",
"THIS BOX IS NOW A TWIN: it gave up its own log, froze, and follows the primary. \
Enrol the key below on the primary in group `twin`."
);
if restart_required {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_BECOME_TWIN,
"this box was already listening when it was told it is a twin: its GIT LISTENER is still bound where a \
primary's was and only a restart moves it to loopback. It serves no git in the meantime — every git \
service is refused by name — but the socket is on the wrong address until the box comes back."
);
}
Ok(Response::new(BecomeTwinResponse {
twin_public_key: key,
detail: format!(
"a twin of {primary}; enrol this key on the primary in group `{}`{}",
crate::TWIN_GROUP,
if restart_required {
". RESTART THIS BOX: its git listener was bound before it was told it is a twin, so the socket is \
still on a primary's address. No git is served through it — every request is refused by name — but \
only a restart puts it on loopback"
} else {
""
}
),
restart_required,
}))
}
Err(err) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_BECOME_TWIN, primary = %primary, decision = "deny", reason = %err, "Bootstrap.BecomeTwin refused");
Err(Status::from(err))
}
}
}
async fn assign_tunnel_address(
&self,
request: Request<AssignTunnelAddressRequest>,
) -> Result<Response<AssignTunnelAddressResponse>, Status> {
let address = request.into_inner().address;
let address = address.trim();
match self.ctx.control.assign_tunnel_address(address) {
Ok(answer) => {
if answer.changed {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_ASSIGN_TUNNEL_ADDRESS, address = %answer.address, wg_public_key = %answer.wg_public_key, tunnel_up = answer.tunnel_up, decision = "allow", "THIS BOX'S TUNNEL ADDRESS WAS HANDED OVER: register its key on the hub at this address");
}
Ok(Response::new(answer))
}
Err(err) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_ASSIGN_TUNNEL_ADDRESS, address = %address, decision = "deny", reason = %err, "Bootstrap.AssignTunnelAddress refused");
Err(Status::from(err))
}
}
}
async fn twin_csr(
&self,
_request: Request<TwinCsrRequest>,
) -> Result<Response<TwinCsrResponse>, Status> {
match self.ctx.control.twin_csr() {
Ok(csr_pem) => {
tracing::info!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_TWIN_CSR, decision = "allow", "Bootstrap.TwinCsr: the twin's replication request (its key stays here)");
Ok(Response::new(TwinCsrResponse { csr_pem }))
}
Err(err) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_TWIN_CSR, decision = "deny", reason = %err, "Bootstrap.TwinCsr refused");
Err(Status::from(err))
}
}
}
async fn install_twin_certificate(
&self,
request: Request<InstallTwinCertificateRequest>,
) -> Result<Response<InstallTwinCertificateResponse>, Status> {
// The issuer a twin pins is the one it trusts for good: only from a fed key.
self.fed_caller(
request.metadata(),
crate::auth::BOOTSTRAP_INSTALL_TWIN_CERTIFICATE_METHOD,
METHOD_INSTALL_TWIN_CERTIFICATE,
)?;
let r = request.into_inner();
let port = u16::try_from(r.replication_port).map_err(|_| {
Status::invalid_argument(format!(
"replication_port {} is not a port",
r.replication_port
))
})?;
match self.ctx.control.install_twin_certificate(
&r.cert_pem,
&r.ca_pem,
port,
&r.replication_host,
) {
Ok(days_left) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_INSTALL_TWIN_CERTIFICATE, days_left, replication_port = port, decision = "allow", "THIS TWIN IS PAIRED FOR MUTUAL TLS: it pins its primary's issuer and follows it over TLS 1.3 from the next attempt");
Ok(Response::new(InstallTwinCertificateResponse {
days_left,
detail: format!("paired: {days_left} day(s) of certificate; the follower picks it up on its next attempt"),
}))
}
Err(err) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_INSTALL_TWIN_CERTIFICATE, decision = "deny", reason = %err, "Bootstrap.InstallTwinCertificate refused");
Err(Status::from(err))
}
}
}
async fn restore(
&self,
request: Request<tonic::Streaming<LogChunk>>,
) -> Result<Response<RestoreResponse>, Status> {
// **Who is streaming, proved, before the box gives up anything** (h-v1-6a,
// 2026-09-28). Checked against the keys this box was FED, never the identity
// table a fresh box does not have; the answer is the owner `restore_begin`
// records, so the resume branch serves only the key that started it.
let keys = self.ctx.control.restore_keys().map_err(Status::from)?;
let owner = crate::auth::authenticate_restore(self.ctx.config.auth, &keys, &self.ctx.replay, request.metadata(), crate::bus::now_unix_ms())
.inspect_err(|status| {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, decision = "deny", reason = %status.message(), "Bootstrap.Restore refused: the stream's credential");
})?;
let mut stream = request.into_inner();
// **The box's ONE intake for this stream** (restore-large-entry, 2026-10-07): the owner
// check and the next sequence (`restore_begin`), and on the server the staging area an
// entry larger than one piece is written into as it arrives. A stream the sender
// abandoned may still hold it for a moment — its last apply has not returned — so this
// waits a little for it instead of racing it over the same staged entry.
let waited = std::time::Instant::now();
let mut intake = loop {
match self.ctx.control.restore_intake(&owner) {
Ok(Some(intake)) => break intake,
Ok(None) if waited.elapsed() < RESTORE_INTAKE_WAIT => tokio::time::sleep(std::time::Duration::from_millis(100)).await,
Ok(None) => {
return Err(Status::unavailable(format!(
"another Bootstrap.Restore stream still holds this box after {} s (an abandoned stream whose last \
apply has not returned); nothing of this stream was taken — the next call resumes where the box is",
RESTORE_INTAKE_WAIT.as_secs()
)))
}
Err(err) => {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, owner = %owner, decision = "deny", reason = %err, "Bootstrap.Restore refused");
return Err(Status::from(err));
}
}
};
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, owner = %owner, decision = "allow", "Bootstrap.Restore: a fresh box takes its twin's log");
// The head the sender announced (the largest `restore_head` seen): a stream that ends
// below it ended SHORT, however cleanly, and the box refuses to finish.
let mut announced = 0u64;
// **Which log is being streamed** (2026-09-21, lane T4). Every chunk
// carries it, like `restore_head`; the first non-empty one is kept and
// `restore_finish` writes it, so the restored box IS that history and
// the twin it was built from keeps following it. A stream that changes
// it mid-way is two senders or two logs, and is refused rather than
// silently taking whichever arrived last.
let mut announced_id = String::new();
// **The room check, once, before the first byte of the restore lands.** A set that left
// four members and arrives at two may hold less than what is coming; without this the
// stream fills the volume and dies in the middle, an hour in, as an ENOSPC. The refusal
// says how much is short and leaves the box fresh.
let mut room_checked = false;
// Every way out of this loop drops the intake, and dropping it keeps what it staged of an
// unfinished entry: a stream that broke, one the sender cancelled at its deadline (tonic
// reads a client's RST_STREAM as a CLEAN end) and one refused below all leave the box
// holding every byte it took, durably.
// **A sender that went silent lets go of the box** (restore-large-entry): one stream at a
// time holds the intake, so a connection that died without a word (no RST reached this
// box) must not hold it for ever. A sender with nothing more to say ends its stream.
loop {
let chunk = match tokio::time::timeout(RESTORE_IDLE, stream.message()).await {
Ok(next) => match next? {
Some(chunk) => chunk,
None => break,
},
Err(_) => {
let (applied, _) = intake.counts();
let said = intake
.end_of_stream()
.err()
.map(|e| format!(": {e}"))
.unwrap_or_default();
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, applied, idle_s = RESTORE_IDLE.as_secs(), decision = "deny", "Bootstrap.Restore: the sender went silent; the stream is ended and the box stays restoring");
return Err(Status::deadline_exceeded(format!(
"no chunk arrived for {} s; this stream is ended and the box stays restoring — the next stream resumes where it is{said}",
RESTORE_IDLE.as_secs()
)));
}
};
announced = announced.max(chunk.restore_head);
let chunk_id = chunk.log_id.trim();
if !chunk_id.is_empty() {
if announced_id.is_empty() {
announced_id.push_str(chunk_id);
} else if announced_id != chunk_id {
return Err(Status::invalid_argument(format!(
"this stream began as log {announced_id} and is now log {chunk_id}: one restore is one history, \
and a box built out of two of them is not a restore of either"
)));
}
}
if !room_checked {
room_checked = true;
self.ctx.control.restore_room(chunk.restore_bytes).map_err(|err| {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, announced_bytes = chunk.restore_bytes, decision = "deny", reason = %err, "Bootstrap.Restore refused for room; nothing of the stream was written");
Status::from(err)
})?;
}
intake.take(&chunk).map_err(Status::from)?;
}
let (applied, lost) = intake.counts();
if let Err(err) = intake.end_of_stream() {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, applied, decision = "deny", reason = %err, "Bootstrap.Restore: the stream ended inside an entry; the box stays restoring");
return Err(Status::from(err));
}
drop(intake);
let head = self.ctx.control.restore_finish(announced, &announced_id).map_err(|err| {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, announced, applied, decision = "deny", reason = %err, "Bootstrap.Restore did not finish; the box stays restoring");
Status::from(err)
})?;
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = METHOD_RESTORE, head, applied, lost, decision = "allow", "Bootstrap.Restore finished; the box must restart");
Ok(Response::new(RestoreResponse {
head_seq: head,
applied,
lost,
restart_required: true,
detail: format!("restored to seq {head} ({applied} applied, {lost} replayed CAS lost); restart the box — it opens as the primary it was"),
}))
}
async fn status(
&self,
_request: Request<BootstrapStatusRequest>,
) -> Result<Response<BootstrapStatusResponse>, Status> {
// A read fault is UNAVAILABLE, never a cheerful "closed". An operator
// who is told the door is shut because the disk could not be read has
// been told the most dangerous possible lie about this server.
let state = self.ctx.control.bootstrap_state().map_err(Status::from)?;
let detail = describe(&state);
let (facts, facts_detail) = fresh_facts(self.ctx.control.as_ref(), state.window_open);
tracing::debug!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_STATUS,
window_open = state.window_open,
decision = "allow",
"Bootstrap.Status"
);
Ok(Response::new(BootstrapStatusResponse {
window_open: state.window_open,
bootstrapped: state.bootstrapped,
bootstrapped_at_unix_ms: state.bootstrapped_at_unix_ms,
bootstrapped_principal: state.bootstrapped_principal,
bootstrapped_fingerprint: state.bootstrapped_fingerprint,
principals: state.principals,
keys: state.keys,
detail,
serving_git: state.serving_git,
readiness: state.readiness,
build: state.build,
role: state.role,
// Read at answer time, and unauthenticated on purpose: see the
// field's note in bootstrap.proto.
server_unix_ms: estate_clock::now_unix_ms(),
clock_last: estate_clock::report::last().map(crate::service::admin::clock_sync),
clock_skew_syncs_suppressed: estate_clock::trigger::suppressed(),
primary: self
.ctx
.control
.primary_heartbeat(self.ctx.heartbeats.bounds().silent_after_ms),
facts,
facts_detail,
recharge_from: state.recharge_from,
restoring: state.restoring,
log_head_seq: state.log_head_seq,
restore_partial_seq: state
.restore_partial
.map(|(seq, _)| seq)
.unwrap_or_default(),
restore_partial_bytes: state
.restore_partial
.map(|(_, bytes)| bytes)
.unwrap_or_default(),
restore_snapshot_resumable: state.restore_snapshot_resumable,
restore_snapshot_id: state
.restore_snapshot
.as_ref()
.map(|s| s.0.clone())
.unwrap_or_default(),
restore_snapshot_items: state
.restore_snapshot
.as_ref()
.map(|s| s.1)
.unwrap_or_default(),
restore_snapshot_bytes: state
.restore_snapshot
.as_ref()
.map(|s| s.2)
.unwrap_or_default(),
restore_snapshot_whole: state.restore_snapshot.as_ref().is_some_and(|s| s.3),
break_glass_open: self.break_glass_open()?,
}))
}
/// **The runtime growth's phase, without a credential.** The one reader
/// `Admin.GrowStatus` also answers from; here for the actor outside the
/// box (T6 in `DATA-SET-GROWTH-FLOW.md`), which owns the volumes and has
/// nothing to sign with while the identity table is off the stripe — or,
/// after a crash in that window, while there is no identity table to
/// sign against at all.
///
/// A read fault is UNAVAILABLE here as on `Status`: an outside actor told
/// `idle` because the journal could not be read would leave the volumes
/// detached.
async fn grow_status(
&self,
_request: Request<GrowStatusRequest>,
) -> Result<Response<GrowStatusResponse>, Status> {
let status = self.ctx.control.grow_status().map_err(Status::from)?;
tracing::debug!(
target: crate::auth::AUDIT_TARGET,
event = "grpc.call",
method = METHOD_GROW_STATUS,
phase = %status.phase,
decision = "allow",
"Bootstrap.GrowStatus"
);
Ok(Response::new(status))
}
}
/// **The machine, for the twin election, while the box is FRESH** (T17, 2026-09-21, for T15's
/// TD8). `xtask elect twin` ranks two boxes by cores, RAM, the data root's size and the
/// start-up disk test — facts that live on the authenticated `Admin.Capacity`. A fresh box
/// accepts nobody's key, and enrolling first is no way round it because `BecomeTwin` requires
/// a FRESH box: the election stopped with `elect/box-silent`.
///
/// So `Status` carries them while the window is open, and ONLY then. They come out of the
/// control plane's own `capacity` answer (asked for nothing: no namespace, no bytes), so the
/// numbers are exactly what `Admin.Capacity` would say — one source, never a second probe.
/// Nothing tenant-shaped is copied: no namespace, no data set, no root path, no verdict.
/// Once anybody has enrolled the facts are withheld and the sentence says where they are.
pub(crate) fn fresh_facts(
control: &dyn ControlPlane,
window_open: bool,
) -> (Option<gunnar_proto::BoxFacts>, String) {
if !window_open {
return (
None,
"withheld: this box is enrolled, so its machine is read with a credential — \
Admin.Capacity carries the same numbers"
.to_owned(),
);
}
match control.capacity(&gunnar_proto::CapacityRequest::default()) {
Ok(c) => {
// `total_bytes` is a plain uint64 on Capacity and 0 when the statvfs failed
// (`CAPACITY_UNMEASURED`); here that failure is an ABSENCE, never a zero.
let measured = c.verdict != gunnar_proto::CapacityVerdict::CapacityUnmeasured as i32;
let data_total_bytes = (measured && c.total_bytes > 0).then_some(c.total_bytes);
(
Some(gunnar_proto::BoxFacts {
cpu_millicores_total: c.cpu_millicores_total,
ram_bytes_total: c.ram_bytes_total,
data_total_bytes,
disk_test: c.disk_test,
disk_seq_write_bps: c.disk_seq_write_bps,
disk_seq_read_bps: c.disk_seq_read_bps,
disk_sync_milliops_per_s: c.disk_sync_milliops_per_s,
disk_read_how: c.disk_read_how,
disk_reason: c.disk_reason,
machine_reason: c.machine_reason,
}),
if data_total_bytes.is_some() {
"this box is fresh: its machine is here for the twin election, without a \
credential, until the first enrolment"
.to_owned()
} else {
format!(
"this box is fresh, but its data root could not be measured, so \
data_total_bytes is absent: {}",
c.reason
)
},
)
}
// A plane that cannot answer Capacity cannot answer this either; said, not zeroed.
Err(err) => (
None,
format!("this box is fresh, but its machine could not be read: {err}"),
),
}
}
impl BootstrapService {
/// **The break-glass token is still the first admin's way in** (RA5-14): no server
/// admin holds a passkey here. gunnar's state, so a console with a new store (a re-lay, a
/// lost redb) or a first admin made another way reads the same. `None`: a plane that keeps
/// no web credentials (it cannot say); a read fault is UNAVAILABLE, as on `Status` itself:
/// never "spent" or "open" for a disk that did not read.
fn break_glass_open(&self) -> Result<Option<bool>, Status> {
use crate::seam::ControlError;
let held = match self.ctx.control.credentials(None) {
Ok(held) => held,
Err(ControlError::Unimplemented(_)) => return Ok(None),
Err(e) => return Err(Status::from(e)),
};
let mut people: Vec<&str> = held.iter().map(|c| c.principal_id.as_str()).collect();
people.sort_unstable();
people.dedup();
for id in people {
if self
.ctx
.control
.may(
id,
&crate::auth::Access::Server,
gunnar_proto::GrantLevel::Admin,
)
.map_err(Status::from)?
{
return Ok(Some(false));
}
}
Ok(Some(true))
}
/// [`crate::auth::authenticate_bootstrap`] against this box's fed keys, audited.
fn fed_caller(
&self,
metadata: &tonic::metadata::MetadataMap,
method: &str,
audited: &str,
) -> Result<String, Status> {
let keys = self.ctx.control.restore_keys().map_err(Status::from)?;
crate::auth::authenticate_bootstrap(self.ctx.config.auth, method, &keys, &self.ctx.replay, metadata, crate::bus::now_unix_ms()).inspect_err(|status| {
tracing::warn!(target: crate::auth::AUDIT_TARGET, event = "grpc.call", method = audited, decision = "deny", reason = %status.message(), "{audited} refused: the caller's credential");
})
}
}