Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6292ad6708 | |||
| 90d94b741d | |||
| fcc4997dee | |||
| b7d296f6e9 |
@@ -15,5 +15,5 @@ type: application
|
||||
# appVersion and image.tag in values.yaml no longer agree, and that is not an oversight:
|
||||
# image.tag stays "latest", which is what a local install actually pulls. appVersion is
|
||||
# metadata and drives nothing.
|
||||
version: 0.44.1
|
||||
appVersion: "v0.44.1"
|
||||
version: 0.45.1
|
||||
appVersion: "v0.45.1"
|
||||
|
||||
@@ -42,10 +42,29 @@ spec:
|
||||
- |
|
||||
SERVICE_URL="http://{{ include "terdut-server.fullname" . }}:{{ .Values.service.port }}"
|
||||
SECRET_NAME="{{ include "terdut-server.bootstrapSecretName" . }}"
|
||||
USERNAME="{{ .Values.bootstrap.username }}"
|
||||
EMAIL="{{ .Values.bootstrap.email }}"
|
||||
K8S_API="https://kubernetes.default.svc"
|
||||
SA_TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
||||
CA_CERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)"
|
||||
SECRETS="$K8S_API/api/v1/namespaces/$NAMESPACE/secrets"
|
||||
|
||||
# kapi METHOD URL [BODY]: sets CODE and BODY from the Kubernetes API.
|
||||
kapi() {
|
||||
_ctype="application/json"
|
||||
[ "$1" = "PATCH" ] && _ctype="application/merge-patch+json"
|
||||
if [ -n "$3" ]; then
|
||||
_resp=$(printf '%s' "$3" | curl -s -w "\n%{http_code}" -X "$1" "$2" \
|
||||
--cacert "$CA_CERT" -H "Authorization: Bearer $SA_TOKEN" \
|
||||
-H "Content-Type: $_ctype" -d @-)
|
||||
else
|
||||
_resp=$(curl -s -w "\n%{http_code}" -X "$1" "$2" \
|
||||
--cacert "$CA_CERT" -H "Authorization: Bearer $SA_TOKEN")
|
||||
fi
|
||||
CODE=$(echo "$_resp" | tail -1)
|
||||
BODY=$(echo "$_resp" | sed '$d')
|
||||
}
|
||||
|
||||
echo "Waiting for terdut-server to be ready..."
|
||||
RETRIES=60
|
||||
@@ -60,42 +79,86 @@ spec:
|
||||
fi
|
||||
echo "Server is ready."
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$SERVICE_URL/api/bootstrap" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"{{ .Values.bootstrap.username }}","email":"{{ .Values.bootstrap.email }}"}')
|
||||
# The Secret is the source of truth for the administrator's password: reuse
|
||||
# the one it holds, so recreating the database brings the same account back.
|
||||
PASSWORD=""
|
||||
EXISTS=0
|
||||
kapi GET "$SECRETS/$SECRET_NAME"
|
||||
if [ "$CODE" = "200" ]; then
|
||||
EXISTS=1
|
||||
PASSWORD=$(echo "$BODY" | grep -o '"password": *"[^"]*"' | head -1 | cut -d'"' -f4 | base64 -d)
|
||||
elif [ "$CODE" != "404" ]; then
|
||||
echo "Failed to read secret '$SECRET_NAME' (HTTP $CODE)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Written BEFORE the server is asked to create the account, so a crash in
|
||||
# between cannot leave an administrator whose password nobody has.
|
||||
GENERATED=0
|
||||
if [ -z "$PASSWORD" ]; then
|
||||
PASSWORD=$(head -c 256 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 32)
|
||||
if [ "${#PASSWORD}" -lt 32 ]; then
|
||||
echo "Failed to generate a password."
|
||||
exit 1
|
||||
fi
|
||||
GENERATED=1
|
||||
DATA=$(printf '"username":"%s","password":"%s"' "$USERNAME" "$PASSWORD")
|
||||
if [ "$EXISTS" = "1" ]; then
|
||||
kapi PATCH "$SECRETS/$SECRET_NAME" "{\"stringData\":{$DATA}}"
|
||||
else
|
||||
kapi POST "$SECRETS" "{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"name\":\"$SECRET_NAME\"},\"stringData\":{$DATA}}"
|
||||
fi
|
||||
case "$CODE" in 200|201) ;; *)
|
||||
echo "Failed to store the password in secret '$SECRET_NAME' (HTTP $CODE)."
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
RESPONSE=$(printf '{"username":"%s","email":"%s","password":"%s"}' "$USERNAME" "$EMAIL" "$PASSWORD" \
|
||||
| curl -s -w "\n%{http_code}" -X POST "$SERVICE_URL/api/bootstrap" \
|
||||
-H "Content-Type: application/json" -d @-)
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | head -1)
|
||||
RESP_BODY=$(echo "$RESPONSE" | head -1)
|
||||
|
||||
if [ "$HTTP_CODE" = "403" ]; then
|
||||
echo "Server already bootstrapped, nothing to do."
|
||||
# Somebody else made the first account. The Secret is only worth keeping
|
||||
# if its password signs in.
|
||||
LOGIN=$(printf '{"username":"%s","password":"%s"}' "$USERNAME" "$PASSWORD" \
|
||||
| curl -s -o /dev/null -w "%{http_code}" -X POST "$SERVICE_URL/api/login" \
|
||||
-H "Content-Type: application/json" -d @-)
|
||||
if [ "$LOGIN" = "200" ]; then
|
||||
echo "Server already bootstrapped; the password in '$SECRET_NAME' signs in."
|
||||
exit 0
|
||||
fi
|
||||
echo "Server already bootstrapped by something else; '$USERNAME' does not sign in with the password in '$SECRET_NAME'."
|
||||
if [ "$GENERATED" = "1" ]; then
|
||||
if [ "$EXISTS" = "1" ]; then
|
||||
kapi PATCH "$SECRETS/$SECRET_NAME" '{"data":{"username":null,"password":null}}'
|
||||
else
|
||||
kapi DELETE "$SECRETS/$SECRET_NAME"
|
||||
fi
|
||||
echo "Removed the password this run generated."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Bootstrap failed (HTTP $HTTP_CODE): $BODY"
|
||||
echo "Bootstrap failed (HTTP $HTTP_CODE): $RESP_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_KEY=$(echo "$BODY" | grep -o '"key":"[^"]*"' | cut -d'"' -f4)
|
||||
API_KEY=$(echo "$RESP_BODY" | grep -o '"key":"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo "Failed to extract API key from response."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Storing API key in secret '$SECRET_NAME'."
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X POST "$K8S_API/api/v1/namespaces/$NAMESPACE/secrets" \
|
||||
--cacert "$CA_CERT" \
|
||||
-H "Authorization: Bearer $SA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"%s"},"stringData":{"api-key":"%s"}}' "$SECRET_NAME" "$API_KEY")")
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Failed to create secret (HTTP $HTTP_CODE)."
|
||||
echo "Bootstrap succeeded. Storing the API key in secret '$SECRET_NAME'."
|
||||
kapi PATCH "$SECRETS/$SECRET_NAME" "{\"stringData\":{\"api-key\":\"$API_KEY\"}}"
|
||||
if [ "$CODE" != "200" ]; then
|
||||
echo "Failed to store the API key (HTTP $CODE)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Secret '$SECRET_NAME' created successfully."
|
||||
echo "Secret '$SECRET_NAME' holds username, password and api-key."
|
||||
{{- end }}
|
||||
|
||||
@@ -27,6 +27,12 @@ rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["create"]
|
||||
# Reading, filling in and (when the account turns out not to be ours) cleaning up the
|
||||
# one Secret by name; `create` cannot be restricted to a name.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["{{ include "terdut-server.bootstrapSecretName" . }}"]
|
||||
verbs: ["get", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
|
||||
@@ -160,9 +160,14 @@ oidc:
|
||||
# Backups are not this chart's business: Postgres is backed up where it runs,
|
||||
# through a k8up.io/backupcommand pg_dump annotation on the database pod itself.
|
||||
|
||||
# Creates the first administrator after install/upgrade, with a generated password, and keeps
|
||||
# the credentials in a Secret. Set enabled: false to create the first user yourself with
|
||||
# POST /api/bootstrap (or on an SSO-only install that wants no local account).
|
||||
bootstrap:
|
||||
enabled: true
|
||||
username: admin
|
||||
email: admin@example.com
|
||||
# secretName overrides the default of <fullname>-admin-key
|
||||
# secretName overrides the default of <fullname>-admin-key. It holds username, password and
|
||||
# api-key. The password is generated once and then reused: delete the Secret and the
|
||||
# database to start over.
|
||||
secretName: ""
|
||||
|
||||
+3
-1
@@ -35,7 +35,9 @@ than an `Ingress`. TLS is terminated at the gateway, so the server itself never
|
||||
| `networking.hostname` | `terdut.example.com` | Hostname the `HTTPRoute` serves |
|
||||
| `networking.listener` | `""` | Gateway listener (`sectionName`) to bind to. Empty attaches to every matching listener, **including plaintext HTTP** — set it to the HTTPS listener's name to serve TLS only |
|
||||
| `networking.servicePort` | `8080` | Port the route forwards to; keep in sync with `service.port` |
|
||||
| `bootstrap.enabled` | `true` | Runs a post-install hook that creates the first user and stores its API key in the `<release>-admin-key` Secret. Already-bootstrapped servers are left alone |
|
||||
| `bootstrap.enabled` | `true` | Runs a post-install/upgrade hook that creates the first administrator with a generated password and stores `username`, `password` and `api-key` in the `<release>-admin-key` Secret. The password is generated once and reused, so recreating the database brings the same account back. Already-bootstrapped servers are left alone. Set `false` to create the first user yourself with `POST /api/bootstrap` |
|
||||
| `bootstrap.username` / `bootstrap.email` | `admin` / `admin@example.com` | Account the hook creates |
|
||||
| `bootstrap.secretName` | `""` | Secret to keep the credentials in; empty means `<release>-admin-key` |
|
||||
| `database.dsn` | `""` | **Required.** Postgres DSN, with no password in it. The chart provisions no database |
|
||||
| `database.passwordSecret.name` | `""` | Secret supplying `PGPASSWORD`. With the Zalando postgres operator, the Secret it generates for the role |
|
||||
| `database.passwordSecret.key` | `password` | Key within that Secret |
|
||||
|
||||
@@ -101,5 +101,5 @@ A login expires after 10 minutes. `GET /api/auth/config` reports `device_login`.
|
||||
use) and password login is the way in. With `TERDUT_PASSWORD_LOGIN=false` that way
|
||||
is closed: set it back to `true`. The first administrator comes from the bootstrap
|
||||
endpoint, and stays a manual administrator that no group can revoke; on an SSO-only
|
||||
install set `bootstrap.enabled: false` in the chart if you don't want that account,
|
||||
or keep it and never give it a password.
|
||||
install set `bootstrap.enabled: false` in the chart if you don't want that account;
|
||||
left on, the chart creates it with a generated password kept in the `<release>-admin-key` Secret.
|
||||
|
||||
@@ -998,6 +998,8 @@ kbd {
|
||||
.admin-settings .setting-value { width: 5.5em; margin-right: 6px; }
|
||||
.admin-settings .setting-unit { max-width: 8em; }
|
||||
.admin-settings button[type="submit"] { margin-top: 12px; }
|
||||
/* The environment section follows the form in the same card; h3 has no margin of its own. */
|
||||
.admin-settings + h3 { margin-top: 24px; }
|
||||
.small { font-size: 13px; }
|
||||
|
||||
/* A name in an admin table is the way to that row's own page -- a person's or
|
||||
|
||||
@@ -139,7 +139,7 @@ function render() {
|
||||
if (!data) {
|
||||
clear(view(), error
|
||||
? h('div', { class: 'load-error', text: error })
|
||||
: h('div', { class: 'card' }, h('p', { class: 'muted', text: 'You are not in a team yet.' })));
|
||||
: h('div', { class: 'card card-pad' }, h('p', { class: 'muted', text: 'You are not in a team yet.' })));
|
||||
return;
|
||||
}
|
||||
clear(view(),
|
||||
|
||||
Reference in New Issue
Block a user