mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
A tenant verifying a MongoDB endpoint needs the CA certificate, and every Secret that holds one also holds a private key: the operator's <release>-ca-cert carries the CA key, the leaf <release>-ssl the server key. Granting either would hand over the key along with the anchor. Render a TenantProjection naming the source, so the CA-extraction controller can publish the key-free <release>.tenant-ca, and select that projection by label from the managed-service definition. The sentinel carries no ownerReferences: it is resolved back to its release through the label Flux stamps, and an owner reference short-circuits that walk and silently costs the tenant its anchor. The sentinel is ungated. The operator issues the certificate chain whatever the chart asks for, so the source exists for every release and there is no plaintext leg to skip. The source is <release>-ssl rather than <release>-ca-cert. It is the name this chart itself sets in spec.secrets.ssl, so the reference cannot drift from the CR; the operator writes it on both its cert-manager path and the self-signed fallback it uses when cert-manager is absent, where no <release>-ca-cert exists at all; and it holds the merged old and new CA through a rotation, where <release>-ca-cert holds only the new one and would stop verifying pods still serving the previous leaf. Withhold the key-bearing Secrets from the tenant explicitly as well. The label selector matches on no name, so a single mis-stamp would promote a private key; the exclude list is evaluated first and closes the enumerable set. Signed-off-by: Aleksei Sviridkin <f@lex.la>
145 lines
7.2 KiB
Bash
145 lines
7.2 KiB
Bash
#!/usr/bin/env bats
|
|
# Unit test: mongodb-rd cozyrds must deliver the TLS trust anchor without
|
|
# private key material.
|
|
#
|
|
# The PSMDB operator creates <release>-ca-cert holding the CA PRIVATE KEY and
|
|
# <release>-ssl / <release>-ssl-internal holding server private keys. The tenant
|
|
# must receive only the key-free <release>.tenant-ca projection, reached by
|
|
# label. These names are the contract between this file and the operator, and
|
|
# nothing else in the repo checks them at this level.
|
|
#
|
|
# The declaration that names the extraction source is a TenantProjection the
|
|
# chart renders, covered by packages/apps/mongodb/tests/tenant_projection_test.yaml.
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
|
|
COZYRDS="$REPO_ROOT/packages/system/mongodb-rd/cozyrds/mongodb.yaml"
|
|
# Bound explicitly rather than read from BATS_TEST_FILENAME inside a test body.
|
|
# These files run under hack/cozytest.sh, not bats: it executes each body with
|
|
# set -u and never exports that variable, so a bare reference aborts the test —
|
|
# and the runner stops at the first failure, taking every later test with it.
|
|
# $0 is not a usable fallback either, since it expands to the runner's own path
|
|
# and would silently scan the wrong file.
|
|
SELF="$REPO_ROOT/hack/check-mongodb-rd-secrets.bats"
|
|
|
|
@test "mongodb-rd checks use the set -e-safe negation form" {
|
|
# Guards the pattern itself, so the next negated assertion added here cannot
|
|
# be silently dead. `!`-prefixed commands are exempt from set -e.
|
|
# SELF is a hardcoded path and grep exits 2 on a missing file, which an `if`
|
|
# reads as simply false — so renaming this file would turn the guard green
|
|
# while it inspected nothing. Every other check here reads $COZYRDS through
|
|
# `grep -q` or a `$(yq ...)` assignment, both of which abort under set -e;
|
|
# this is the one input that can vanish quietly.
|
|
if [ ! -f "$SELF" ]; then
|
|
echo "SELF does not resolve ($SELF); this guard would be vacuous" >&2
|
|
exit 1
|
|
fi
|
|
# [[:space:]] rather than \s: the latter is a GNU/PCRE extension, not POSIX
|
|
# ERE, and degrades to a literal 's' on a BSD grep — which would make this
|
|
# guard vacuous, the one property a vacuity guard must not have.
|
|
if grep -nE '^[[:space:]]*![[:space:]]' "$SELF"; then
|
|
echo "Negated assertion above is exempt from set -e; use if/exit 1 instead" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds selects the key-free projection by label" {
|
|
grep -q 'internal.cozystack.io/tenant-ca: "true"' "$COZYRDS"
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds secrets.include path resolves" {
|
|
# Positive control for the two structural checks below. Both walk
|
|
# .spec.secrets.include, and yq returns 0 for a path that does not exist — so
|
|
# a rename of that field would leave them green while asserting nothing. The
|
|
# grep-based tests would not notice either, since the strings stay in the
|
|
# file. Deleting `include` outright is already caught by the label grep; this
|
|
# covers the rename.
|
|
n="$(yq '.spec.secrets.include // [] | length' "$COZYRDS")"
|
|
if [ "$n" -lt 2 ]; then
|
|
echo "spec.secrets.include has $n entries; the structural checks would be vacuous" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds has no catch-all secrets selector" {
|
|
# The mine this guards: an include entry with an empty matchLabels and no
|
|
# resourceNames. LabelSelectorAsSelector({}) resolves to labels.Everything()
|
|
# and a nil resourceNames matches any name, so such an entry promotes EVERY
|
|
# Secret in the namespace — including the key-bearing TLS Secrets — to the
|
|
# tenant. Checked structurally rather than by grep, because the correct
|
|
# selector being present says nothing about a second, wider one alongside it.
|
|
empty="$(yq '[.spec.secrets.include[]
|
|
| select((.matchLabels // {} | length) == 0 and (.resourceNames // [] | length) == 0)]
|
|
| length' "$COZYRDS")"
|
|
if [ "$empty" != "0" ]; then
|
|
echo "Found $empty secrets.include entry/entries matching every Secret in the namespace" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds does not hand the tenant any key-bearing secret by name" {
|
|
# resourceNames entries are name grants. The CA and leaf secrets each carry a
|
|
# private key, so neither may appear as one.
|
|
#
|
|
# Matched on the suffix over the parsed resourceNames lists, not on a literal
|
|
# spelling: the same object can be written "mongodb-{{ .name }}-ca-cert" or
|
|
# "{{ .release }}-ca-cert", and a grep pinned to either form misses the other.
|
|
# Walking the yq path also confines the check to grants, so a legitimate
|
|
# mention of one of these names elsewhere in the file cannot trip it.
|
|
leaks="$(yq '[.spec.secrets.include[].resourceNames[]?
|
|
| select(test("-(ca-cert|ssl|ssl-internal|ssl-old|ssl-internal-old)$"))]
|
|
| length' "$COZYRDS")"
|
|
if [ "$leaks" != "0" ]; then
|
|
echo "Found $leaks key-bearing secret(s) granted to the tenant by name" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds excludes every key-bearing secret" {
|
|
# The exclude list is the structural backstop behind the label-only include
|
|
# selector, and nothing else pins it: deleting it leaves every other check in
|
|
# this file green. Asserted positively — a count alone would go vacuous on a
|
|
# renamed path, the same way the include checks would without their own
|
|
# positive control.
|
|
for name in ca-cert ssl ssl-internal ssl-old ssl-internal-old \
|
|
percona-server-mongodb-users mongodb-encryption-key mongodb-keyfile s3-creds; do
|
|
hits="$(yq "[.spec.secrets.exclude[].resourceNames[]? | select(. == \"mongodb-{{ .name }}-$name\")] | length" "$COZYRDS")"
|
|
if [ "$hits" != "1" ]; then
|
|
echo "mongodb-{{ .name }}-$name is not excluded (found $hits entries)" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds excludes the operator-internal secrets" {
|
|
# These carry the internal- prefix rather than a release suffix, so they do
|
|
# not fit the loop above. Quoted: the template braces contain spaces, and an
|
|
# unquoted list item would word-split into "internal-mongodb-{{", ".name",
|
|
# "}}" and assert on three names that do not exist.
|
|
for name in "internal-mongodb-{{ .name }}" "internal-mongodb-{{ .name }}-users"; do
|
|
hits="$(yq "[.spec.secrets.exclude[].resourceNames[]? | select(. == \"$name\")] | length" "$COZYRDS")"
|
|
if [ "$hits" != "1" ]; then
|
|
echo "$name is not excluded (found $hits entries)" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
@test "mongodb-rd cozyrds still delivers the credentials secret" {
|
|
# The exclude list added alongside the trust anchor is evaluated BEFORE
|
|
# include and wins outright, so it now sits upstream of this pre-existing
|
|
# grant. Every other line in that block is about withholding a Secret, which
|
|
# makes adding this one to it a plausible edit — and one that would silently
|
|
# revoke the tenant's access to their own connection string. Assert both
|
|
# directions.
|
|
inc="$(yq '[.spec.secrets.include[].resourceNames[]? | select(. == "mongodb-{{ .name }}-credentials")] | length' "$COZYRDS")"
|
|
if [ "$inc" != "1" ]; then
|
|
echo "credentials is not granted via include (found $inc entries)" >&2
|
|
exit 1
|
|
fi
|
|
exc="$(yq '[.spec.secrets.exclude[].resourceNames[]? | select(. == "mongodb-{{ .name }}-credentials")] | length' "$COZYRDS")"
|
|
if [ "$exc" != "0" ]; then
|
|
echo "credentials appears in exclude, which wins over include" >&2
|
|
exit 1
|
|
fi
|
|
}
|