Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions elements/oci/k0s-sysext.bst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ sources:
- kind: local
path: files/k0s/kiosk
directory: kiosk-src
- kind: local
path: files/k0s/kubeflex
directory: kubeflex-src
- kind: local
path: files/os/issue.d/40-kubestellar.issue
directory: issue-src
Expand Down Expand Up @@ -82,6 +85,11 @@ config:
# Stage Console kiosk proxy assets.
cp -a kiosk-src/. sysext/usr/share/k0s/kiosk/

# Stage KubeStellar first-boot credential helper.
mkdir -p sysext/usr/share/k0s/kubeflex
cp -a kubeflex-src/. sysext/usr/share/k0s/kubeflex/
chmod 0755 sysext/usr/share/k0s/kubeflex/generate-postgres-secret.sh

# Generate self-signed TLS certificate for local kiosk proxy
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout sysext/usr/share/k0s/kiosk/key.pem \
Expand Down
36 changes: 36 additions & 0 deletions files/k0s/kubeflex/generate-postgres-secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
# First-boot generator for the KubeStellar postgres superuser password.
#
# The postgres StatefulSet (20-postgres.yaml) reads its superuser password from
# the kubeflex-postgres Secret via secretKeyRef. This script creates that Secret
# once, with a random password, so no usable credential is committed to git or
# exposed in the pod spec (see projectbluefin/server#98).
#
# Idempotent: the password is generated only if the Secret already exists, so
# the initialized database stays accessible across re-boots. Run once per boot
# by k0s-first-boot.service, before k0s applies the manifests (its 15- name
# sorts before 20-postgres.yaml, so the Secret exists first).
set -euo pipefail

manifest_dir="${KUBEFLEX_MANIFEST_DIR:-/var/lib/k0s/manifests/kubestellar}"
secret_file="$manifest_dir/15-kubeflex-postgres-secret.yaml"

if [ -e "$secret_file" ]; then
exit 0
fi

mkdir -p "$manifest_dir"
password="$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | cut -c1-30)"

cat > "$secret_file" <<EOF
apiVersion: v1
kind: Secret
metadata:
name: kubeflex-postgres
namespace: kubeflex-system
type: Opaque
stringData:
password: "$password"
EOF

chmod 0600 "$secret_file"
8 changes: 7 additions & 1 deletion files/k0s/manifests/kubestellar/20-postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ spec:
env:
- name: POSTGRESQL_USERNAME
value: "postgres"
# Superuser password lives in the kubeflex-postgres Secret (see
# 15-kubeflex-postgres-secret.yaml, generated at first boot) so it is
# never committed to git or exposed in the pod spec. See #98.
- name: POSTGRESQL_PASSWORD
value: "kubeflex"
valueFrom:
secretKeyRef:
name: kubeflex-postgres
key: password
- name: POSTGRESQL_DATABASE
value: "postgres"
ports:
Expand Down
1 change: 1 addition & 0 deletions files/os/systemd/system/k0s-first-boot.service
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ExecStartPre=/usr/bin/test -e /var/lib/k0s/k0s.raw
ExecStart=/usr/bin/install -D -m 0644 /var/lib/k0s/k0s.raw /run/extensions/k0s.raw
ExecStart=/usr/bin/systemd-sysext merge
ExecStart=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/k0s-manifests.conf
ExecStart=/usr/share/k0s/kubeflex/generate-postgres-secret.sh
ExecStart=/usr/bin/systemctl daemon-reload
ExecStart=/usr/bin/systemctl enable --now k0scontroller.service
[Install]
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/test_k0s_manifests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from pathlib import Path

import os
import subprocess

import yaml

ROOT = Path(__file__).resolve().parents[2]


Expand Down Expand Up @@ -33,3 +38,55 @@ def test_k0s_manifest_files():
assert (ks_dir / "30-kubestellar-core.yaml").is_file()
assert (ks_dir / "40-kubestellar-console.yaml").is_file()
assert (ks_dir / "41-kubestellar-kiosk-proxy.yaml").is_file()


def test_postgres_password_not_hardcoded():
# #98: the postgres superuser password must not be committed to git in
# plaintext, and must not be the publicly documented kubeflex default.
manifest = ROOT / "files" / "k0s" / "manifests" / "kubestellar" / "20-postgres.yaml"
text = manifest.read_text()
assert "kubeflex" not in text.lower().replace("kubeflex-system", "").replace("kubeflex-postgres", "")
assert 'value: "kubeflex"' not in text
assert "POSTGRESQL_PASSWORD" in text


def test_postgres_password_from_secret():
# The StatefulSet reads the password from a Secret, not a plaintext env.
docs = list(yaml.safe_load_all((ROOT / "files" / "k0s" / "manifests" / "kubestellar" / "20-postgres.yaml").read_text()))
statefulset = next(d for d in docs if d and d.get("kind") == "StatefulSet")
container = statefulset["spec"]["template"]["spec"]["containers"][0]
env = {e["name"]: e for e in container["env"]}
assert "value" not in env["POSTGRESQL_PASSWORD"]
ref = env["POSTGRESQL_PASSWORD"]["valueFrom"]["secretKeyRef"]
assert ref == {"name": "kubeflex-postgres", "key": "password"}


def test_k0s_first_boot_generates_postgres_secret_before_k0s():
# The Secret must be staged before k0s applies the manifests, and the
# generated 15- file must sort before 20-postgres.yaml.
unit = ROOT / "files" / "os" / "systemd" / "system" / "k0s-first-boot.service"
text = unit.read_text()
lines = [l for l in text.splitlines() if l.startswith("ExecStart")]
gen = next((i for i, l in enumerate(lines) if "generate-postgres-secret.sh" in l), None)
k0s = next((i for i, l in enumerate(lines) if "k0scontroller.service" in l), None)
assert gen is not None, "first-boot service never runs the postgres secret generator"
assert k0s is not None, "first-boot service never starts k0scontroller"
assert gen < k0s, "postgres secret generator must run before k0s applies manifests"


def test_generate_postgres_secret_is_idempotent(tmp_path):
# Running the generator twice must not change an already-created password,
# so the initialized database stays accessible across re-boots.
script = ROOT / "files" / "k0s" / "kubeflex" / "generate-postgres-secret.sh"
env = dict(os.environ, KUBEFLEX_MANIFEST_DIR=str(tmp_path))
run = lambda: subprocess.run(["/bin/bash", str(script)], env=env, check=True, capture_output=True, text=True)
run()
secret_file = tmp_path / "15-kubeflex-postgres-secret.yaml"
assert secret_file.is_file()
secret = yaml.safe_load(secret_file.read_text())
assert secret["kind"] == "Secret"
assert secret["metadata"]["name"] == "kubeflex-postgres"
assert secret["stringData"]["password"]
first = secret_file.read_text()
run()
assert secret_file.read_text() == first, "password changed on re-run; DB would lose access"