Skip to content

Commit 9d42851

Browse files
authored
Remove the mTLS check and resumption change, as this was a red herring (#99)
Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent 10efb01 commit 9d42851

8 files changed

Lines changed: 83 additions & 114 deletions

File tree

.claude/CHANGELOG.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,32 @@ The format is based on the regulated environment requirements:
99

1010
---
1111

12-
## [2026-06-26 10:00] - Switch rustls crypto provider ring -> aws-lc-rs (fixes mTLS to k0smotron-hosted control planes)
12+
## [2026-06-27 14:00] - THE ACTUAL k0smotron taint/drain fix: NetworkPolicy egress + server-side-apply type meta
1313

1414
**Author:** Erick Bourgeois
1515

1616
### Changed
17-
- `Cargo.toml`: `kube` now uses `default-features = false` + `["…, "rustls-tls", "aws-lc-rs"]`
18-
instead of the default that pulls the `ring` crypto provider. `ring` leaves the lock; the
19-
rustls TLS stack now runs on aws-lc-rs (BoringSSL-derived). 682 lib tests pass, fmt/clippy clean.
20-
21-
### Why
22-
5-Spot's child client could not complete the mTLS TLS-1.3 handshake against a k0smotron-hosted
23-
control plane — it stalled after the client-certificate step and timed out
24-
(`client error (Connect) -> deadline has elapsed`), so node taints/drains never happened on the
25-
hosted-CP tier. Proven on a live cluster that `curl --cert/--key` (OpenSSL) and `client-go`
26-
(kubelet/kubectl) complete the *identical* handshake to the *same* endpoint in ~13ms, while
27-
kube-rs/rustls-with-`ring` hangs (captured via `rustls=trace`). aws-lc-rs shares OpenSSL/BoringSSL's
28-
crypto lineage, so it behaves like the clients that work.
29-
30-
### Build note
31-
CI builds **natively per arch** (`ubuntu-24.04` + `ubuntu-24.04-arm`, plain `cargo build --release`),
32-
so aws-lc-rs compiles natively (cmake + cc, present on GitHub runners) — no cross-toolchain and no
33-
OpenSSL runtime lib in the distroless image, i.e. it avoids the cross-compile problem that motivated
34-
`ring` in the first place.
17+
- `src/reconcilers/helpers.rs` (`apply_node_taints`): the server-side-apply patch was missing
18+
`apiVersion: v1` + `kind: Node`. SSA REQUIRES the object's type meta in the body; without it the
19+
API server rejects with `invalid object type: /, Kind=: BadRequest (400)` and the taint never lands.
20+
- `src/reconcilers/helpers_tests.rs`: the apply-taints test now asserts `apiVersion`+`kind` are in the
21+
patch body (would have caught this).
22+
- `deploy/deployment/networkpolicy.yaml`: egress allowed only `53` + `6443`, which **silently dropped
23+
every child-cluster connection at the network layer** — k0smotron's hosted CP is on the NodePort
24+
`apiPort` (default **30443**), not 6443. Added 30443 to the egress (with guidance for other ports).
25+
- `docs/src/security/index.md`, `docs/src/security/threat-model.md`: the egress-posture rows now
26+
state the policy permits the mgmt apiserver (6443) + child/k0smotron-hosted CPs (30443) + DNS,
27+
matching the updated NetworkPolicy.
28+
29+
### Why — and a correction
30+
On the k0smotron tier the child client could never taint/drain. Live diagnosis on the playground proved
31+
**two** independent root causes, both above were confirmed by experiment (delete the NetworkPolicy →
32+
connection succeeds; a properly-typed `kubectl apply --server-side` → taint lands). The earlier
33+
TLS-centric changes — `insecure-skip-tls-verify`, the `ring``aws-lc-rs` provider swap, and disabling
34+
rustls session resumption — were **misdiagnoses**: the `rustls=trace` "resumption stall" was the
35+
*management* client's normal TLS, while the child client was firewalled before it ever reached TLS. Those
36+
TLS experiments have been **reverted** (back to the default `ring` provider + `Client::try_from`);
37+
this NetworkPolicy + server-side-apply change is the real fix.
3538

3639
### Impact
3740
- [ ] Breaking change

Cargo.lock

Lines changed: 28 additions & 83 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,7 @@ path = "src/bin/spot_schedule_capital_markets.rs"
4545
# Controller. The feature is "unstable" in the SemVer sense (the API
4646
# may change between minor kube-runtime versions) but is the canonical
4747
# way to push external triggers into the controller loop.
48-
# TLS: use rustls with the aws-lc-rs crypto provider (BoringSSL-derived) instead of the
49-
# `ring` provider kube pulls by default. `ring` cannot complete the mTLS TLS-1.3 handshake
50-
# against a k0smotron-hosted control plane (it stalls after the client cert step), whereas
51-
# aws-lc-rs — same crypto lineage as OpenSSL/BoringSSL, which client-go and curl use
52-
# successfully against that exact endpoint — does. default-features=false drops the default
53-
# `ring`; we re-add `client` + `rustls-tls` explicitly alongside `aws-lc-rs`.
54-
kube = { version = "3.1", default-features = false, features = ["runtime", "derive", "client", "ws", "unstable-runtime", "rustls-tls", "aws-lc-rs"] }
48+
kube = { version = "3.1", features = ["runtime", "derive", "client", "ws", "unstable-runtime"] }
5549
kube-lease-manager = "0.11"
5650
k8s-openapi = { version = "0.27", features = ["latest", "schemars"] }
5751
tokio = { version = "1", features = ["full"] }

deploy/deployment/networkpolicy.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#
66
# Ingress: Only Prometheus (monitoring namespace) may scrape metrics on 8080.
77
# Kubelet health probes are allowed on 8081 from any source (k8s internal).
8-
# Egress: Only Kubernetes API server (6443) and DNS (53) are permitted.
9-
# All other egress is denied, preventing data exfiltration.
8+
# Egress: DNS (53) and Kubernetes API servers (6443 management + standard child
9+
# clusters, 30443 k0smotron-hosted control planes) are permitted. All other
10+
# egress is denied, preventing data exfiltration. Child clusters on other
11+
# API ports must add that port to the egress rule below.
1012
#
1113
# NIST SP 800-53 SC-7: Boundary Protection
1214
apiVersion: networking.k8s.io/v1
@@ -45,7 +47,19 @@ spec:
4547
- protocol: TCP
4648
port: 53
4749

48-
# Kubernetes API server
50+
# Kubernetes API servers — BOTH the management cluster AND any child/workload
51+
# cluster the controller drives (taint/drain in CAPI / k0smotron topologies).
52+
# * 6443 — the management apiserver (the kube API ClusterIP DNATs to it) and
53+
# standard child clusters.
54+
# * 30443 — a k0smotron-hosted control plane's default NodePort apiPort; the
55+
# child kubeconfig's server is `…:30443`, and the connection is
56+
# evaluated on that pod port, so it MUST be allowed here.
57+
#
58+
# ⚠ A child cluster on any OTHER API port must add it here, or the child-cluster
59+
# client is silently dropped at the network layer ("client error (Connect) ->
60+
# deadline has elapsed") and taints/drains never run.
4961
- ports:
5062
- protocol: TCP
5163
port: 6443
64+
- protocol: TCP
65+
port: 30443

docs/src/security/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Per-field validation status and downstream sinks for every attacker-controllable
3434
|---|---|---|
3535
| Non-root container, read-only rootfs, all caps dropped || `deploy/deployment/deployment.yaml` |
3636
| Least-privilege RBAC — explicit resources, no wildcards || `deploy/deployment/rbac/clusterrole.yaml` |
37-
| NetworkPolicy — egress to API server only || `deploy/deployment/networkpolicy.yaml` |
37+
| NetworkPolicy — egress to API servers only (mgmt 6443 + child/k0smotron 30443) and DNS || `deploy/deployment/networkpolicy.yaml` |
3838
| Admission validation — 13 CEL rules, `failurePolicy: Fail` || `deploy/admission/` |
3939
| Finalizer cleanup timeout (10 min) — prevents deletion hangs || `src/constants.rs` |
4040
| Cross-namespace resource creation prevented || `namespace` field removed from `EmbeddedResource` |

docs/src/security/threat-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ in `5spot-system`.
325325
|---|---|
326326
| Kubernetes ResourceQuota | Limit `ScheduledMachine` count per namespace (e.g., max 50) |
327327
| `ValidatingAdmissionPolicy` ✅ deployed 2026-04-08 | Validates `bootstrapSpec.apiVersion`, `infrastructureSpec.apiVersion`, `kind` fields, duration format, and day/hour item format at admission time — see `deploy/admission/` |
328-
| NetworkPolicy | Restrict controller pod egress to Kubernetes API server only |
328+
| NetworkPolicy | Restrict controller pod egress to the management API server (6443), child/k0smotron-hosted control planes (NodePort apiPort 30443), and DNS — all other egress denied |
329329
| Audit logging | Enable API server audit log at `RequestResponse` level for `scheduledmachines` resources |
330330
| RBAC for SM creation | Only grant `create` on `scheduledmachines` to trusted identities; do not grant to end users directly |
331331
| Secrets for bootstrap data | Move sensitive bootstrap config out of CR spec into Secrets; reference from spec |

src/reconcilers/helpers.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3485,7 +3485,12 @@ pub async fn apply_node_taints(
34853485
.collect();
34863486
let annotation_value = serde_json::to_string(&owned).unwrap_or_else(|_| "[]".to_string());
34873487

3488+
// Server-side apply REQUIRES the object's type meta in the body; without
3489+
// `apiVersion` + `kind` the API server rejects the patch with
3490+
// `invalid object type: /, Kind=: BadRequest (400)` and the taint never lands.
34883491
let patch = json!({
3492+
"apiVersion": "v1",
3493+
"kind": "Node",
34893494
"metadata": {
34903495
"annotations": {
34913496
APPLIED_TAINTS_ANNOTATION: annotation_value,

0 commit comments

Comments
 (0)