harmony-nats-callout becomes a deployable service, not just a library:
- New [[bin]] target with env+secret-file driven config and
SIGINT/SIGTERM-aware shutdown.
- Dockerfile (single-stage archlinux:base, non-root, matches
harmony-fleet-operator convention).
- Refactored handler into a pure `decide()` function so the entire
authorization decision tree is unit-testable without async-nats.
- New `roles` module with role resolution + a `validate_device_id`
security gate that rejects NATS subject metacharacters in device_id
(.>* whitespace) — closes a real escalation path through the
`{device_id}` placeholder in the per-device permissions block.
- Configurable role claim path + admin/device role names; admin wins
when both are present (privilege-escalation invariant).
57 unit tests cover every reachable branch of the security decision
tree; 4 e2e tests in nats/integration-test-callout exercise real NATS
in podman with: device pubsub on own subjects, cross-device subject
isolation, admin-can-read-anything, and JWT-without-role rejection.
harmony/src/modules/nats_auth_callout/:
- New `NatsAuthCalloutScore` deploys the callout as a K8s Deployment +
Secret. fsGroup + 0o440 secret mode so the non-root container can
read its mounted seed/password without leaving them in env vars.
- `render_auth_callout_block` helper produces the YAML for NATS Helm
`config.merge.authorization.auth_callout` so both halves stay in
sync.
examples/fleet_auth_callout/:
- `bring_up_stack()` orchestrates k3d -> Zitadel + Postgres ->
CoreDNS rewrite -> project + roles + machine users with JWT keys
-> NATS Helm with auth_callout block -> callout image build +
sideload -> NatsAuthCalloutScore deploy. Idempotent across re-runs
(issuer NKey persisted in a K8s secret so user JWTs survive
restarts).
- `mint_access_token()` RFC 7523 JWT-bearer client. Uses Host header
with port so Zitadel emits a matching issuer.
- main.rs prints URLs/creds/keyIds and waits for Ctrl-C.
- Three #[tokio::test] functions sharing one cluster via OnceCell:
admin_can_read_any_device_subject, device_can_only_access_own_subjects,
unknown_role_is_rejected. All green on real k3d.
47 lines
1.1 KiB
TOML
47 lines
1.1 KiB
TOML
[package]
|
|
name = "example-fleet-auth-callout"
|
|
edition = "2024"
|
|
version.workspace = true
|
|
readme.workspace = true
|
|
license.workspace = true
|
|
description = "End-to-end fleet IoT security model: Zitadel + NATS + auth callout on k3d"
|
|
|
|
[lib]
|
|
name = "example_fleet_auth_callout"
|
|
path = "src/lib.rs"
|
|
|
|
[[bin]]
|
|
name = "fleet-auth-callout"
|
|
path = "src/main.rs"
|
|
|
|
[[test]]
|
|
name = "security_model"
|
|
path = "tests/security_model.rs"
|
|
|
|
[dependencies]
|
|
harmony = { path = "../../harmony" }
|
|
harmony-k8s = { path = "../../harmony-k8s" }
|
|
harmony_types = { path = "../../harmony_types" }
|
|
k3d-rs = { path = "../../k3d" }
|
|
harmony-nats-callout = { path = "../../nats/callout" }
|
|
async-nats.workspace = true
|
|
nkeys = "0.4"
|
|
jsonwebtoken = "9"
|
|
reqwest = { workspace = true }
|
|
tokio = { workspace = true, features = ["full"] }
|
|
tokio-test.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
anyhow.workspace = true
|
|
tracing.workspace = true
|
|
tracing-subscriber.workspace = true
|
|
log.workspace = true
|
|
env_logger.workspace = true
|
|
futures-util.workspace = true
|
|
k8s-openapi.workspace = true
|
|
kube.workspace = true
|
|
base64 = "0.22"
|
|
tempfile.workspace = true
|
|
url.workspace = true
|
|
directories = "6.0.0"
|