krabka_guard/lib.rs
1//! Administers krabka topic write freezes and break-glass proposals.
2//!
3//! A write freeze is a broker-owned state where the cluster is up, every read
4//! works, and the broker refuses every new client write to a topic. A
5//! break-glass proposal is a standing authorization that two different people
6//! agreed to, which a privileged transition spends. This is the operator's side
7//! of both: freeze a scope, read the registry back and prove it, lift a freeze
8//! with an approval, and open, approve, withdraw and read the proposals.
9//!
10//! It is one command for one incident. It is a library as well as a binary so
11//! tests call [`run_from_args`] in process: a test that spawns the binary needs
12//! a Cargo working tree to build it from, and a Bazel test sandbox has none.
13//! That is the same reason `krabka-barrier` and `krabka-format` are libraries.
14//!
15//! # The two properties that matter
16//!
17//! `--sign-with` never leaves the machine. It takes a PKCS#8 Ed25519 key file,
18//! builds the canonical signing bytes here, signs them here, and puts only the
19//! `key_id` and the detached signature on the wire. The private key never
20//! reaches a broker, so a broker cannot make a signature in an operator's name.
21//!
22//! `freeze list --verify-signatures` checks the registry here as well, against
23//! operator public keys on this machine. That makes the operator's own machine,
24//! and not the broker that served the rows, the thing that says the registry is
25//! authentic.
26//!
27//! # Exit codes
28//!
29//! A runbook branches on `$?`, so every number means one thing across this tool
30//! and `krabka-barrier`. See [`EXIT_REFUSED`], [`EXIT_UNREACHABLE`],
31//! [`EXIT_MISMATCH`], [`EXIT_NO_APPROVAL`] and [`EXIT_BAD_SIGNATURE`].
32//!
33//! # The api keys
34//!
35//! Every subcommand speaks one krabka-private api key, in the 1015 to 1019
36//! range. A JVM `AdminClient` cannot send those. The freeze is visible to the
37//! JVM tools another way: `DescribeConfigs` reports a read-only `write.freeze`
38//! key for every topic, so `kafka-configs --describe` shows it.
39
40use {self::command::dispatch, clap::Parser};
41
42mod broker_codes;
43mod cli;
44mod command;
45mod failure;
46mod report;
47#[cfg(test)]
48mod signed_approval_interop;
49pub mod signing;
50pub mod verify;
51
52#[cfg(test)]
53mod tests;
54
55pub use self::{
56 cli::{
57 Action, BreakGlassCommand, Cli, Command, FreezeCommand, FreezeSigningArgs, ScopeArgs,
58 action_name, pattern_name,
59 },
60 report::{code_name, exit_for_code},
61 verify::{CheckedEntry, Unproved, VerifyOutcome},
62};
63
64/// The exit code for a request the broker refused.
65pub const EXIT_REFUSED: i32 = 1;
66/// The exit code for a transport failure, where nothing is known about the
67/// request's outcome.
68pub const EXIT_UNREACHABLE: i32 = 2;
69/// The exit code for a registry the local trust set does not match.
70///
71/// It carries `krabka-barrier`'s meaning: the tool asked the cluster for
72/// something and what came back does not agree with what the operator holds. A
73/// registry entry that names an operator key this machine does not have is that
74/// disagreement, and it is not the same answer as a signature that failed. This
75/// one says the tool could not check.
76pub const EXIT_MISMATCH: i32 = 3;
77/// The exit code for an action that needs a break-glass approval which does not
78/// exist.
79///
80/// This is the code a runbook branches on. It separates "go and get a second
81/// person" from every other refusal.
82pub const EXIT_NO_APPROVAL: i32 = 4;
83/// The exit code for a signature that did not verify, or that the broker needed
84/// and did not get.
85///
86/// It keeps "the tool could not check" apart from "the tool checked and the
87/// answer is wrong". KFC-5's verifier draws the same distinction.
88pub const EXIT_BAD_SIGNATURE: i32 = 5;
89
90krabka_macros::parsed_cli_entrypoint!({
91 /// Run the tool from an argv-style iterator, returning its exit code.
92 ///
93 /// `0` means the broker accepted the request. `1` means it refused one, and the
94 /// reason is on stderr. `2` means the broker could not be reached, so nothing
95 /// is known about the outcome. `3` means the registry names an operator key
96 /// this machine does not hold. `4` means the action needs a break-glass
97 /// approval that does not exist. `5` means a signature did not verify.
98 ///
99 /// # Panics
100 ///
101 /// Panics if `argv` does not parse, which for a caller passing a literal
102 /// argument list is a bug in that list rather than a runtime condition.
103});
104
105/// Run one parsed command.
106pub async fn run(cli: Cli) -> i32 {
107 let client =
108 krabka_macros::connect_cli_client!(&cli.bootstrap_server, "krabka-guard", EXIT_UNREACHABLE);
109 match dispatch(&client, cli.command).await {
110 Ok(code) => code,
111 Err(failure) => {
112 eprintln!("{}", failure.message());
113 failure.exit_code()
114 }
115 }
116}