swarm-logs: authenticate as the agent with its own queue secret
swarm-logs reads `swarm/agents/<agent>/queue` from the swarm secret store with the agent's own store certificate (the read hive-agent makes for its queue connection) and presents it as `Bearer swarm-agent.<agent>.<secret>`, which the gateway's swarm-logs-auth checks. On a 401 it reads the secret again and retries once, so a secret re-minted between the read and the check does not fail the query. The OIDC mint against the hive's shared `hive-<h>-agent` client is gone from swarm-logs, along with the unused `HIVE_AGENT_OIDC_CA_FILE` anchor. logs.nix now gates on the store address instead of the queue's token endpoint and wraps the binary with the paths queue-identity.nix gives the harness; the host forwards the query URL wherever it forwards a store identity. The shared client is still delivered into containers; removing that is Part of #4459.
This commit is contained in:
parent
1f4736e79b
commit
0f53e9be6c
12 changed files with 260 additions and 331 deletions
|
|
@ -46,41 +46,29 @@ pub fn run(
|
|||
format: Format,
|
||||
out: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
// Minted per invocation, with the query URL as the audience and the
|
||||
// `authelia.bearer.authz` scope. All three are load-bearing: authelia
|
||||
// refuses a token carrying no audience at the authz endpoint the
|
||||
// gateway's `auth_request` calls, and the audience it checks is the URL
|
||||
// being requested — so the string sent here and the string requested
|
||||
// below must be one binding, which is why `Config` holds exactly one.
|
||||
//
|
||||
// The scope has to be ASKED for, not merely registered: the agent client
|
||||
// is granted `authelia.bearer.authz` by the `agentClients` entry in
|
||||
// `swarm-authelia.nix`, but an OAuth2 server issues no scope the client
|
||||
// never requested, and a scopeless token is refused at the authz endpoint
|
||||
// exactly as an unauthenticated one is — a bare nginx 401 with nothing in
|
||||
// it that names the scope. `swarm-otel.nix` records the same failure
|
||||
// against the collector's client, on the same string.
|
||||
let token = swarm_queue_client::mint_token_for_blocking(
|
||||
&cfg.queue,
|
||||
Some(&cfg.query_url),
|
||||
Some("authelia.bearer.authz"),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{}", swarm_queue_client::chain(&e)))
|
||||
.context("minting an access token for the swarm log store")?;
|
||||
|
||||
let http = build_http_client(cfg)?;
|
||||
let http = build_http_client()?;
|
||||
|
||||
let mut form = vec![("query", logsql.to_owned())];
|
||||
if let Some(limit) = limit {
|
||||
form.push(("limit", limit.to_string()));
|
||||
}
|
||||
let send = || -> Result<reqwest::blocking::Response> {
|
||||
let bearer = cfg
|
||||
.bearer()
|
||||
.context("reading this agent's credential for the swarm log store")?;
|
||||
http.post(&cfg.query_url)
|
||||
.bearer_auth(bearer)
|
||||
.form(&form)
|
||||
.send()
|
||||
.with_context(|| format!("querying the swarm log store at {}", cfg.query_url))
|
||||
};
|
||||
|
||||
let response = http
|
||||
.post(&cfg.query_url)
|
||||
.bearer_auth(&token)
|
||||
.form(&form)
|
||||
.send()
|
||||
.with_context(|| format!("querying the swarm log store at {}", cfg.query_url))?;
|
||||
let mut response = send()?;
|
||||
// A 401 can be a secret re-minted between the store read and the check,
|
||||
// so it is read again and the query sent once more.
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
response = send()?;
|
||||
}
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
|
|
@ -140,37 +128,13 @@ fn render(line: &str, format: Format) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// The HTTP client for the query itself.
|
||||
/// The HTTP client for the query itself, trusting the container's own roots.
|
||||
///
|
||||
/// Separate from the one `swarm-queue-client` builds for the token endpoint
|
||||
/// because they are two hosts — `auth.<swarm>` and `logs.<swarm>` — and the
|
||||
/// CA-trust knob is declared per endpoint. Same optional anchor, same reason:
|
||||
/// without it a swarm using its own CA fails at TLS with
|
||||
/// `invalid peer certificate: UnknownIssuer`, having never looked at the
|
||||
/// anchor sitting on the host.
|
||||
///
|
||||
/// No timeout, deliberately, where the token client has one: a query over a
|
||||
/// long retention window legitimately takes longer than any number that would
|
||||
/// be right for a token request, and a client-side cut would truncate a
|
||||
/// correct answer into a shorter one with no marker saying so.
|
||||
fn build_http_client(cfg: &Config) -> Result<reqwest::blocking::Client> {
|
||||
let mut builder = reqwest::blocking::Client::builder();
|
||||
if let Some(path) = &cfg.queue.ca_file {
|
||||
let pem = std::fs::read(path).with_context(|| {
|
||||
format!(
|
||||
"reading the log-store CA certificate from {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let cert = reqwest::Certificate::from_pem(&pem).with_context(|| {
|
||||
format!(
|
||||
"parsing the log-store CA certificate from {} as PEM",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
builder = builder.add_root_certificate(cert);
|
||||
}
|
||||
builder
|
||||
/// No timeout, deliberately: a query over a long retention window legitimately
|
||||
/// takes long, and a client-side cut would truncate a correct answer into a
|
||||
/// shorter one with no marker saying so.
|
||||
fn build_http_client() -> Result<reqwest::blocking::Client> {
|
||||
reqwest::blocking::Client::builder()
|
||||
.build()
|
||||
.context("building the log-store HTTP client")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue