Skip to content

Commit 9a3fed9

Browse files
authored
feat(nats): publish ca.crt-only trust anchor to tenants (#3340)
## What this PR does Converges nats onto the key-free CA trust-anchor contract. A TLS-enabled nats release renders a `TenantProjection` sentinel naming the chart's cert-manager CA Secret as its source, and the nats ResourceDefinition selects the resulting `<release>.tenant-ca` object by label. The projection is built by the CA extraction controller in `internal/controller/cacert`, which parses the source certificate and re-encodes it into a single `ca.crt` key, so a tenant that enables TLS gets something to verify the server against without ever getting a private key. No key-bearing Secret is granted to a tenant, by name or by label. `packages/apps/nats/templates/certmanager.yaml` is not touched. An earlier draft of this change stamped publish labels onto the CA `Certificate`'s `secretTemplate`; the controller that shipped consumes a sentinel object instead, so the chart now declares its source rather than marking it. The functional change is one new template and one `matchLabels` entry in the ResourceDefinition. The sentinel is gated on the chart's effective TLS state, which is tri-state: left unset it follows `external`. That gate is the single divergence from the postgres implementation of the same contract, and it exists because the nats CA is only issued while TLS resolves true. An ungated sentinel would sit `Ready=False/SourceNotFound` forever on every plaintext release. The CA extraction controller is already on main, so this is live on merge rather than dormant until a follow-up lands. The `do-not-merge/hold` label predates that and no longer guards anything. ## Verified against a running cluster The premise this rests on is that cert-manager populates `ca.crt` on the Secret of a `selfSigned`, `isCA: true` Certificate. Nothing in this repository witnesses it: `hack/e2e-chainsaw/` has no nats suite, and nats appears in no e2e workflow, so every unit test here encodes the same assumption the chart encodes and the two cannot catch each other being wrong. I closed it against a live cluster instead, running cert-manager v1.20.2, the version this repo ships, on two independent nats releases: - `nats-<name>-ca`, the chart's CA Secret, holds `ca.crt`, `tls.crt` and `tls.key`. The declared source key exists. - `nats-<name>.tenant-ca`, the projection, holds exactly one key, `ca.crt`. - The projection's SHA-256 certificate fingerprint equals the source's, so it carries that CA and not some other certificate. - The projection carries `internal.cozystack.io/tenant-ca: "true"` and `internal.cozystack.io/tenantresource: "true"`. The second is stamped by the lineage webhook, computed from the ResourceDefinition selector this PR adds, so its presence means the whole chain held. - `kubectl get tenantsecret nats-<name>-ca` returns NotFound. The key-bearing Secret is not served through the tenant API. What is still missing is an automated regression witness for that premise. Adding one means a new chainsaw suite for nats, which has no e2e coverage of any kind today, and that is a larger change than this one. ## What the tests pin 61 helm-unittest cases and 3 bats cases. The sentinel's absence of `ownerReferences` (an owner would short-circuit the lineage walk before it reaches the Flux label the attribution depends on), the length of `spec.projections` as well as its single entry (the controller refuses a multi-entry sentinel, so a second entry is a silent loss of the anchor rather than a leak), both implied legs of the tri-state TLS gate, and `notExists` on either visibility label for both key-bearing Certificates. The bats guard covers the two grant surfaces nothing else asserts on. The ResourceDefinition is rendered, by the `nats-rd` chart inlining `cozyrds/*`, but that chart has neither a tests directory nor the `test` target `hack/helm-unit-tests.sh` looks for, and no `packages/system/*-rd` package does. The dashboard Role is rendered by a chart that has both, and no suite under `packages/apps/nats/tests/` asserts on it. Both are compared whole rather than filtered down to the entries that mention a Secret, because every such filter admits the spelling it did not anticipate. A name whitelist passes an added label selector, and `controller.cert-manager.io/fao` is stamped on both key-bearing Secrets, so a selector on it is a one-line grant of the CA private key. On the Role side, a rule with no `resourceNames` names no Secret, a rule with `resources: ["*"]` names no resource, and a ClusterRole is not a Role, while all three grant read access to the CA key. Comparing the rendered output whole needs no filter and leaves no spelling to find. ## Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff, file by file. No trigger fires, and no follow-up PR is owed. `packages/apps/nats/templates/tenant-projection.yaml` is a new template and `packages/apps/nats/tests/` holds chart tests. No package is added, renamed or removed; `values.yaml` and `values.schema.json` are untouched, so neither the hand-written Terraform schema, its version enums, nor its restated defaults have anything to track. `kind`, `plural` and `release.prefix` are unchanged, and no output Secret or Service is renamed, so the provider's concatenated name lookups still resolve. `packages/system/nats-rd/cozyrds/nats.yaml` is edited in place rather than renamed, so the ccp external-app skill's dependency-contract anchor still resolves. The provider offers no typed access to the new projection, which is the position it already holds for every Secret it does not model, and not a break. `packages/apps/nats/README.md` is covered by the one automated path that does exist: `nats` is present in the website `Makefile` `APPS` list, so the docs bot regenerates the managed-apps reference page from this `README.md` on the next stable tag. This is the silent-skip failure mode the trigger map warns about, so I checked the list rather than assuming; an app missing from it gets no page at all, which is what happened to `opensearch`. `hack/check-nats-rd-secrets.bats` is a new file, not a move or a rename, and it changes no make target's contract: `Makefile` globs `hack/*.bats` into `BATS_UNIT_FILES` automatically. The ccp skills anchor on `hack/package.mk` and `hack/common-envs.mk`, neither of which is touched. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: Part of #2814. ### Release note ```release-note feat(nats): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret ```
2 parents 9ddaefc + 286b31c commit 9a3fed9

6 files changed

Lines changed: 343 additions & 0 deletions

File tree

‎hack/check-nats-rd-secrets.bats‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env bats
2+
# Unit test: the nats trust anchor reaches tenants as a key-free projection, and
3+
# the two key-BEARING Secrets the chart creates never do.
4+
#
5+
# The chart issues nats-<name>-ca (CA certificate AND CA private key, the latter
6+
# under tls.key) and nats-<name>-tls (server certificate AND server private key).
7+
# Neither may be handed to a tenant: read access to the CA key would let the
8+
# holder issue certificates for anything rather than merely verify the server.
9+
#
10+
# What a client actually needs is ca.crt alone. That is delivered by the
11+
# CA-extraction controller as the key-free <release>.tenant-ca projection,
12+
# selected here by LABEL rather than by name. The label is stamped only on an
13+
# object the controller itself produced, whereas a name grant conveys whatever
14+
# happens to occupy the name.
15+
#
16+
# These assertions exist because both halves are one edit away from breaking
17+
# with a fully green chart suite, for two different reasons. The
18+
# ResourceDefinition is rendered, by the nats-rd chart inlining cozyrds/*, but
19+
# that chart carries neither a tests directory nor the test target
20+
# hack/helm-unit-tests.sh looks for, and no packages/system/*-rd package does.
21+
# The dashboard Role is rendered by a chart that has both, and no suite under
22+
# packages/apps/nats/tests/ asserts on it.
23+
#
24+
# The grants are asserted as WHITELISTS against rendered output rather than as
25+
# a blacklist of forbidden names over source text. A blacklist grepping for
26+
# "{{ .Release.Name }}-ca" is defeated by quoting the scalar, by $.Release.Name,
27+
# or by a fullname helper -- all of which grant the CA private key while the
28+
# guard stays green.
29+
30+
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
31+
COZYRDS="$REPO_ROOT/packages/system/nats-rd/cozyrds/nats.yaml"
32+
CHART="$REPO_ROOT/packages/apps/nats"
33+
34+
# The complete set of grants the tenant holds, asserted as one value rather than
35+
# per-dimension: a name whitelist alone passes an added label selector, and a
36+
# label whitelist alone passes an added name. Either dimension can reach a
37+
# key-bearing Secret -- controller.cert-manager.io/fao is stamped on both of
38+
# them, so a selector on it is a one-line grant of the CA private key.
39+
EXPECTED_RD_INCLUDE='[{"resourceNames":["nats-{{ .name }}-credentials"]},{"matchLabels":{"internal.cozystack.io/tenant-ca":"true"}}]'
40+
41+
# The dashboard template's entire RBAC output: every rule of the Role, and the
42+
# kinds it renders. Asserted whole rather than filtered down to the rules that mention
43+
# secrets, because every filter admits the spelling it did not anticipate: a rule
44+
# with no resourceNames names no Secret, a rule with resources ["*"] names no
45+
# resource, and a ClusterRole is not a Role. Each of those reaches both
46+
# key-bearing Secrets. There is no filter that is right for all of them, and one
47+
# comparison against the whole output needs none.
48+
EXPECTED_ROLE_RULES='[{"apiGroups":[""],"resources":["services"],"resourceNames":["nats-test"],"verbs":["get","list","watch"]},{"apiGroups":[""],"resources":["secrets"],"resourceNames":["nats-test-credentials"],"verbs":["get","list","watch"]},{"apiGroups":["cozystack.io"],"resources":["workloadmonitors"],"resourceNames":["nats-test"],"verbs":["get","list","watch"]}]'
49+
EXPECTED_RBAC_KINDS='["Role","RoleBinding"]'
50+
51+
# Subsumed by the whitelist below, which cannot match without this entry. Kept
52+
# because the runner stops at the first failing test, so this one fires first
53+
# and says what the loss costs rather than printing two JSON blobs to diff.
54+
@test "nats-rd cozyrds selects the key-free tenant-CA projection by label" {
55+
if [ ! -f "$COZYRDS" ]; then
56+
echo "ResourceDefinition not found at $COZYRDS -- did it move?" >&2
57+
exit 1
58+
fi
59+
60+
count=$(yq eval \
61+
'[.spec.secrets.include[] | select(.matchLabels."internal.cozystack.io/tenant-ca" == "true")] | length' \
62+
"$COZYRDS") || exit 1
63+
64+
if [ "$count" -lt 1 ]; then
65+
echo 'nats-rd does not select internal.cozystack.io/tenant-ca: "true"' >&2
66+
echo "Without it the tenant cannot read ca.crt and TLS verification is impossible." >&2
67+
exit 1
68+
fi
69+
}
70+
71+
@test "nats-rd cozyrds grants nothing beyond the credentials Secret and the trust anchor" {
72+
if [ ! -f "$COZYRDS" ]; then
73+
echo "ResourceDefinition not found at $COZYRDS -- did it move?" >&2
74+
exit 1
75+
fi
76+
77+
include=$(yq eval --output-format=json --indent=0 '.spec.secrets.include' "$COZYRDS") || exit 1
78+
79+
if [ "$include" != "$EXPECTED_RD_INCLUDE" ]; then
80+
echo "Unexpected nats-rd spec.secrets.include: $include" >&2
81+
echo "Expected exactly: $EXPECTED_RD_INCLUDE" >&2
82+
echo "nats-<name>-ca holds the CA private key and nats-<name>-tls the server key;" >&2
83+
echo "neither may be granted to a tenant, by name or by label." >&2
84+
exit 1
85+
fi
86+
}
87+
88+
# Asserted against RENDERED output, so the spelling used in the template --
89+
# quoted, helper-derived, or otherwise -- cannot smuggle a name past the check.
90+
@test "nats dashboard Role renders exactly the grants it is supposed to" {
91+
if [ ! -d "$CHART" ]; then
92+
echo "Chart not found at $CHART -- did it move?" >&2
93+
exit 1
94+
fi
95+
96+
# stderr is deliberately not discarded: when the render breaks, helm's own
97+
# message is the only thing that says why, and a CI red showing just
98+
# "helm template failed" sends the reader back here to rerun it by hand.
99+
rendered=$(helm template nats-test "$CHART" \
100+
--namespace tenant-test \
101+
--set '_cluster.cluster-domain=cozy.local' \
102+
--show-only templates/dashboard-resourcemap.yaml) || {
103+
echo "helm template failed for $CHART" >&2
104+
exit 1
105+
}
106+
107+
# eval-all aggregates the documents into one result, so the Role may sit at any
108+
# position among them.
109+
kinds=$(printf '%s\n' "$rendered" | yq eval-all --output-format=json --indent=0 \
110+
'[.kind]' -) || exit 1
111+
112+
if [ "$kinds" != "$EXPECTED_RBAC_KINDS" ]; then
113+
echo "Unexpected RBAC kinds rendered by the nats dashboard template: $kinds" >&2
114+
echo "Expected exactly: $EXPECTED_RBAC_KINDS" >&2
115+
echo "A ClusterRole here would grant across namespaces and is not covered by" >&2
116+
echo "the rule comparison below, which reads the namespaced Role." >&2
117+
exit 1
118+
fi
119+
120+
# Fails closed on an empty list: a Role that stopped granting anything, or a
121+
# render that silently produced nothing, does not match the expected rules.
122+
rules=$(printf '%s\n' "$rendered" | yq eval-all --output-format=json --indent=0 \
123+
'[select(.kind == "Role") | .rules[]]' -) || exit 1
124+
125+
if [ "$rules" != "$EXPECTED_ROLE_RULES" ]; then
126+
echo "Unexpected rules in the nats dashboard Role: $rules" >&2
127+
echo "Expected exactly: $EXPECTED_ROLE_RULES" >&2
128+
echo "nats-<release>-ca holds the CA PRIVATE KEY and nats-<release>-tls the" >&2
129+
echo "server private key. The trust anchor reaches the tenant as the key-free" >&2
130+
echo "<release>.tenant-ca projection via tenantsecrets, not by a Role grant." >&2
131+
exit 1
132+
fi
133+
}

‎packages/apps/nats/README.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,22 @@ Presets follow a cloud-style `<series>.<size>` naming convention. Five series co
5656

5757
See [`docs/operations/resource-presets.md`](../../../docs/operations/resource-presets.md) for the full size matrix and the legacy-to-instance-type mapping.
5858

59+
### tls
60+
61+
`tls.enabled` is tri-state. Left unset it follows `external`, so TLS is enabled automatically for a NATS instance exposed outside the cluster and disabled for one that is not. Setting it explicitly overrides that in either direction.
62+
63+
When TLS is on, the chart issues a self-signed CA and a server certificate from it, and NATS serves the client port on `:4222` with the server certificate.
64+
65+
**Retrieving the CA certificate** for client verification:
66+
67+
The trust anchor is published as `nats-<name>.tenant-ca`: an object holding `ca.crt` and nothing else, delivered to tenants through the `core.cozystack.io/tenantsecrets` API that the base tenant roles already grant. It exists only while TLS is on, because that is the only state in which the chart issues a CA to publish.
68+
69+
```bash
70+
kubectl --context <ctx> --namespace <tenant> \
71+
get tenantsecret nats-<name>.tenant-ca \
72+
--output jsonpath='{.data.ca\.crt}' | base64 --decode
73+
```
74+
75+
`nats-<name>.tenant-ca` is the only object that hands over the CA certificate without also handing over a private key, which is why it exists. The chart's own `nats-<name>-ca` Secret stores the CA **private key** under `tls.key` alongside the certificate — read access to it would let the holder issue certificates for anything, so it is never granted to a tenant. The same applies to `nats-<name>-tls`, which holds the server's private key.
76+
77+
It is reached through `tenantsecrets` rather than by reading the Secret directly, and that is deliberate: `tenantsecrets` surfaces only objects the platform has vouched for, whereas a direct grant on the name would convey whatever happens to occupy that name.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{- /* Gated on the same condition as certmanager.yaml: the source Secret only exists when TLS is on. */}}
2+
{{- /* cert-manager writes the certificate of a selfSigned isCA Certificate to ca.crt as well as to tls.crt, and ca.crt is the key the trust-anchor contract names. */}}
3+
{{- if eq (include "nats.tls.enabled" .) "true" }}
4+
---
5+
apiVersion: internal.cozystack.io/v1alpha1
6+
kind: TenantProjection
7+
metadata:
8+
name: {{ .Release.Name }}-ca
9+
spec:
10+
projections:
11+
- type: CACert
12+
sourceSecretName: {{ .Release.Name }}-ca
13+
sourceKey: ca.crt
14+
{{- end }}

