Skip to content

Commit 0402069

Browse files
committed
feat: address review feedback
1 parent dfb525e commit 0402069

5 files changed

Lines changed: 227 additions & 47 deletions

File tree

docs/METRICS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ submitting health events naming another node. See
168168
|------------|------|--------|-------------|
169169
| `platform_connector_auth_decisions_total` | Counter | `decision` | Health event batches by the node scope granted to the caller. Values: `node_local`, `cross_node` |
170170
| `platform_connector_auth_node_claim_total` | Counter | `result` | Whether an authenticated caller's token carried a node claim. `verified`: it carried one and it named this node. `absent`: it carried none, so the caller was pinned to this node — the same scope a tokenless caller gets. |
171-
| `platform_connector_auth_violations_total` | Counter | `reason` | Health event batches that violated the node-binding rule. Under `global.platformConnectorAuth.mode: enforce` (default) these are rejected; under `mode: audit` they are recorded here but let through. Reasons: `node_mismatch` (a node-local caller named a different node), `node_claim_mismatch` (the token was issued on another node — applies to every caller, allowlisted or not), `unbound_cross_node_token` (an allowlisted caller presented a token bound to no pod, e.g. from `kubectl create token`), `cross_node_claim_absent` (pod-bound, but the pod was never scheduled), `missing_node_name` (no node name on the event and none could be stamped), `token_invalid` (TokenReview rejected the token), `malformed_credentials` (the authorization header was duplicated or did not use the Bearer scheme). Three further reasons mean **no verdict could be reached**, not that the caller was rejected: `validator_unavailable` (the API server was unreachable), `validator_timeout` (the caller gave up or its deadline passed) and `validator_error` (an unexpected failure, typically this component's own RBAC to create TokenReviews). With `failOpenOnUnavailable: true`, `validator_unavailable` and `validator_timeout` still increment this counter but fall back to node-local scope instead of rejecting the request, in enforce mode too — an outage says nothing about the caller's credential. Exclude those three when alerting on suspected credential abuse — a control-plane outage increments them for every in-flight request and would otherwise look identical to an attack: `sum(rate(platform_connector_auth_violations_total{reason=~"node_mismatch\|token_invalid\|node_claim_mismatch\|unbound_cross_node_token\|malformed_credentials\|cross_node_claim_absent\|missing_node_name"}[5m]))` |
171+
| `platform_connector_auth_violations_total` | Counter | `reason` | Health event batches that violated the node-binding rule. Under `global.platformConnectorAuth.mode: enforce` (default) these are rejected; under `mode: audit` they are recorded here but let through. Reasons: `node_mismatch` (a node-local caller named a different node), `node_claim_mismatch` (the token was issued on another node — applies to every caller, allowlisted or not), `unbound_cross_node_token` (an allowlisted caller presented a token bound to no pod, e.g. from `kubectl create token`), `cross_node_claim_absent` (pod-bound, but the pod was never scheduled), `missing_node_name` (no node name on the event and none could be stamped), `token_invalid` (TokenReview rejected the token), `malformed_credentials` (the authorization header was duplicated or did not use the Bearer scheme). Three further reasons mean **no verdict could be reached**, not that the caller was rejected: `validator_unavailable` (the API server was unreachable), `validator_timeout` (the caller gave up or its deadline passed) and `validator_error` (an unexpected failure, typically this component's own RBAC to create TokenReviews). With `failOpenOnUnavailable: true`, `validator_unavailable` and `validator_timeout` still increment this counter but fall back to a degraded node-local scope instead of rejecting the request, in enforce mode too — an outage says nothing about the caller's credential. That scope is a guess, not a verified identity: an event with a blank or matching node name is accepted and stamped as usual, but one naming a different node is refused as retryable `Unavailable` rather than counted as `node_mismatch`, since the caller might really be an allowlisted cross-node publisher the outage prevented from being verified. `missing_node_name` is the one violation `mode: audit` does not let through — a blank node name from a cross-node caller is always rejected, in both modes, because nothing downstream can handle it. Exclude those three when alerting on suspected credential abuse — a control-plane outage increments them for every in-flight request and would otherwise look identical to an attack: `sum(rate(platform_connector_auth_violations_total{reason=~"node_mismatch\|token_invalid\|node_claim_mismatch\|unbound_cross_node_token\|malformed_credentials\|cross_node_claim_absent\|missing_node_name"}[5m]))` |
172172

173173
### Kubernetes Connector Metrics
174174

docs/configuration/authentication.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ validator failure (including `validator_unavailable` and
5050
request. `audit` changes what happens after a violation is detected, not
5151
whether requests are checked.
5252

53+
One rejection is not affected by `mode`: a cross-node caller's event with no
54+
node name (`missing_node_name`) is always rejected, in both `audit` and
55+
`enforce`. Nothing downstream can handle an empty node name, so forwarding
56+
that event would not be a useful preview of what `enforce` would do — it
57+
would just be an event `enforce` could never have produced, sitting in the
58+
datastore.
59+
5360
### `failOpenOnUnavailable`
5461

5562
Distinguishes a validator that never reached a verdict from one that reached
@@ -62,9 +69,22 @@ credential) does say something about the caller and is always rejected under
6269

6370
Defaults to `false`: an unreachable validator rejects the request, matching
6471
behaviour prior to this setting's introduction. Set `true` to fall back to
65-
node-local scope instead — the same treatment an anonymous caller gets
72+
a **degraded** node-local scope instead — a guess, not a verified identity
6673
so that a control-plane blip degrades publishers to their own node rather
67-
than blocking their health events outright.
74+
than blocking their health events outright:
75+
76+
- An event with a blank or matching node name is accepted and stamped exactly
77+
as a verified node-local caller's would be. This is the common case the
78+
setting exists for: most publishers present a token but leave the node name
79+
blank for platform-connector to fill in.
80+
- An event naming a *different* node is refused as retryable `Unavailable` —
81+
the same code the validator itself returned — rather than as
82+
`node_mismatch` / `PermissionDenied`. The caller might really be an
83+
allowlisted cross-node publisher the outage prevented from being verified,
84+
not an actual mismatch, so it is not counted as `node_mismatch` (one of the
85+
reasons an operator would alert on as suspected credential abuse) and not
86+
rejected in a way publishers treat as non-retryable; it is retried once the
87+
validator recovers instead of being dropped for good.
6888

6989
### `audience`
7090

@@ -156,7 +176,7 @@ cluster already uses.
156176
| `missing_node_name` | An event carried no node name and none could be stamped. |
157177
| `token_invalid` | TokenReview rejected the token. |
158178
| `malformed_credentials` | The authorization header was duplicated, or did not use the Bearer scheme. A *completely absent* header is not a violation — that caller is accepted and pinned to the connector's node. |
159-
| `validator_unavailable` / `validator_timeout` / `validator_error` | The API server could not be reached, or returned no identity. With `failOpenOnUnavailable: true`, `validator_unavailable` and `validator_timeout` still increment this counter but fall back to node-local scope instead of rejecting the request. |
179+
| `validator_unavailable` / `validator_timeout` / `validator_error` | The API server could not be reached, or returned no identity. With `failOpenOnUnavailable: true`, `validator_unavailable` and `validator_timeout` still increment this counter but fall back to a degraded node-local scope instead of rejecting the request — see [`failOpenOnUnavailable`](#failopenonunavailable) for how that scope treats a blank vs. a differently-named node. |
160180

161181
A healthy cluster reports zero violations. A sustained non-zero
162182
`validator_unavailable` is an API-server problem, not a caller problem.

docs/platform-connectors.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,15 @@ authenticated caller stays accepted for up to two minutes after its token
280280
expires or its pod is deleted. An unreachable API server is
281281
**retried inside the call** with exponential backoff for a few seconds before
282282
anything is surfaced. Past that the connector **fails closed** with
283-
`Unavailable`, which every publisher treats as retryable; tokenless callers
284-
never trigger a TokenReview at all, so custom monitors keep working with no
285-
API-server dependency whatsoever.
283+
`Unavailable` by default, which every publisher treats as retryable.
284+
`global.platformConnectorAuth.failOpenOnUnavailable: true` changes this: the
285+
caller falls back to a degraded node-local scope instead of being rejected —
286+
an outage says nothing about the caller's credential — with a blank or
287+
matching node name accepted and stamped as usual, and an event naming a
288+
different node kept retryable rather than silently accepted or counted as a
289+
mismatch. See [Authentication Configuration](configuration/authentication.md)
290+
for the detail. Tokenless callers never trigger a TokenReview at all, so
291+
custom monitors keep working with no API-server dependency whatsoever.
286292

287293
#### Configuration
288294

@@ -296,10 +302,15 @@ allowlist and the publishers' projected tokens cannot drift apart:
296302
| `tokenExpirationSeconds` | `3600` | Token lifetime. Must be a whole number in `600 <= x <= 2^32`; Kubernetes rejects anything else. |
297303
| `tokenMountPath` | `/var/run/secrets/nvsentinel/platform-connector` | Where publishers mount the token. |
298304
| `crossNodeServiceAccounts` | `[]` | **Additional** fully-qualified usernames allowed to name other nodes. The bundled monitors are derived from the release namespace for whichever are enabled — do not list them here. |
305+
| `mode` | `enforce` | `enforce` rejects a violating request. `audit` validates every request and still increments `platform_connector_auth_violations_total` by reason, but lets the request through instead of rejecting it — use it to roll node-binding out against real traffic, confirm the violation counters stay at zero, then switch to `enforce` with evidence. One exception applies in both modes: a cross-node caller's event with no node name is always rejected, since nothing downstream can handle it. |
306+
| `failOpenOnUnavailable` | `false` | When `true`, a validator that never reached a verdict (the API server was unreachable, or the call timed out) falls back to a degraded node-local scope instead of rejecting the request, since an outage says nothing about the caller's credential. A rejected credential (an invalid or malformed token) is always rejected regardless of this setting. |
299307

300-
There is no `mode` and no `requireNodeClaim`. A setting that is "on but
301-
not enforcing" cannot be reasoned about from its configuration alone, so
302-
enforcement is not separable from enablement.
308+
There is no `requireNodeClaim`: whether a claim is checked is not
309+
configurable, only what happens after a violation is found (`mode`) and how an
310+
unavailable validator is treated (`failOpenOnUnavailable`). See
311+
[Authentication Configuration](configuration/authentication.md) for the full
312+
detail on both, including how a degraded scope treats a blank vs. a
313+
differently-named node.
303314

304315
A **custom cluster-scoped monitor** needs three things: a projected token volume
305316
with the configured audience, its token path passed to the client dial, and its
@@ -384,7 +395,7 @@ Related metrics:
384395
| Metric | Labels | Meaning |
385396
|--------|--------|---------|
386397
| `platform_connector_auth_decisions_total` | `decision` | Batches by granted scope (`node_local`, `cross_node`) |
387-
| `platform_connector_auth_violations_total` | `reason` | Batches rejected by the interceptor |
398+
| `platform_connector_auth_violations_total` | `reason` | Batches that violated the node-binding rule. Rejected under `mode: enforce`; recorded but let through under `mode: audit`, except `missing_node_name`, which is always rejected. |
388399
| `platform_connector_auth_node_claim_total` | `result` | Whether the token carried a node claim (`verified`, `absent`) |
389400

390401
Not every `reason` means a caller was rejected. `validator_unavailable`,

0 commit comments

Comments
 (0)