mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 14:02:56 +09:00
users[].password is gone from the chart, its schema and the API type. Every application-user password is generated into <release>-credentials and kept across reconciles by lookup, so an upgraded release keeps the password it already has and a password left in values is ignored. The init-job mounts that Secret and applies each password with ALTER ROLE and a psql variable, so no password is rendered into the init-script Secret that Helm keeps in release history. While bootstrap is enabled the Secret is marked credentials-pending, and NOTES say the generated passwords apply once the init-job runs. Names reach superuser SQL and the init-script shell, so render-time guards reject a username, database or extension name outside [-._a-zA-Z0-9] or past the length PostgreSQL silently truncates: 63 bytes for a role or extension, 54 for a database, whose <db>_readonly role would otherwise truncate and fail the next run. A username over 63 bytes that the credentials Secret already carries still upgrades. users.postgres, the CNPG superuser, is rejected in every bootstrap state. Changing a database's key in values drops the database, so the reject message and the README give the rename that keeps the data: a platform administrator renames it and its roles as the CNPG superuser before the key changes. The init-job cannot hang its Helm hook: the readiness wait ends on elapsed time below activeDeadlineSeconds and the Job never retries, so a purged restore target fails with a diagnostic. The delete-users pass reads managed users from pg_shdescription instead of parsing the version-dependent \du+ layout, which on PostgreSQL 16 and later matched nothing and left a removed user able to log in; it compares names as CREATE ROLE stores them and reassigns a removed user's objects in every database before dropping it. A database without roles renders, and extension names are quoted for hyphenated names and folded to lowercase so they resolve as they did unquoted. A value that carries its own surrounding quotes, the only way a hyphenated extension rendered before, has that pair removed and keeps its case, so such a release still upgrades. The README reads a password with jq, since jsonpath splits a dotted key. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
247 lines
11 KiB
YAML
247 lines
11 KiB
YAML
suite: init-script rejects a username outside the safe charset
|
|
|
|
# The username becomes a Kubernetes Secret data key AND is interpolated into the
|
|
# init-script shell (pw=$(cat "/credentials/<user>")) and role SQL. A name with a
|
|
# shell metacharacter is a command-injection shape; the apiserver would reject it
|
|
# later as an invalid Secret key with an opaque error. The chart must reject it at
|
|
# render time, naming the offending username, so the metacharacter never reaches
|
|
# the script. Reverting the guard in init-script.yaml lets the render succeed,
|
|
# so the first test fails.
|
|
|
|
templates:
|
|
- templates/init-script.yaml
|
|
|
|
release:
|
|
name: pg-test
|
|
namespace: tenant-test
|
|
|
|
set:
|
|
_cluster:
|
|
scheduling: {}
|
|
|
|
tests:
|
|
- it: fails the render for a username carrying a shell metacharacter
|
|
set:
|
|
users:
|
|
'evil";id;echo "x': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "evil\";id;echo \"x": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
- it: fails the render for a username with a space
|
|
set:
|
|
users:
|
|
'a b': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "a b": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
# "." and ".." match the charset regex but are rejected by k8s IsConfigMapKey as
|
|
# Secret keys, so the guard rejects them explicitly (the length cap is a separate,
|
|
# PostgreSQL-driven concern pinned by the 63/64 boundary tests below). These pin
|
|
# the guard to the full IsConfigMapKey contract.
|
|
- it: fails the render for the reserved name "."
|
|
set:
|
|
users:
|
|
'.': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username ".": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
- it: fails the render for the reserved name ".."
|
|
set:
|
|
users:
|
|
'..': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "..": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
# IsConfigMapKey rejects ANY name starting with ".." (hasChDirPrefix), not just
|
|
# the literal "..", so "..foo" is charset-legal yet cannot install.
|
|
- it: fails the render for a name starting with ".."
|
|
set:
|
|
users:
|
|
'..foo': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "..foo": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
# A username is a PostgreSQL role name: CREATE ROLE silently truncates past 63
|
|
# bytes (NAMEDATALEN-1) with a NOTICE, not an error, so a 64-char name would be
|
|
# created as its 63-byte prefix and then dropped by the delete pass (catalog
|
|
# truncated != declared full name) in the same hook. The cap is 63, not the
|
|
# 253-char Secret-key limit. 64 chars fails; 63 renders. Reverting the cap to 253
|
|
# lets the 64-char case through.
|
|
- it: fails the render for a username of 64 characters (past the 63-byte role limit)
|
|
set:
|
|
users:
|
|
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
- it: renders normally for a username at the 63-character boundary
|
|
set:
|
|
users:
|
|
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': {}
|
|
documentSelector:
|
|
path: metadata.name
|
|
value: pg-test-credentials
|
|
asserts:
|
|
- matchRegex:
|
|
path: stringData["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
|
|
pattern: ^[a-zA-Z0-9]{16}$
|
|
|
|
- it: renders normally for names using the full safe charset
|
|
set:
|
|
users:
|
|
a-b.c_1: {}
|
|
documentSelector:
|
|
path: metadata.name
|
|
value: pg-test-credentials
|
|
asserts:
|
|
- matchRegex:
|
|
path: stringData["a-b.c_1"]
|
|
pattern: ^[a-zA-Z0-9]{16}$
|
|
|
|
# `postgres` is the CNPG-managed superuser and is forbidden as an app role. The
|
|
# rejection must not depend on bootstrap.enabled: while it is true the
|
|
# create-users block is gated off, so a restore target carrying users.postgres
|
|
# installs, and the restore controller's automatic bootstrap clear
|
|
# (disablePostgresAppBootstrap) then wedges the next reconcile against this fail
|
|
# — a state the controller records as Succeeded. Rejecting it regardless of
|
|
# bootstrap keeps the controller from converging a release into an unrenderable
|
|
# one. Moving the guard back inside the `not .Values.bootstrap.enabled` block
|
|
# lets the bootstrap case render, so this test fails.
|
|
- it: rejects users.postgres even while bootstrap.enabled is true
|
|
set:
|
|
bootstrap:
|
|
enabled: true
|
|
users:
|
|
postgres:
|
|
replication: false
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: '`users.postgres` is forbidden by policy. Use a different username.'
|
|
|
|
- it: rejects users.postgres on the normal (non-bootstrap) path too
|
|
set:
|
|
users:
|
|
postgres:
|
|
replication: false
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: '`users.postgres` is forbidden by policy. Use a different username.'
|
|
|
|
# The credentials Secret is gated on .Values.users, so a release installed with
|
|
# the default `users: {}` has none. Adding its FIRST user is an upgrade whose
|
|
# lookup is legitimately empty — indistinguishable at render time from a deleted
|
|
# Secret — so postgres must NOT reject an empty-lookup upgrade (that would hard-
|
|
# block the common first-user path). It mints instead; the post-upgrade init-job
|
|
# applies the password. Re-adding a `not $existingSecret` fail on IsUpgrade turns
|
|
# this red — the regression this pins (a userless release could no longer gain a
|
|
# user). MariaDB is the opposite case (its Secret always exists via root, and
|
|
# root desyncs permanently), so its guard stays; see mariadb secret.yaml.
|
|
- it: mints when the first user is added to a userless release on upgrade
|
|
release:
|
|
name: pg-test
|
|
namespace: tenant-test
|
|
upgrade: true
|
|
set:
|
|
users:
|
|
app: {}
|
|
documentSelector:
|
|
path: metadata.name
|
|
value: pg-test-credentials
|
|
asserts:
|
|
- matchRegex:
|
|
path: stringData.app
|
|
pattern: ^[a-zA-Z0-9]{16}$
|
|
|
|
# A username of 64+ bytes installed before the 63 cap: its credentials Secret
|
|
# key was written, while the role itself was truncated and dropped in the same
|
|
# hook. Failing the render on the next upgrade would wedge the whole release
|
|
# over a user that never worked, so on upgrade the cap applies only to a user
|
|
# the existing credentials Secret does not already carry, i.e. a NEW user.
|
|
# Applying the cap on upgrade regardless of the Secret makes the first case
|
|
# fail to render; dropping the Secret-key conjunct lets the second render, so
|
|
# it fails.
|
|
- it: renders on upgrade for a 64-character user the credentials Secret already carries
|
|
release:
|
|
name: pg-test
|
|
namespace: tenant-test
|
|
upgrade: true
|
|
kubernetesProvider:
|
|
scheme:
|
|
"v1/Secret":
|
|
gvr: {version: v1, resource: secrets}
|
|
namespaced: true
|
|
objects:
|
|
- apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: pg-test-credentials
|
|
namespace: tenant-test
|
|
data:
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ZXhpc3RpbmdwYXNzd29yZA==
|
|
set:
|
|
users:
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {}
|
|
documentSelector:
|
|
path: metadata.name
|
|
value: pg-test-credentials
|
|
asserts:
|
|
- equal:
|
|
path: stringData["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
|
|
value: existingpassword
|
|
|
|
- it: still fails on upgrade for a NEW 64-character user
|
|
release:
|
|
name: pg-test
|
|
namespace: tenant-test
|
|
upgrade: true
|
|
kubernetesProvider:
|
|
scheme:
|
|
"v1/Secret":
|
|
gvr: {version: v1, resource: secrets}
|
|
namespaced: true
|
|
objects:
|
|
- apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: pg-test-credentials
|
|
namespace: tenant-test
|
|
data:
|
|
app: ZXhpc3RpbmdwYXNzd29yZA==
|
|
set:
|
|
users:
|
|
app: {}
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|
|
|
|
# A fresh install has no release to protect, so the cap holds even when a stale
|
|
# credentials Secret left in the namespace happens to carry the key. Dropping
|
|
# the ".Release.IsInstall" conjunct lets the render succeed, so this fails.
|
|
- it: still fails on install for a 64-character user a stale Secret carries
|
|
kubernetesProvider:
|
|
scheme:
|
|
"v1/Secret":
|
|
gvr: {version: v1, resource: secrets}
|
|
namespaced: true
|
|
objects:
|
|
- apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: pg-test-credentials
|
|
namespace: tenant-test
|
|
data:
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: ZXhpc3RpbmdwYXNzd29yZA==
|
|
set:
|
|
users:
|
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {}
|
|
asserts:
|
|
- failedTemplate:
|
|
errorMessage: 'invalid username "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": a username may contain only letters, digits and the characters -._ , must not be "." or start with "..", and must be at most 63 characters (it is a PostgreSQL role name, which CREATE ROLE silently truncates past 63 bytes, and it becomes a Kubernetes Secret key)'
|