zot: add oauth proxy and user sync scripts

This commit is contained in:
Brad Stein 2025-12-15 12:57:02 -03:00
parent 828f66d18c
commit b87f06f6ff
23 changed files with 538 additions and 23 deletions

View File

@ -372,8 +372,8 @@ function xmrwallet_bootstrap --description "Interactive setup of monero-wallet-r
end end
# Use your private image by default (in Zot) # 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 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 registry.bstein.dev/infra/monero-wallet-rpc:0.18.4.1; end 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 _require "Container image" $image; or return 1
# --- Secrets (defaults: RPC user=wallet name, passwords auto if missing) # --- 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 " 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)." echo " Set xmrwallet_SKIP_DAEMON_CHECK=1 to bypass the daemon probe (not recommended)."
end end

View File

@ -23,7 +23,7 @@ end
# Default image chooser (you should override with your own multi-arch image) # Default image chooser (you should override with your own multi-arch image)
function _sui_default_image -a NET 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 end
# Convert any string to a k8s-safe name (RFC-1123 label-ish) # Convert any string to a k8s-safe name (RFC-1123 label-ish)

92
scripts/gitea_cred_sync.sh Executable file
View File

@ -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 "$@"

87
scripts/gitops_cred_sync.sh Executable file
View File

@ -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 "$@"

94
scripts/jenkins_cred_sync.sh Executable file
View File

@ -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 "$@"

View File

@ -1,6 +1,6 @@
#!/usr/bin/env fish #!/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 begin
kubectl -n longhorn-system get volumes.longhorn.io -o json \ kubectl -n longhorn-system get volumes.longhorn.io -o json \
| jq -r '.items[] | "\(.metadata.name)\t\(.status.actualSize)\t\(.spec.size)"' \ | jq -r '.items[] | "\(.metadata.name)\t\(.status.actualSize)\t\(.spec.size)"' \

View File

@ -39,7 +39,7 @@ SITES=(
"Jellyfin|https://stream.bstein.dev" "Jellyfin|https://stream.bstein.dev"
"Gitea|https://scm.bstein.dev" "Gitea|https://scm.bstein.dev"
"Jenkins|https://ci.bstein.dev" "Jenkins|https://ci.bstein.dev"
"Zot|https://registry.bstein.dev" "Zot|https://web.registry.bstein.dev"
"Vault|https://secret.bstein.dev" "Vault|https://secret.bstein.dev"
"Jitsi|https://meet.bstein.dev" "Jitsi|https://meet.bstein.dev"
"Grafana|https://metrics.bstein.dev" "Grafana|https://metrics.bstein.dev"

76
scripts/zot_cred_sync.sh Executable file
View File

@ -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 "$@"

View File

@ -35,7 +35,7 @@ spec:
values: ["rpi4"] values: ["rpi4"]
containers: containers:
- name: monerod - 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"] command: ["/opt/monero/monerod"]
args: args:
- --data-dir=/data - --data-dir=/data

View File

@ -32,7 +32,7 @@ spec:
values: ["rpi4"] values: ["rpi4"]
containers: containers:
- name: monero-p2pool - name: monero-p2pool
image: registry.bstein.dev/infra/monero-p2pool:4.9 image: cli.registry.bstein.dev/infra/monero-p2pool:4.9
imagePullPolicy: Always imagePullPolicy: Always
command: ["p2pool"] command: ["p2pool"]
args: args:

View File

@ -63,7 +63,7 @@ spec:
--key "$CLIENT_ID" \ --key "$CLIENT_ID" \
--secret "$CLIENT_SECRET" \ --secret "$CLIENT_SECRET" \
--auto-discover-url "$DISCOVERY_URL" \ --auto-discover-url "$DISCOVERY_URL" \
--scopes "openid profile email" \ --scopes "openid profile email groups" \
--required-claim-name "" \ --required-claim-name "" \
--required-claim-value "" \ --required-claim-value "" \
--group-claim-name groups \ --group-claim-name groups \
@ -77,7 +77,7 @@ spec:
--key "$CLIENT_ID" \ --key "$CLIENT_ID" \
--secret "$CLIENT_SECRET" \ --secret "$CLIENT_SECRET" \
--auto-discover-url "$DISCOVERY_URL" \ --auto-discover-url "$DISCOVERY_URL" \
--scopes "openid profile email" \ --scopes "openid profile email groups" \
--required-claim-name "" \ --required-claim-name "" \
--required-claim-value "" \ --required-claim-value "" \
--group-claim-name groups \ --group-claim-name groups \

View File

@ -52,7 +52,7 @@ spec:
- name: zot-regcred - name: zot-regcred
initContainers: initContainers:
- name: mailu-http-listener - 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 imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"] command: ["/bin/sh", "-c"]
args: args:

View File

@ -39,7 +39,7 @@ spec:
- operator: Exists - operator: Exists
containers: containers:
- name: dcgm-exporter - 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 imagePullPolicy: Always
ports: ports:
- name: metrics - name: metrics

View File

@ -6,7 +6,7 @@ metadata:
namespace: sso namespace: sso
spec: spec:
forwardAuth: forwardAuth:
address: http://oauth2-proxy.sso.svc.cluster.local:4180/oauth2/auth address: http://oauth2-proxy.sso.svc.cluster.local/oauth2/auth
trustForwardHeader: true trustForwardHeader: true
authResponseHeaders: authResponseHeaders:
- Authorization - Authorization

View File

@ -58,7 +58,7 @@ spec:
containers: containers:
- name: pegasus - 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 imagePullPolicy: Always
command: ["/pegasus"] command: ["/pegasus"]
env: env:

View File

@ -5,7 +5,7 @@ metadata:
name: pegasus name: pegasus
namespace: jellyfin namespace: jellyfin
spec: spec:
image: registry.bstein.dev/pegasus image: cli.registry.bstein.dev/pegasus
interval: 1m0s interval: 1m0s
secretRef: secretRef:
name: zot-regcred name: zot-regcred

View File

@ -26,14 +26,14 @@ data:
"repositories": { "repositories": {
"**": { "**": {
"policies": [ "policies": [
{ "users": ["bstein"], "actions": ["read", "create", "update", "delete"] } { "users": ["bstein", "zot-ui-proxy"], "actions": ["read", "create", "update", "delete"] }
], ],
"defaultPolicy": [], "defaultPolicy": [],
"anonymousPolicy": [] "anonymousPolicy": []
} }
}, },
"adminPolicy": { "adminPolicy": {
"users": ["bstein"], "users": ["bstein", "zot-ui-proxy"],
"actions": ["read", "create", "update", "delete"] "actions": ["read", "create", "update", "delete"]
} }
} }

