From b87f06f6ff2b638025d60a5aea8b07d50a544eff Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 15 Dec 2025 12:57:02 -0300 Subject: [PATCH] zot: add oauth proxy and user sync scripts --- scripts/crypto_wallet_monero_setup.fish | 5 +- scripts/crypto_wallet_sui_setup.fish | 2 +- scripts/gitea_cred_sync.sh | 92 ++++++++++++++++++++++ scripts/gitops_cred_sync.sh | 87 +++++++++++++++++++++ scripts/jenkins_cred_sync.sh | 94 +++++++++++++++++++++++ scripts/longhorn_volume_usage.fish | 2 +- scripts/nextcloud-maintenance.sh | 2 +- scripts/zot_cred_sync.sh | 76 ++++++++++++++++++ services/crypto/monerod/deployment.yaml | 2 +- services/crypto/xmr-miner/deployment.yaml | 2 +- services/gitea/deployment.yaml | 4 +- services/keycloak/deployment.yaml | 2 +- services/monitoring/dcgm-exporter.yaml | 2 +- services/oauth2-proxy/middleware.yaml | 2 +- services/pegasus/deployment.yaml | 2 +- services/pegasus/image.yaml | 2 +- services/zot/configmap.yaml | 4 +- services/zot/deployment.yaml | 34 +++++++- services/zot/ingress.yaml | 35 ++++++++- services/zot/kustomization.yaml | 3 + services/zot/middleware-ui.yaml | 10 +++ services/zot/oauth2-proxy-deployment.yaml | 83 ++++++++++++++++++++ services/zot/oauth2-proxy-service.yaml | 14 ++++ 23 files changed, 538 insertions(+), 23 deletions(-) create mode 100755 scripts/gitea_cred_sync.sh create mode 100755 scripts/gitops_cred_sync.sh create mode 100755 scripts/jenkins_cred_sync.sh create mode 100755 scripts/zot_cred_sync.sh create mode 100644 services/zot/middleware-ui.yaml create mode 100644 services/zot/oauth2-proxy-deployment.yaml create mode 100644 services/zot/oauth2-proxy-service.yaml diff --git a/scripts/crypto_wallet_monero_setup.fish b/scripts/crypto_wallet_monero_setup.fish index 86c746a..fc1bc16 100644 --- a/scripts/crypto_wallet_monero_setup.fish +++ b/scripts/crypto_wallet_monero_setup.fish @@ -372,8 +372,8 @@ function xmrwallet_bootstrap --description "Interactive setup of monero-wallet-r end # Use your private image by default (in Zot) - read -P "Container image for wallet RPC [registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1]: " image - if test -z "$image"; set image registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1; end + read -P "Container image for wallet RPC [cli.registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1]: " image + if test -z "$image"; set image cli.registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1; end _require "Container image" $image; or return 1 # --- Secrets (defaults: RPC user=wallet name, passwords auto if missing) @@ -1375,4 +1375,3 @@ function xmrwallet_help_detailed echo " Probes it via a temporary port-forward so it works from your workstation." echo " Set xmrwallet_SKIP_DAEMON_CHECK=1 to bypass the daemon probe (not recommended)." end - diff --git a/scripts/crypto_wallet_sui_setup.fish b/scripts/crypto_wallet_sui_setup.fish index 5883a7c..544ceaf 100644 --- a/scripts/crypto_wallet_sui_setup.fish +++ b/scripts/crypto_wallet_sui_setup.fish @@ -23,7 +23,7 @@ end # Default image chooser (you should override with your own multi-arch image) function _sui_default_image -a NET - echo registry.bstein.dev/infra/sui-tools:1.53.2 + echo cli.registry.bstein.dev/infra/sui-tools:1.53.2 end # Convert any string to a k8s-safe name (RFC-1123 label-ish) diff --git a/scripts/gitea_cred_sync.sh b/scripts/gitea_cred_sync.sh new file mode 100755 index 0000000..445a6b2 --- /dev/null +++ b/scripts/gitea_cred_sync.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Sync Keycloak users into Gitea local accounts (for CLI + tokens). +# Requires: curl, jq, kubectl. Expects a Keycloak client with realm-management +# permissions (manage-users) and a Gitea admin token stored in a secret. + +set -euo pipefail + +require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required binary: $1" >&2; exit 1; }; } +require curl; require jq; require kubectl + +: "${KEYCLOAK_URL:=https://sso.bstein.dev}" +: "${KEYCLOAK_REALM:=atlas}" +: "${KEYCLOAK_CLIENT_ID:?set KEYCLOAK_CLIENT_ID or export via secret}" +: "${KEYCLOAK_CLIENT_SECRET:?set KEYCLOAK_CLIENT_SECRET or export via secret}" +: "${GITEA_BASE_URL:=https://scm.bstein.dev}" +: "${GITEA_NAMESPACE:=gitea}" +: "${GITEA_TOKEN_SECRET_NAME:=gitea-admin-token}" +: "${GITEA_TOKEN_SECRET_KEY:=token}" +: "${DEFAULT_PASSWORD:=TempSsoPass!2025}" + +fetch_token() { + curl -fsS -X POST \ + -d "grant_type=client_credentials" \ + -d "client_id=${KEYCLOAK_CLIENT_ID}" \ + -d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \ + "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \ + | jq -r '.access_token' +} + +pull_users() { + local token="$1" + curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?max=500" \ + | jq -r '.[] | select(.enabled == true) | select(.username | startswith("service-account-") | not) | [.username, (.email // ""), (.firstName // ""), (.lastName // "")] | @tsv' +} + +get_gitea_token() { + if [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + echo "${GITEA_ADMIN_TOKEN}" + return + fi + kubectl -n "${GITEA_NAMESPACE}" get secret "${GITEA_TOKEN_SECRET_NAME}" -o "jsonpath={.data.${GITEA_TOKEN_SECRET_KEY}}" \ + | base64 -d +} + +user_exists() { + local token="$1" username="$2" + local code + code=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${token}" \ + "${GITEA_BASE_URL}/api/v1/admin/users/${username}") + [[ "${code}" == "200" ]] +} + +create_user() { + local token="$1" username="$2" email="$3" fname="$4" lname="$5" + local body status fullname + fullname="$(echo "${fname} ${lname}" | xargs)" + if [[ -z "${email}" ]]; then + email="${username}@example.local" + fi + body=$(jq -n --arg u "${username}" --arg e "${email}" --arg p "${DEFAULT_PASSWORD}" \ + --arg fn "${fullname}" '{username:$u, email:$e, password:$p, must_change_password:false, full_name:$fn}') + status=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" \ + -X POST \ + -d "${body}" \ + "${GITEA_BASE_URL}/api/v1/admin/users") + if [[ "${status}" == "201" ]]; then + echo "created gitea user ${username}" + elif [[ "${status}" == "409" ]]; then + echo "gitea user ${username} already exists (409)" >&2 + else + echo "failed to create gitea user ${username} (status ${status})" >&2 + fi +} + +main() { + local kc_token gitea_token + kc_token="$(fetch_token)" + gitea_token="$(get_gitea_token)" + + while IFS=$'\t' read -r username email fname lname; do + if user_exists "${gitea_token}" "${username}"; then + continue + fi + create_user "${gitea_token}" "${username}" "${email}" "${fname}" "${lname}" + done < <(pull_users "${kc_token}") +} + +main "$@" diff --git a/scripts/gitops_cred_sync.sh b/scripts/gitops_cred_sync.sh new file mode 100755 index 0000000..8f85caa --- /dev/null +++ b/scripts/gitops_cred_sync.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Ensure Keycloak users are in the GitOps admin group used by weave-gitops (cd.bstein.dev). +# Weave GitOps relies on OIDC; membership in the "admin" group maps to cluster-admin via RBAC. +# Requires: curl, jq. Needs a Keycloak client with realm-management (manage-users/groups). + +set -euo pipefail + +require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required binary: $1" >&2; exit 1; }; } +require curl; require jq + +: "${KEYCLOAK_URL:=https://sso.bstein.dev}" +: "${KEYCLOAK_REALM:=atlas}" +: "${KEYCLOAK_CLIENT_ID:?set KEYCLOAK_CLIENT_ID or export via secret}" +: "${KEYCLOAK_CLIENT_SECRET:?set KEYCLOAK_CLIENT_SECRET or export via secret}" +: "${GITOPS_GROUP:=admin}" +# Comma-separated usernames to sync; set SYNC_ALL_USERS=true to include all Keycloak users. +: "${TARGET_USERNAMES:=bstein}" +: "${SYNC_ALL_USERS:=false}" + +fetch_token() { + curl -fsS -X POST \ + -d "grant_type=client_credentials" \ + -d "client_id=${KEYCLOAK_CLIENT_ID}" \ + -d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \ + "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \ + | jq -r '.access_token' +} + +ensure_group() { + local token="$1" group="$2" id + id=$(curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/groups?search=${group}" \ + | jq -r --arg g "${group}" '.[] | select(.name==$g) | .id' | head -n1) + if [[ -n "${id}" ]]; then + echo "${id}" + return + fi + curl -fsS -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${group}\"}" \ + -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/groups" + # Fetch again to get id + curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/groups?search=${group}" \ + | jq -r --arg g "${group}" '.[] | select(.name==$g) | .id' | head -n1 +} + +user_id_by_name() { + local token="$1" username="$2" + curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?username=${username}" \ + | jq -r '.[0].id' +} + +add_user_to_group() { + local token="$1" user_id="$2" group_id="$3" username="$4" + if [[ -z "${user_id}" ]]; then + echo "user ${username} not found in Keycloak; skip" >&2 + return + fi + curl -fsS -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer ${token}" \ + -X PUT "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/${user_id}/groups/${group_id}" \ + | grep -qE '^(204|409)$' || echo "failed adding ${username} to group" >&2 +} + +main() { + local token group_id users=() + token="$(fetch_token)" + group_id="$(ensure_group "${token}" "${GITOPS_GROUP}")" + + if [[ "${SYNC_ALL_USERS}" == "true" ]]; then + readarray -t users < <(curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?max=500" \ + | jq -r '.[] | select(.enabled==true) | .username') + else + IFS=',' read -ra users <<< "${TARGET_USERNAMES}" + fi + + for user in "${users[@]}"; do + user="$(echo "${user}" | xargs)" + [[ -z "${user}" ]] && continue + add_user_to_group "${token}" "$(user_id_by_name "${token}" "${user}")" "${group_id}" "${user}" + done +} + +main "$@" diff --git a/scripts/jenkins_cred_sync.sh b/scripts/jenkins_cred_sync.sh new file mode 100755 index 0000000..d4919a1 --- /dev/null +++ b/scripts/jenkins_cred_sync.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Sync Keycloak users into Jenkins local accounts (for CLI/API use). +# Jenkins is OIDC-enabled, but local users can still be provisioned for tokens. +# Requires: curl, jq, kubectl. Needs Jenkins admin user+API token. + +set -euo pipefail + +require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required binary: $1" >&2; exit 1; }; } +require curl; require jq; require kubectl + +: "${KEYCLOAK_URL:=https://sso.bstein.dev}" +: "${KEYCLOAK_REALM:=atlas}" +: "${KEYCLOAK_CLIENT_ID:?set KEYCLOAK_CLIENT_ID or export via secret}" +: "${KEYCLOAK_CLIENT_SECRET:?set KEYCLOAK_CLIENT_SECRET or export via secret}" +: "${JENKINS_URL:=https://ci.bstein.dev}" +: "${JENKINS_NAMESPACE:=jenkins}" +: "${JENKINS_ADMIN_SECRET_NAME:=jenkins-admin-token}" +: "${JENKINS_ADMIN_USER_KEY:=username}" +: "${JENKINS_ADMIN_TOKEN_KEY:=token}" +: "${DEFAULT_PASSWORD:=TempSsoPass!2025}" + +fetch_token() { + curl -fsS -X POST \ + -d "grant_type=client_credentials" \ + -d "client_id=${KEYCLOAK_CLIENT_ID}" \ + -d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \ + "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \ + | jq -r '.access_token' +} + +pull_users() { + local token="$1" + curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?max=500" \ + | jq -r '.[] | select(.enabled == true) | select(.username | startswith("service-account-") | not) | [.id, .username, (.email // "")] | @tsv' +} + +get_admin_auth() { + local user token + if [[ -n "${JENKINS_ADMIN_USER:-}" && -n "${JENKINS_ADMIN_TOKEN:-}" ]]; then + echo "${JENKINS_ADMIN_USER}:${JENKINS_ADMIN_TOKEN}" + return + fi + user=$(kubectl -n "${JENKINS_NAMESPACE}" get secret "${JENKINS_ADMIN_SECRET_NAME}" -o "jsonpath={.data.${JENKINS_ADMIN_USER_KEY}}" | base64 -d) + token=$(kubectl -n "${JENKINS_NAMESPACE}" get secret "${JENKINS_ADMIN_SECRET_NAME}" -o "jsonpath={.data.${JENKINS_ADMIN_TOKEN_KEY}}" | base64 -d) + echo "${user}:${token}" +} + +get_crumb() { + local auth="$1" + curl -fsS -u "${auth}" "${JENKINS_URL}/crumbIssuer/api/json" | jq -r .crumb +} + +user_exists() { + local auth="$1" user="$2" + local code + code=$(curl -s -o /dev/null -w '%{http_code}' -u "${auth}" "${JENKINS_URL}/user/${user}/api/json") + [[ "${code}" == "200" ]] +} + +create_user() { + local auth="$1" crumb="$2" username="$3" email="$4" + local status + status=$(curl -s -o /dev/null -w '%{http_code}' \ + -u "${auth}" \ + -H "Jenkins-Crumb: ${crumb}" \ + -X POST \ + --data "username=${username}&password1=${DEFAULT_PASSWORD}&password2=${DEFAULT_PASSWORD}&fullname=${username}&email=${email}" \ + "${JENKINS_URL}/securityRealm/createAccountByAdmin") + + if [[ "${status}" == "200" || "${status}" == "302" ]]; then + echo "created jenkins user ${username}" + elif [[ "${status}" == "400" ]]; then + echo "jenkins user ${username} already exists (400)" >&2 + else + echo "failed to create jenkins user ${username} (status ${status})" >&2 + fi +} + +main() { + local kc_token auth crumb + kc_token="$(fetch_token)" + auth="$(get_admin_auth)" + crumb="$(get_crumb "${auth}")" + + while IFS=$'\t' read -r _ uid email; do + if user_exists "${auth}" "${uid}"; then + continue + fi + create_user "${auth}" "${crumb}" "${uid}" "${email}" + done < <(pull_users "${kc_token}") +} + +main "$@" diff --git a/scripts/longhorn_volume_usage.fish b/scripts/longhorn_volume_usage.fish index b2f9f7b..1d32ccd 100755 --- a/scripts/longhorn_volume_usage.fish +++ b/scripts/longhorn_volume_usage.fish @@ -1,6 +1,6 @@ #!/usr/bin/env fish -function pvc-usage --description "Show Longhorn PVC usage (human-readable) mapped to namespace/name" +function pvc-usage --description "Show Longhorn PVC usage mapped to namespace/name" begin kubectl -n longhorn-system get volumes.longhorn.io -o json \ | jq -r '.items[] | "\(.metadata.name)\t\(.status.actualSize)\t\(.spec.size)"' \ diff --git a/scripts/nextcloud-maintenance.sh b/scripts/nextcloud-maintenance.sh index e8ea18c..3dd3763 100755 --- a/scripts/nextcloud-maintenance.sh +++ b/scripts/nextcloud-maintenance.sh @@ -39,7 +39,7 @@ SITES=( "Jellyfin|https://stream.bstein.dev" "Gitea|https://scm.bstein.dev" "Jenkins|https://ci.bstein.dev" - "Zot|https://registry.bstein.dev" + "Zot|https://web.registry.bstein.dev" "Vault|https://secret.bstein.dev" "Jitsi|https://meet.bstein.dev" "Grafana|https://metrics.bstein.dev" diff --git a/scripts/zot_cred_sync.sh b/scripts/zot_cred_sync.sh new file mode 100755 index 0000000..e33c379 --- /dev/null +++ b/scripts/zot_cred_sync.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Sync Keycloak users into Zot htpasswd. +# Intended to be rendered into a ConfigMap/Job; keep real secrets out of git. + +set -euo pipefail + +require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required binary: $1" >&2; exit 1; }; } +require curl; require jq; require kubectl; require htpasswd + +: "${KEYCLOAK_URL:=https://sso.bstein.dev}" +: "${KEYCLOAK_REALM:=atlas}" +: "${KEYCLOAK_CLIENT_ID:?set KEYCLOAK_CLIENT_ID or export via secret}" +: "${KEYCLOAK_CLIENT_SECRET:?set KEYCLOAK_CLIENT_SECRET or export via secret}" +: "${ZOT_NAMESPACE:=zot}" +: "${HTPASSWD_SECRET_NAME:=zot-htpasswd}" +: "${DEFAULT_PASSWORD:=TempSsoPass!2025}" +: "${UI_PROXY_USER:=zot-ui-proxy}" +: "${UI_PROXY_PASSWORD:=TempSsoUiPass!2025}" + +fetch_token() { + curl -fsS -X POST \ + -d "grant_type=client_credentials" \ + -d "client_id=${KEYCLOAK_CLIENT_ID}" \ + -d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \ + "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \ + | jq -r '.access_token' +} + +pull_users() { + local token="$1" + curl -fsS -H "Authorization: Bearer ${token}" \ + "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?max=500" \ + | jq -r '.[] | select(.enabled == true) | select(.username | startswith("service-account-") | not) | .username' +} + +decode_secret_file() { + local ns="$1" name="$2" key="$3" out="$4" + if kubectl -n "${ns}" get secret "${name}" >/dev/null 2>&1; then + kubectl -n "${ns}" get secret "${name}" -o "jsonpath={.data.${key}}" | base64 -d > "${out}" || true + else + : > "${out}" + fi +} + +ensure_htpasswd_line() { + local user="$1" password="$2" file="$3" + if ! grep -q "^${user}:" "${file}" 2>/dev/null; then + htpasswd -nbB "${user}" "${password}" >> "${file}" + echo "added user ${user} to htpasswd" + fi +} + +main() { + local token tmp existing + tmp="$(mktemp)" + existing="$(mktemp)" + trap 'rm -f "${tmp}" "${existing}"' EXIT + + decode_secret_file "${ZOT_NAMESPACE}" "${HTPASSWD_SECRET_NAME}" htpasswd "${existing}" + cp "${existing}" "${tmp}" + + ensure_htpasswd_line "${UI_PROXY_USER}" "${UI_PROXY_PASSWORD}" "${tmp}" + + token="$(fetch_token)" + readarray -t users < <(pull_users "${token}") + for user in "${users[@]}"; do + ensure_htpasswd_line "${user}" "${DEFAULT_PASSWORD}" "${tmp}" + done + + kubectl create secret generic "${HTPASSWD_SECRET_NAME}" \ + --namespace "${ZOT_NAMESPACE}" \ + --from-file=htpasswd="${tmp}" \ + --dry-run=client -o yaml | kubectl apply -f - +} + +main "$@" diff --git a/services/crypto/monerod/deployment.yaml b/services/crypto/monerod/deployment.yaml index 1b98e47..8a913d1 100644 --- a/services/crypto/monerod/deployment.yaml +++ b/services/crypto/monerod/deployment.yaml @@ -35,7 +35,7 @@ spec: values: ["rpi4"] containers: - name: monerod - image: registry.bstein.dev/infra/monerod:0.18.4.1 + image: cli.registry.bstein.dev/infra/monerod:0.18.4.1 command: ["/opt/monero/monerod"] args: - --data-dir=/data diff --git a/services/crypto/xmr-miner/deployment.yaml b/services/crypto/xmr-miner/deployment.yaml index 178f2b2..dc72499 100644 --- a/services/crypto/xmr-miner/deployment.yaml +++ b/services/crypto/xmr-miner/deployment.yaml @@ -32,7 +32,7 @@ spec: values: ["rpi4"] containers: - name: monero-p2pool - image: registry.bstein.dev/infra/monero-p2pool:4.9 + image: cli.registry.bstein.dev/infra/monero-p2pool:4.9 imagePullPolicy: Always command: ["p2pool"] args: diff --git a/services/gitea/deployment.yaml b/services/gitea/deployment.yaml index 0508f57..d17a007 100644 --- a/services/gitea/deployment.yaml +++ b/services/gitea/deployment.yaml @@ -63,7 +63,7 @@ spec: --key "$CLIENT_ID" \ --secret "$CLIENT_SECRET" \ --auto-discover-url "$DISCOVERY_URL" \ - --scopes "openid profile email" \ + --scopes "openid profile email groups" \ --required-claim-name "" \ --required-claim-value "" \ --group-claim-name groups \ @@ -77,7 +77,7 @@ spec: --key "$CLIENT_ID" \ --secret "$CLIENT_SECRET" \ --auto-discover-url "$DISCOVERY_URL" \ - --scopes "openid profile email" \ + --scopes "openid profile email groups" \ --required-claim-name "" \ --required-claim-value "" \ --group-claim-name groups \ diff --git a/services/keycloak/deployment.yaml b/services/keycloak/deployment.yaml index c4ffcda..3acb187 100644 --- a/services/keycloak/deployment.yaml +++ b/services/keycloak/deployment.yaml @@ -52,7 +52,7 @@ spec: - name: zot-regcred initContainers: - name: mailu-http-listener - image: registry.bstein.dev/sso/mailu-http-listener:0.1.0 + image: cli.registry.bstein.dev/sso/mailu-http-listener:0.1.0 imagePullPolicy: IfNotPresent command: ["/bin/sh", "-c"] args: diff --git a/services/monitoring/dcgm-exporter.yaml b/services/monitoring/dcgm-exporter.yaml index 06152e7..b30e810 100644 --- a/services/monitoring/dcgm-exporter.yaml +++ b/services/monitoring/dcgm-exporter.yaml @@ -39,7 +39,7 @@ spec: - operator: Exists containers: - name: dcgm-exporter - image: registry.bstein.dev/monitoring/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04 + image: cli.registry.bstein.dev/monitoring/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04 imagePullPolicy: Always ports: - name: metrics diff --git a/services/oauth2-proxy/middleware.yaml b/services/oauth2-proxy/middleware.yaml index db5f3a4..0adaff9 100644 --- a/services/oauth2-proxy/middleware.yaml +++ b/services/oauth2-proxy/middleware.yaml @@ -6,7 +6,7 @@ metadata: namespace: sso spec: forwardAuth: - address: http://oauth2-proxy.sso.svc.cluster.local:4180/oauth2/auth + address: http://oauth2-proxy.sso.svc.cluster.local/oauth2/auth trustForwardHeader: true authResponseHeaders: - Authorization diff --git a/services/pegasus/deployment.yaml b/services/pegasus/deployment.yaml index d9fa0be..fcd2b63 100644 --- a/services/pegasus/deployment.yaml +++ b/services/pegasus/deployment.yaml @@ -58,7 +58,7 @@ spec: containers: - name: pegasus - image: registry.bstein.dev/pegasus:1.2.32 # {"$imagepolicy": "jellyfin:pegasus"} + image: cli.registry.bstein.dev/pegasus:1.2.32 # {"$imagepolicy": "jellyfin:pegasus"} imagePullPolicy: Always command: ["/pegasus"] env: diff --git a/services/pegasus/image.yaml b/services/pegasus/image.yaml index 1d891ff..d5acf53 100644 --- a/services/pegasus/image.yaml +++ b/services/pegasus/image.yaml @@ -5,7 +5,7 @@ metadata: name: pegasus namespace: jellyfin spec: - image: registry.bstein.dev/pegasus + image: cli.registry.bstein.dev/pegasus interval: 1m0s secretRef: name: zot-regcred diff --git a/services/zot/configmap.yaml b/services/zot/configmap.yaml index 0261fc1..3046d78 100644 --- a/services/zot/configmap.yaml +++ b/services/zot/configmap.yaml @@ -26,14 +26,14 @@ data: "repositories": { "**": { "policies": [ - { "users": ["bstein"], "actions": ["read", "create", "update", "delete"] } + { "users": ["bstein", "zot-ui-proxy"], "actions": ["read", "create", "update", "delete"] } ], "defaultPolicy": [], "anonymousPolicy": [] } }, "adminPolicy": { - "users": ["bstein"], + "users": ["bstein", "zot-ui-proxy"], "actions": ["read", "create", "update", "delete"] } } diff --git a/services/zot/deployment.yaml b/services/zot/deployment.yaml index e4fdc1f..ef6ade9 100644 --- a/services/zot/deployment.yaml +++ b/services/zot/deployment.yaml @@ -35,6 +35,9 @@ spec: image: ghcr.io/project-zot/zot-linux-arm64:v2.1.8 imagePullPolicy: IfNotPresent args: ["serve", "/etc/zot/config.json"] + env: + - name: UI_PROXY_HTPASSWD + value: "zot-ui-proxy:$2y$05$ctfbLo5KBoNA6pluLGGWde6TK8eOPnIH9u8x/IivAhcE/k0qCCR3y" ports: - { name: http, containerPort: 5000 } volumeMounts: @@ -45,7 +48,6 @@ spec: - name: htpasswd mountPath: /etc/zot/htpasswd subPath: htpasswd - readOnly: true - name: zot-data mountPath: /var/lib/registry readinessProbe: @@ -60,13 +62,41 @@ spec: periodSeconds: 10 resources: requests: { cpu: "50m", memory: "64Mi" } + initContainers: + - name: merge-htpasswd + image: busybox:1.36 + command: + - sh + - -c + - | + set -e + if [ -f /src/htpasswd ]; then + cp /src/htpasswd /merged/htpasswd + else + touch /merged/htpasswd + fi + if [ -n "${UI_PROXY_HTPASSWD}" ]; then + echo "${UI_PROXY_HTPASSWD}" >> /merged/htpasswd + fi + env: + - name: UI_PROXY_HTPASSWD + value: "zot-ui-proxy:$2y$05$ctfbLo5KBoNA6pluLGGWde6TK8eOPnIH9u8x/IivAhcE/k0qCCR3y" + volumeMounts: + - name: htpasswd-source + mountPath: /src + readOnly: true + - name: htpasswd + mountPath: /merged volumes: - name: cfg configMap: name: zot-config - - name: htpasswd + - name: htpasswd-source secret: secretName: zot-htpasswd + optional: true + - name: htpasswd + emptyDir: {} - name: zot-data persistentVolumeClaim: claimName: zot-data diff --git a/services/zot/ingress.yaml b/services/zot/ingress.yaml index 12f6c60..86747c6 100644 --- a/services/zot/ingress.yaml +++ b/services/zot/ingress.yaml @@ -2,7 +2,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: zot + name: zot-cli namespace: zot annotations: cert-manager.io/cluster-issuer: letsencrypt @@ -12,10 +12,10 @@ metadata: spec: ingressClassName: traefik tls: - - hosts: [ "registry.bstein.dev" ] - secretName: registry-bstein-dev-tls + - hosts: [ "cli.registry.bstein.dev" ] + secretName: cli-registry-bstein-dev-tls rules: - - host: registry.bstein.dev + - host: cli.registry.bstein.dev http: paths: - path: / @@ -25,3 +25,30 @@ spec: name: zot port: number: 5000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: zot-ui + namespace: zot + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.middlewares: zot-zot-ui-auth-header@kubernetescrd, zot-zot-resp-headers@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: [ "web.registry.bstein.dev" ] + secretName: web-registry-bstein-dev-tls + rules: + - host: web.registry.bstein.dev + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: zot-oauth2-proxy + port: + number: 80 diff --git a/services/zot/kustomization.yaml b/services/zot/kustomization.yaml index 73a097c..0795b7e 100644 --- a/services/zot/kustomization.yaml +++ b/services/zot/kustomization.yaml @@ -7,5 +7,8 @@ resources: - deployment.yaml - configmap.yaml - service.yaml + - oauth2-proxy-deployment.yaml + - oauth2-proxy-service.yaml - ingress.yaml - middleware.yaml + - middleware-ui.yaml diff --git a/services/zot/middleware-ui.yaml b/services/zot/middleware-ui.yaml new file mode 100644 index 0000000..7feaf53 --- /dev/null +++ b/services/zot/middleware-ui.yaml @@ -0,0 +1,10 @@ +# services/zot/middleware-ui.yaml +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: zot-ui-auth-header + namespace: zot +spec: + headers: + customRequestHeaders: + Authorization: "Basic em90LXVpLXByb3h5OlRlbXBTc29VaVBhc3MhMjAyNQ==" diff --git a/services/zot/oauth2-proxy-deployment.yaml b/services/zot/oauth2-proxy-deployment.yaml new file mode 100644 index 0000000..b071f9a --- /dev/null +++ b/services/zot/oauth2-proxy-deployment.yaml @@ -0,0 +1,83 @@ +# services/zot/oauth2-proxy-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zot-oauth2-proxy + namespace: zot + labels: { app: zot-oauth2-proxy } +spec: + replicas: 1 + selector: + matchLabels: { app: zot-oauth2-proxy } + template: + metadata: + labels: { app: zot-oauth2-proxy } + spec: + nodeSelector: + node-role.kubernetes.io/worker: "true" + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 50 + preference: + matchExpressions: + - key: hardware + operator: In + values: ["rpi4","rpi5"] + containers: + - name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 + imagePullPolicy: IfNotPresent + args: + - --provider=oidc + - --redirect-url=https://web.registry.bstein.dev/oauth2/callback + - --oidc-issuer-url=https://sso.bstein.dev/realms/atlas + - --scope=openid profile email + - --email-domain=* + - --cookie-domain=web.registry.bstein.dev + - --cookie-name=_zot_ui_oauth + - --set-xauthrequest=true + - --set-authorization-header=false + - --pass-authorization-header=false + - --pass-access-token=false + - --cookie-secure=true + - --cookie-samesite=lax + - --cookie-refresh=20m + - --cookie-expire=168h + - --upstream=http://zot:5000 + - --http-address=0.0.0.0:4180 + - --skip-provider-button=true + - --skip-jwt-bearer-tokens=true + env: + - name: OAUTH2_PROXY_CLIENT_ID + valueFrom: + secretKeyRef: + name: zot-oidc + key: client_id + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: zot-oidc + key: client_secret + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: zot-oidc + key: client_secret + ports: + - containerPort: 4180 + name: http + readinessProbe: + httpGet: + path: /ping + port: 4180 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /ping + port: 4180 + initialDelaySeconds: 20 + periodSeconds: 20 + resources: + requests: { cpu: "25m", memory: "64Mi" } diff --git a/services/zot/oauth2-proxy-service.yaml b/services/zot/oauth2-proxy-service.yaml new file mode 100644 index 0000000..4a7e96a --- /dev/null +++ b/services/zot/oauth2-proxy-service.yaml @@ -0,0 +1,14 @@ +# services/zot/oauth2-proxy-service.yaml +apiVersion: v1 +kind: Service +metadata: + name: zot-oauth2-proxy + namespace: zot + labels: { app: zot-oauth2-proxy } +spec: + type: ClusterIP + selector: { app: zot-oauth2-proxy } + ports: + - name: http + port: 80 + targetPort: 4180