‎packages/apps/nats/tests/certmanager_test.yaml‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,55 @@ tests:
138138
path: spec.issuerRef.name
139139
value: my-nats-ca
140140

141+
###################################################
142+
# Key-bearing Secrets stay invisible to tenants #
143+
###################################################
144+
145+
# Both Certificates below mint a private key: the CA Secret stores the CA key
146+
# under tls.key, the leaf Secret the server key. The nats ResourceDefinition
147+
# grants tenants read access to Secrets matching
148+
# internal.cozystack.io/tenant-ca, so only the controller-authored projection
149+
# may carry that label.
150+
#
151+
# A stamp here does not by itself reach a tenant today: tenantsecrets serves
152+
# only objects the lineage webhook marked tenantresource=true, and it resolves
153+
# lineage from ownerReferences or the Flux helm labels, neither of which a
154+
# cert-manager Secret carries while enableCertificateOwnerRef stays false.
155+
# These assertions become load-bearing the moment either of those changes, and
156+
# the second gate is not one this chart controls.
157+
#
158+
# Selected by the Secret each Certificate writes rather than by position: a
159+
# documentIndex would follow the slot, so reordering this template would let
160+
# a labelled key-bearing Certificate slide out from under the assertion and
161+
# pass green.
162+
- it: the key-bearing CA Secret is never marked tenant-readable
163+
set:
164+
tls:
165+
enabled: true
166+
external: false
167+
documentSelector:
168+
path: spec.secretName
169+
value: my-nats-ca
170+
asserts:
171+
- notExists:
172+
path: spec.secretTemplate.labels["internal.cozystack.io/tenant-ca"]
173+
- notExists:
174+
path: spec.secretTemplate.labels["internal.cozystack.io/tenantresource"]
175+
176+
- it: the key-bearing leaf Secret is never marked tenant-readable
177+
set:
178+
tls:
179+
enabled: true
180+
external: false
181+
documentSelector:
182+
path: spec.secretName
183+
value: my-nats-tls
184+
asserts:
185+
- notExists:
186+
path: spec.secretTemplate.labels["internal.cozystack.io/tenant-ca"]
187+
- notExists:
188+
path: spec.secretTemplate.labels["internal.cozystack.io/tenantresource"]
189+
141190
###################################################
142191
# SAN coverage (internal, tls.enabled=true) #
143192
###################################################
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
suite: nats TenantProjection sentinel
2+
3+
# The chart renders one TenantProjection per TLS-enabled release: the
4+
# declaration that tells the CA-extraction controller to lift ca.crt out of the
5+
# key-bearing <release>-ca into the tenant-readable <release>.tenant-ca
6+
# projection.
7+
#
8+
# The load-bearing invariant is the ABSENCE of ownerReferences. The sentinel must
9+
# carry none — it owns the projection, and it is itself owned only through the
10+
# helm.toolkit.fluxcd.io/name label Flux stamps, which is how the lineage webhook
11+
# resolves the application. An ownerReference here would short-circuit that walk
12+
# and silently break the tenant's read path (pkg/lineage/lineage.go).
13+
14+
release:
15+
name: my-nats
16+
namespace: tenant-test
17+
18+
templates:
19+
- templates/tenant-projection.yaml
20+
21+
tests:
22+
- it: renders exactly one TenantProjection named after the release
23+
set:
24+
tls:
25+
enabled: true
26+
asserts:
27+
- hasDocuments:
28+
count: 1
29+
- isKind:
30+
of: TenantProjection
31+
- equal:
32+
path: apiVersion
33+
value: internal.cozystack.io/v1alpha1
34+
- equal:
35+
path: metadata.name
36+
value: my-nats-ca
37+
38+
# The list length is asserted alongside the entry: hasDocuments counts
39+
# documents, not list entries, so a second entry naming tls.key as its source
40+
# renders inside this same document and leaves every other assertion here
41+
# untouched. The controller refuses a multi-entry sentinel outright, which
42+
# makes that a silent loss of the anchor rather than a leak.
43+
- it: declares exactly one projection, of the chart's CA
44+
set:
45+
tls:
46+
enabled: true
47+
asserts:
48+
- lengthEqual:
49+
path: spec.projections
50+
count: 1
51+
- equal:
52+
path: spec.projections[0].type
53+
value: CACert
54+
- equal:
55+
path: spec.projections[0].sourceSecretName
56+
value: my-nats-ca
57+
- equal:
58+
path: spec.projections[0].sourceKey
59+
value: ca.crt
60+
61+
# The one load-bearing invariant: an ownerReference here would short-circuit
62+
# the lineage walk before it reaches the helm-label fallback, so the tenant
63+
# would silently lose the anchor.
64+
- it: carries no ownerReferences, so the lineage walk reaches the helm label
65+
set:
66+
tls:
67+
enabled: true
68+
asserts:
69+
- notExists:
70+
path: metadata.ownerReferences
71+
72+
# Not an invariant — conformance to the shape the merged engines share. An
73+
# explicit namespace would pin the sentinel outside the release, and chart
74+
# labels would compete with the Flux label that is the whole attribution
75+
# path. Pinned separately so a failure here is not read as a lineage break.
76+
- it: omits namespace and chart-added labels, matching the shared shape
77+
set:
78+
tls:
79+
enabled: true
80+
asserts:
81+
- notExists:
82+
path: metadata.namespace
83+
- notExists:
84+
path: metadata.labels
85+
86+
# The nats CA exists only while TLS is on. An ungated sentinel would sit
87+
# Ready=False/SourceNotFound forever on every plaintext release.
88+
#
89+
# The gate must track the chart's effective TLS state, not the raw
90+
# tls.enabled value: leaving it unset means "follow external". A naive
91+
# `if .Values.tls.enabled` still passes the two explicit cases below while
92+
# silently publishing no anchor to exactly the releases most likely to need
93+
# one, so both implied legs are pinned too.
94+
- it: renders the sentinel when TLS is implied by external access
95+
set:
96+
external: true
97+
asserts:
98+
- hasDocuments:
99+
count: 1
100+
101+
- it: renders nothing when TLS is implied off by external being false
102+
set:
103+
external: false
104+
asserts:
105+
- hasDocuments:
106+
count: 0
107+
108+
- it: renders nothing when TLS is explicitly disabled despite external access
109+
set:
110+
external: true
111+
tls:
112+
enabled: false
113+
asserts:
114+
- hasDocuments:
115+
count: 0
116+
117+
- it: renders nothing when TLS is disabled
118+
set:
119+
tls:
120+
enabled: false
121+
asserts:
122+
- hasDocuments:
123+
count: 0

‎packages/system/nats-rd/cozyrds/nats.yaml‎

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

0 commit comments

Comments
 (0)