View File

@ -35,6 +35,9 @@ spec:
image: ghcr.io/project-zot/zot-linux-arm64:v2.1.8 image: ghcr.io/project-zot/zot-linux-arm64:v2.1.8
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
args: ["serve", "/etc/zot/config.json"] args: ["serve", "/etc/zot/config.json"]
env:
- name: UI_PROXY_HTPASSWD
value: "zot-ui-proxy:$2y$05$ctfbLo5KBoNA6pluLGGWde6TK8eOPnIH9u8x/IivAhcE/k0qCCR3y"
ports: ports:
- { name: http, containerPort: 5000 } - { name: http, containerPort: 5000 }
volumeMounts: volumeMounts:
@ -45,7 +48,6 @@ spec:
- name: htpasswd - name: htpasswd
mountPath: /etc/zot/htpasswd mountPath: /etc/zot/htpasswd
subPath: htpasswd subPath: htpasswd
readOnly: true
- name: zot-data - name: zot-data
mountPath: /var/lib/registry mountPath: /var/lib/registry
readinessProbe: readinessProbe:
@ -60,13 +62,41 @@ spec:
periodSeconds: 10 periodSeconds: 10
resources: resources:
requests: { cpu: "50m", memory: "64Mi" } 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: volumes:
- name: cfg - name: cfg
configMap: configMap:
name: zot-config name: zot-config
- name: htpasswd - name: htpasswd-source
secret: secret:
secretName: zot-htpasswd secretName: zot-htpasswd
optional: true
- name: htpasswd
emptyDir: {}
- name: zot-data - name: zot-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: zot-data claimName: zot-data

View File

@ -2,7 +2,7 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: zot name: zot-cli
namespace: zot namespace: zot
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt cert-manager.io/cluster-issuer: letsencrypt
@ -12,10 +12,10 @@ metadata:
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls: tls:
- hosts: [ "registry.bstein.dev" ] - hosts: [ "cli.registry.bstein.dev" ]
secretName: registry-bstein-dev-tls secretName: cli-registry-bstein-dev-tls
rules: rules:
- host: registry.bstein.dev - host: cli.registry.bstein.dev
http: http:
paths: paths:
- path: / - path: /
@ -25,3 +25,30 @@ spec:
name: zot name: zot
port: port:
number: 5000 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

View File

@ -7,5 +7,8 @@ resources:
- deployment.yaml - deployment.yaml
- configmap.yaml - configmap.yaml
- service.yaml - service.yaml
- oauth2-proxy-deployment.yaml
- oauth2-proxy-service.yaml
- ingress.yaml - ingress.yaml
- middleware.yaml - middleware.yaml
- middleware-ui.yaml

View File

@ -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=="

View File

@ -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" }

View File

@ -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