Implement confidential OIDC authentication boundary

This commit is contained in:
ms2k 2026-07-18 18:08:41 +09:00
commit a4e615096c
5 changed files with 1896 additions and 329 deletions

View file

@ -207,6 +207,9 @@ spec:
spec:
automountServiceAccountToken: false
enableServiceLinks: false
volumes:
- name: bootstrap
emptyDir: {}
containers:
- name: zitadel
image: @@ZITADEL_IMAGE@@
@ -220,6 +223,21 @@ spec:
- {name: ZITADEL_EXTERNALPORT, value: "8080"}
- {name: ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME, value: nuthatch-admin}
- {name: ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD, value: Synthetic-Zitadel-1!}
- {name: ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED, value: "false"}
- {name: ZITADEL_FIRSTINSTANCE_PATPATH, value: /bootstrap/admin.pat}
- {name: ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME, value: nuthatch-provisioner}
- {name: ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME, value: Nuthatch Provisioner}
- {name: ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE, value: "2036-01-01T00:00:00Z"}
- {name: ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH, value: /bootstrap/login-client.pat}
- {name: ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME, value: nuthatch-login-client}
- {name: ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME, value: Nuthatch Login Client}
- {name: ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE, value: "2036-01-01T00:00:00Z"}
- {name: ZITADEL_DEFAULTINSTANCE_OIDCSETTINGS_ACCESSTOKENLIFETIME, value: 5m}
- {name: ZITADEL_DEFAULTINSTANCE_OIDCSETTINGS_IDTOKENLIFETIME, value: 5m}
- {name: ZITADEL_DEFAULTINSTANCE_OIDCSETTINGS_REFRESHTOKENIDLEEXPIRATION, value: 1h}
- {name: ZITADEL_DEFAULTINSTANCE_OIDCSETTINGS_REFRESHTOKENEXPIRATION, value: 2h}
volumeMounts:
- {name: bootstrap, mountPath: /bootstrap}
ports:
- {name: http, containerPort: 8080}
readinessProbe:
@ -233,6 +251,25 @@ spec:
resources:
requests: {cpu: 200m, memory: 512Mi}
limits: {cpu: "2", memory: 1Gi}
- name: oidc-provisioner
image: @@CURL_IMAGE@@
imagePullPolicy: Never
command: [/bin/sh, -ec]
args:
- |
while [ ! -s /bootstrap/admin.pat ] || [ ! -s /bootstrap/login-client.pat ]; do
sleep 1
done
exec sleep 2147483647
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: ["ALL"]}
runAsUser: 0
volumeMounts:
- {name: bootstrap, mountPath: /bootstrap, readOnly: true}
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 100m, memory: 32Mi}
---
apiVersion: v1
kind: Service

View file

@ -197,6 +197,7 @@ render_manifests() {
-e "s|@@GARAGE_IMAGE@@|$garage|g" \
-e "s|@@VERSITYGW_IMAGE@@|$versitygw|g" \
-e "s|@@COREDNS_IMAGE@@|$coredns|g" \
-e "s|@@CURL_IMAGE@@|$curl|g" \
-e "s|@@AWS_CLI_IMAGE@@|$aws_cli|g" \
-e "s|@@POSTGRES_REPLICAS@@|$postgres_replicas|g" \
-e "s|@@REDIS_REPLICAS@@|$redis_replicas|g" \
@ -528,6 +529,153 @@ provision_lane_target_principal() {
printf -v "$secret_key_var" '%s' "$secret_key"
}
keycloak_admin_token() {
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
sh -ec '
/opt/keycloak/bin/kcadm.sh config credentials \
--server http://127.0.0.1:8080 \
--realm master \
--user nuthatch-admin \
--password synthetic-keycloak >/dev/null
/opt/keycloak/bin/kcadm.sh get serverinfo >/dev/null
' >/dev/null
}
provision_lane_keycloak_registration() {
local lane="$1"
local redirect_uri="$2"
local client_id_var="$3"
local client_secret_var="$4"
local additional_audiences_var="$5"
local user_a_var="$6"
local user_b_var="$7"
local client_id="nuthatch-$lane"
local client_secret="synthetic-keycloak-client-secret-$lane"
local username
keycloak_admin_token || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create clients \
--realm master \
-s "clientId=$client_id" \
-s enabled=true \
-s protocol=openid-connect \
-s publicClient=false \
-s clientAuthenticatorType=client-secret \
-s standardFlowEnabled=true \
-s directAccessGrantsEnabled=false \
-s "secret=$client_secret" \
-s "redirectUris=[\"$redirect_uri\"]" \
-s 'attributes={"pkce.code.challenge.method":"S256"}' >/dev/null || return
for username in "nuthatch-$lane-a" "nuthatch-$lane-b"; do
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh create users \
--realm master \
-s "username=$username" \
-s enabled=true \
-s "firstName=$username" >/dev/null || return
kube --namespace "$FIXTURE_NAMESPACE" exec deployment/keycloak -- \
/opt/keycloak/bin/kcadm.sh set-password \
--realm master \
--username "$username" \
--new-password Synthetic-OIDC-1! >/dev/null || return
done
printf -v "$client_id_var" '%s' "$client_id"
printf -v "$client_secret_var" '%s' "$client_secret"
printf -v "$additional_audiences_var" ''
printf -v "$user_a_var" '%s' "nuthatch-$lane-a"
printf -v "$user_b_var" '%s' "nuthatch-$lane-b"
}
zitadel_api() {
local token_path="$1"
local method="$2"
local path="$3"
local body="$4"
kube --namespace "$FIXTURE_NAMESPACE" exec \
deployment/zitadel \
--container oidc-provisioner \
-- sh -ec '
token="$(tr -d "\r\n" < "$1")"
curl --fail --silent --show-error \
--request "$2" \
--header "Host: zitadel.nuthatch.test" \
--header "Authorization: Bearer $token" \
--header "Content-Type: application/json" \
--data "$4" \
"http://127.0.0.1:8080$3"
' sh "$token_path" "$method" "$path" "$body"
}
provision_lane_zitadel_registration() {
local lane="$1"
local redirect_uri="$2"
local client_id_var="$3"
local client_secret_var="$4"
local additional_audiences_var="$5"
local user_a_var="$6"
local user_b_var="$7"
local project_response
local project_id
local app_response
local client_id
local client_secret
local username
local user_response
local user_ids=()
project_response="$(
zitadel_api \
/bootstrap/admin.pat \
POST \
/management/v1/projects \
"{\"name\":\"Nuthatch lane $lane\"}"
)" || return
project_id="$(jq -er .id <<< "$project_response")" || return
app_response="$(
zitadel_api \
/bootstrap/admin.pat \
POST \
"/management/v1/projects/$project_id/apps/oidc" \
"{\"projectId\":\"$project_id\",\"name\":\"Nuthatch lane $lane\",\"redirectUris\":[\"$redirect_uri\"],\"responseTypes\":[\"OIDC_RESPONSE_TYPE_CODE\"],\"grantTypes\":[\"OIDC_GRANT_TYPE_AUTHORIZATION_CODE\"],\"appType\":\"OIDC_APP_TYPE_WEB\",\"authMethodType\":\"OIDC_AUTH_METHOD_TYPE_BASIC\",\"version\":\"OIDC_VERSION_1_0\",\"devMode\":false}"
)" || return
client_id="$(jq -er .clientId <<< "$app_response")" || return
client_secret="$(jq -er .clientSecret <<< "$app_response")" || return
for username in "nuthatch-$lane-a" "nuthatch-$lane-b"; do
user_response="$(
zitadel_api \
/bootstrap/admin.pat \
POST \
/management/v1/users/human \
"{\"userName\":\"$username\",\"profile\":{\"firstName\":\"Nuthatch\",\"lastName\":\"$username\",\"displayName\":\"$username\",\"preferredLanguage\":\"en\"},\"email\":{\"email\":\"$username@example.test\",\"isEmailVerified\":true},\"initialPassword\":\"Synthetic-OIDC-1!\"}"
)" || return
user_ids+=("$(jq -er .userId <<< "$user_response")") || return
done
printf -v "$client_id_var" '%s' "$client_id"
printf -v "$client_secret_var" '%s' "$client_secret"
printf -v "$additional_audiences_var" '%s' "$project_id"
printf -v "$user_a_var" '%s' "${user_ids[0]}"
printf -v "$user_b_var" '%s' "${user_ids[1]}"
}
create_oidc_automation_secret() {
local lane="$1"
local provider="$2"
local login_pat=unused
if [[ "$provider" = zitadel ]]; then
login_pat="$(
kube --namespace "$FIXTURE_NAMESPACE" exec \
deployment/zitadel \
--container oidc-provisioner \
-- sh -ec 'tr -d "\r\n" < /bootstrap/login-client.pat'
)" || return
fi
kube create secret generic "nuthatch-oidc-automation-$lane" \
--namespace "$FIXTURE_NAMESPACE" \
--from-literal="NUTHATCH_OIDC_LOGIN_PAT=$login_pat" \
--dry-run=client \
--output yaml |
kube apply --filename - >/dev/null
}
write_lane_profile() {
local lane="$1"
local coordination="$2"
@ -538,6 +686,12 @@ write_lane_profile() {
local database=
local coordination_database=
local oidc_issuer=
local oidc_client_id=
local oidc_client_secret=
local oidc_additional_audiences=
local oidc_redirect_uri=
local oidc_user_a=
local oidc_user_b=
local target_url=
local target_bucket=
local target_region=
@ -550,11 +704,36 @@ write_lane_profile() {
coordination_database="$(reset_lane_coordination_store "$coordination" "$lane")" || return
fi
if fixture_has_family oidc; then
oidc_redirect_uri="https://nuthatch-$lane:8080/oidc/callback"
case "$oidc" in
keycloak) oidc_issuer=http://keycloak.nuthatch.test:8080/realms/master ;;
zitadel) oidc_issuer=http://zitadel.nuthatch.test:8080 ;;
*) return 1 ;;
esac
case "$oidc" in
keycloak)
provision_lane_keycloak_registration \
"$lane" \
"$oidc_redirect_uri" \
oidc_client_id \
oidc_client_secret \
oidc_additional_audiences \
oidc_user_a \
oidc_user_b || return
;;
zitadel)
provision_lane_zitadel_registration \
"$lane" \
"$oidc_redirect_uri" \
oidc_client_id \
oidc_client_secret \
oidc_additional_audiences \
oidc_user_a \
oidc_user_b || return
;;
*) return 1 ;;
esac
create_oidc_automation_secret "$lane" "$oidc" || return
fi
if fixture_has_family target; then
target_bucket="nuthatch-lane-$lane"
@ -581,6 +760,12 @@ write_lane_profile() {
printf 'NUTHATCH_PG_CONN=host=postgres port=5432 user=nuthatch password=synthetic-postgres dbname=%s\n' "$database"
printf 'NUTHATCH_REDIS_URL=redis://%s:6379/%s\n' "$coordination" "$coordination_database"
printf 'NUTHATCH_OIDC_ISSUER=%s\n' "$oidc_issuer"
printf 'NUTHATCH_OIDC_CLIENT_ID=%s\n' "$oidc_client_id"
printf 'NUTHATCH_OIDC_ADDITIONAL_AUDIENCES=%s\n' "$oidc_additional_audiences"
printf 'NUTHATCH_OIDC_REDIRECT_URI=%s\n' "$oidc_redirect_uri"
printf 'NUTHATCH_OIDC_USER_A=%s\n' "$oidc_user_a"
printf 'NUTHATCH_OIDC_USER_B=%s\n' "$oidc_user_b"
printf 'NUTHATCH_OIDC_USER_PASSWORD=Synthetic-OIDC-1!\n'
printf 'NUTHATCH_S3_ENDPOINT=%s\n' "$target_url"
printf 'NUTHATCH_S3_BUCKET=%s\n' "$target_bucket"
printf 'NUTHATCH_S3_REGION=%s\n' "$target_region"
@ -588,6 +773,10 @@ write_lane_profile() {
printf 'NUTHATCH_S3_SECRET_KEY=%s\n' "$target_secret_key"
printf 'NUTHATCH_S3_PATH_STYLE=true\n'
} > "$profile_file" || return
if fixture_has_family oidc; then
printf '%s' "$oidc_client_secret" > "$profile_file.oidc-secret" || return
chmod 0600 "$profile_file.oidc-secret" || return
fi
kube create configmap "$profile" \
--namespace "$FIXTURE_NAMESPACE" \
--from-env-file="$profile_file" \
@ -682,8 +871,12 @@ cleanup_lane_resources() {
"job/$job" \
"configmap/$profile" \
"configmap/$profile-file" \
"secret/nuthatch-oidc-automation-${profile##*-}" \
--ignore-not-found >/dev/null || return
rm -f "$RUNTIME/$profile.env" "$RUNTIME/$job.yaml"
rm -f \
"$RUNTIME/$profile.env" \
"$RUNTIME/$profile.env.oidc-secret" \
"$RUNTIME/$job.yaml"
}
run_job_matrix() {
@ -722,6 +915,7 @@ destroy_fixture() {
"$RUNTIME/probe.yaml" \
"$RUNTIME/fixture.env" \
"$RUNTIME"/nuthatch-fixture-lane-*.env \
"$RUNTIME"/nuthatch-fixture-lane-*.env.oidc-secret \
"$RUNTIME"/wp05-app-*.yaml || status=1
return "$status"
}

View file

@ -21,8 +21,14 @@ spec:
envFrom:
- configMapRef: {name: @@PROFILE_NAME@@}
env:
- {name: NUTHATCH_JWT_SECRET, value: synthetic-wp04-jwt-secret}
- {name: NUTHATCH_JWT_SECRET, value: synthetic-wp04-session-jwt-secret}
- {name: NUTHATCH_REDIS_URL, value: "redis://127.0.0.1:6379/0"}
- {name: NUTHATCH_PUBLIC_BASE_URL, value: "https://wp04.invalid"}
- {name: NUTHATCH_OIDC_ISSUER, value: "https://issuer.invalid"}
- {name: NUTHATCH_OIDC_CLIENT_ID, value: wp04}
- {name: NUTHATCH_OIDC_ADDITIONAL_AUDIENCES, value: ""}
- {name: NUTHATCH_OIDC_CLIENT_SECRET, value: synthetic-wp04-oidc-client-secret}
- {name: NUTHATCH_OIDC_REDIRECT_URI, value: "https://wp04.invalid/oidc/callback"}
resources:
requests: {cpu: 100m, memory: 32Mi}
limits: {cpu: "2", memory: 96Mi}

View file

@ -11,6 +11,15 @@ spec:
spec:
automountServiceAccountToken: false
enableServiceLinks: false
dnsPolicy: None
dnsConfig:
nameservers: [10.96.0.53]
searches:
- nuthatch-testbed.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- {name: ndots, value: "5"}
restartPolicy: Never
initContainers:
- name: credential-negative
@ -57,18 +66,301 @@ spec:
command: [/bin/sh, -ec]
args:
- |
base="http://nuthatch-@@LANE@@:8080"
curl --fail --silent --show-error "$base/healthz" | grep -F '"ok":true'
curl --fail --silent --show-error "$base/readyz" | grep -F '"ready":true'
curl --fail --silent --show-error \
--cookie-jar /tmp/cookies \
--dump-header /tmp/login-headers \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{"username":"wp05-smoke"}' \
"$base/login" >/tmp/login.json
csrf="$(awk 'BEGIN{IGNORECASE=1} $1=="X-CSRF-Token:"{gsub("\r","",$2); print $2}' /tmp/login-headers)"
base="https://nuthatch-@@LANE@@:8080"
curl() {
command curl --insecure "$@"
}
header_value() {
awk -v expected="$2" '
BEGIN { IGNORECASE=1 }
$1 == expected ":" {
sub(/^[^:]*:[[:space:]]*/, "")
sub(/\r$/, "")
print
exit
}
' "$1"
}
json_field() {
tr -d '\n' < "$1" |
sed -n 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
}
oidc_callback_for() {
user="$1"
suffix="$2"
authorization_url="$3"
printf 'Completing %s authorization %s\n' \
"$NUTHATCH_OIDC_PROVIDER" "$suffix" >&2
case "$NUTHATCH_OIDC_PROVIDER" in
keycloak)
curl --fail --silent --show-error \
--cookie-jar "/tmp/idp-$suffix" \
--dump-header "/tmp/idp-start-$suffix.headers" \
"$authorization_url" >"/tmp/idp-$suffix.html"
action="$(
sed -n 's/.*action="\([^"]*\)".*/\1/p' "/tmp/idp-$suffix.html" |
head -n 1 |
sed 's/&amp;/\&/g'
)"
test -n "$action"
curl --silent --show-error \
--cookie "/tmp/idp-$suffix" \
--cookie-jar "/tmp/idp-$suffix" \
--dump-header "/tmp/idp-finish-$suffix.headers" \
--data-urlencode "username=$user" \
--data-urlencode "password=$NUTHATCH_OIDC_USER_PASSWORD" \
--data-urlencode credentialId= \
"$action" >"/tmp/idp-finish-$suffix.body"
header_value "/tmp/idp-finish-$suffix.headers" Location
;;
zitadel)
curl --silent --show-error \
--dump-header "/tmp/idp-start-$suffix.headers" \
"$authorization_url" >"/tmp/idp-start-$suffix.body"
login_location="$(
header_value "/tmp/idp-start-$suffix.headers" Location
)"
auth_request="${login_location##*authRequest=}"
auth_request="${auth_request%%&*}"
test -n "$auth_request"
curl --fail --silent --show-error \
--header "Authorization: Bearer $NUTHATCH_OIDC_LOGIN_PAT" \
--header 'Content-Type: application/json' \
--data "{\"checks\":{\"user\":{\"userId\":\"$user\"},\"password\":{\"password\":\"$NUTHATCH_OIDC_USER_PASSWORD\"}},\"lifetime\":\"300s\"}" \
"$NUTHATCH_OIDC_ISSUER/v2/sessions" \
>"/tmp/session-$suffix.json"
session_id="$(json_field "/tmp/session-$suffix.json" sessionId)"
session_token="$(json_field "/tmp/session-$suffix.json" sessionToken)"
test -n "$session_id"
test -n "$session_token"
curl --fail --silent --show-error \
--header "Authorization: Bearer $NUTHATCH_OIDC_LOGIN_PAT" \
--header 'Content-Type: application/json' \
--data "{\"session\":{\"sessionId\":\"$session_id\",\"sessionToken\":\"$session_token\"}}" \
"$NUTHATCH_OIDC_ISSUER/v2/oidc/auth_requests/$auth_request" \
>"/tmp/finalize-$suffix.json"
json_field "/tmp/finalize-$suffix.json" callbackUrl |
sed 's/\\u0026/\&/g; s#\\/#/#g'
;;
*)
exit 1
;;
esac
}
login() {
user="$1"
suffix="$2"
cookies="/tmp/cookies-$suffix"
printf 'Starting login %s\n' "$suffix" >&2
curl --insecure --silent --show-error \
--cookie-jar "$cookies" \
--dump-header "/tmp/login-$suffix.headers" \
"$base/login" >"/tmp/login-$suffix.body"
test "$(header_value "/tmp/login-$suffix.headers" Status 2>/dev/null || true)" = ""
authorization_url="$(
header_value "/tmp/login-$suffix.headers" Location
)"
test -n "$authorization_url"
grep -i '^set-cookie: nuthatch_oidc=' "/tmp/login-$suffix.headers" |
grep -i 'secure' |
grep -i 'httponly' |
grep -i 'samesite=lax' |
grep -i 'path=/oidc/callback' >/dev/null
state="$(
printf '%s' "$authorization_url" |
sed -n 's/.*[?&]state=\([^&]*\).*/\1/p'
)"
test -n "$state"
if [ "$suffix" = a ]; then
wrong_redirect_url="$(
printf '%s' "$authorization_url" |
sed 's#redirect_uri=[^&]*#redirect_uri=https%3A%2F%2Finvalid.example%2Fcallback#'
)"
wrong_redirect_status="$(
curl --silent --output /dev/null \
--write-out '%{http_code}' \
"$wrong_redirect_url"
)"
test "$wrong_redirect_status" -ge 400
printf 'Redirect URI rejection passed\n' >&2
fi
if curl --insecure --fail --silent \
--cookie "$cookies" \
"$base/oidc/callback?code=unused&state=invalid-$state" >/dev/null; then
exit 1
fi
if [ "$suffix" = a ]; then
transaction="$(
awk -F '\t' '$6 == "nuthatch_oidc" { print $7 }' "$cookies"
)"
test "${#transaction}" -gt 1
tampered_transaction="x${transaction#?}"
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header 'Cookie: nuthatch_oidc=malformed' \
"$base/oidc/callback?code=unused&state=$state"
)" = 400
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header "Cookie: nuthatch_oidc=$tampered_transaction" \
"$base/oidc/callback?code=unused&state=$state"
)" = 400
fi
callback="$(
oidc_callback_for "$user" "$suffix" "$authorization_url"
)"
test -n "$callback"
printf 'Authorization callback received %s\n' "$suffix" >&2
callback_status="$(
curl --insecure --silent --show-error \
--output "/tmp/callback-$suffix.body" \
--write-out '%{http_code}' \
--cookie "$cookies" \
--cookie-jar "$cookies" \
--dump-header "/tmp/callback-$suffix.headers" \
"$callback"
)"
printf 'Application callback status=%s %s\n' \
"$callback_status" "$suffix" >&2
if [ "$callback_status" != 303 ]; then
sed -n '1,8p' "/tmp/callback-$suffix.body" >&2
exit 1
fi
test "$(header_value "/tmp/callback-$suffix.headers" Location)" = "/"
printf 'Application callback accepted %s\n' "$suffix" >&2
sed -n \
's/^\([Ss]et-[Cc]ookie: [^=]*\)=.*/\1=<redacted>/p' \
"/tmp/callback-$suffix.headers" >&2
session_cookie="$(
grep -i '^set-cookie: nuthatch_session=' "/tmp/callback-$suffix.headers"
)"
csrf_cookie="$(
grep -i '^set-cookie: nuthatch_csrf=' "/tmp/callback-$suffix.headers"
)"
transaction_cookie="$(
grep -i '^set-cookie: nuthatch_oidc=' "/tmp/callback-$suffix.headers"
)"
printf '%s' "$session_cookie" |
grep -i secure |
grep -i httponly |
grep -i samesite=lax |
grep -i 'path=/;' >/dev/null
printf '%s' "$csrf_cookie" |
grep -i secure |
grep -i samesite=lax |
grep -iv httponly |
grep -i 'path=/;' >/dev/null
printf '%s' "$transaction_cookie" |
grep -i 'max-age=0' |
grep -i 'path=/oidc/callback;' >/dev/null
if printf '%s\n%s\n%s\n' \
"$session_cookie" "$csrf_cookie" "$transaction_cookie" |
grep -qi 'domain='; then
exit 1
fi
printf 'Cookie attributes passed %s\n' "$suffix" >&2
awk -F '\t' '
NF == 7 {
printf "Stored cookie %s value-length=%d\n", $6, length($7) > "/dev/stderr"
}
$6 == "nuthatch_csrf" { print $7 }
' "$cookies"
}
curl --insecure --fail --silent --show-error "$base/healthz" |
grep -F '"ok":true'
curl --insecure --fail --silent --show-error "$base/readyz" |
grep -F '"ready":true'
curl --insecure --fail --silent --show-error "$base/nuthatch.js" |
grep -F 'nuthatch'
{
curl --insecure --fail --silent --show-error "$base/nuthatch.js"
curl --insecure --silent --show-error "$base/login"
} > /tmp/browser-visible
if grep -Ei \
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
/tmp/browser-visible; then
exit 1
fi
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--request POST \
"$base/login"
)" = 405
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
"$base/"
)" = 303
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
"$base/download?root=personal&path=unauthenticated"
)" = 303
unauthenticated_digest="$(
printf x | sha256sum | cut -d' ' -f1
)"
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header 'X-Declared-Size: 1' \
--header "X-Content-SHA256: $unauthenticated_digest" \
--header 'X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
--data x \
"$base/upload?root=personal&path=unauthenticated&operation_id=40000000-0000-4000-8000-000000000001"
)" = 303
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header 'Content-Type: application/json' \
--data '{"root":"personal","path":"unauthenticated","operation_id":"40000000-0000-4000-8000-000000000002","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
"$base/delete"
)" = 303
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header 'Content-Type: application/json' \
--data '{"root":"personal","path":"unauthenticated","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
"$base/delete-prefix"
)" = 303
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--header 'X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
--request POST \
"$base/logout"
)" = 303
printf 'Unauthenticated boundary probes passed\n'
csrf="$(login "$NUTHATCH_OIDC_USER_A" a)"
test -n "$csrf"
cookies=/tmp/cookies-a
printf 'Login a completed csrf-length=%d\n' "${#csrf}"
authenticated_status="$(
curl --insecure --silent --output /tmp/authenticated.body \
--write-out '%{http_code}' \
--cookie "$cookies" \
"$base/"
)"
printf 'Authenticated browse status=%s\n' "$authenticated_status"
test "$authenticated_status" = 200
curl --insecure --fail --silent --show-error \
--cookie "$cookies" \
--header 'Accept: application/json' \
"$base/" >/tmp/authenticated.json
if grep -Ei \
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
/tmp/authenticated.body /tmp/authenticated.json; then
exit 1
fi
exercise_root() {
root="$1"
@ -78,22 +370,28 @@ spec:
printf '%s' "$first" >/tmp/body
size="$(wc -c </tmp/body | tr -d ' ')"
digest="$(sha256sum /tmp/body | cut -d' ' -f1)"
curl --fail --silent --show-error \
--cookie /tmp/cookies \
upload_status="$(
curl --silent --show-error \
--output /tmp/upload.json \
--write-out '%{http_code}' \
--cookie "$cookies" \
--header 'Content-Type: application/octet-stream' \
--header "X-Declared-Size: $size" \
--header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}" |
grep -F '"result":"succeeded"'
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}"
)"
printf 'Upload %s status=%s\n' "$root" "$upload_status"
test "$upload_status" = 200
grep -F '"result":"succeeded"' /tmp/upload.json
curl --fail --silent --show-error \
--cookie /tmp/cookies \
--cookie "$cookies" \
--header 'Accept: application/json' \
"$base/?root=$root" |
grep -F "smoke-$suffix.txt"
curl --fail --silent --show-error \
--cookie /tmp/cookies \
--cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
test "$(cat /tmp/download)" = "$first"
@ -101,7 +399,7 @@ spec:
size="$(wc -c </tmp/body | tr -d ' ')"
digest="$(sha256sum /tmp/body | cut -d' ' -f1)"
curl --fail --silent --show-error \
--cookie /tmp/cookies \
--cookie "$cookies" \
--header 'Content-Type: application/octet-stream' \
--header "X-Declared-Size: $size" \
--header "X-Content-SHA256: $digest" \
@ -110,18 +408,18 @@ spec:
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=10000000-0000-4000-8000-0000000000${suffix}" |
grep -F '"result":"succeeded"'
curl --fail --silent --show-error \
--cookie /tmp/cookies \
--cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
test "$(cat /tmp/download)" = "$second"
curl --fail --silent --show-error \
--cookie /tmp/cookies \
--cookie "$cookies" \
--header 'Content-Type: application/json' \
--data "{\"root\":\"$root\",\"path\":\"smoke-$suffix.txt\",\"operation_id\":\"20000000-0000-4000-8000-0000000000${suffix}\",\"csrf\":\"$csrf\"}" \
"$base/delete" |
grep -F '"result":"succeeded"'
if curl --fail --silent \
--cookie /tmp/cookies \
--cookie "$cookies" \
"$base/download?root=$root&path=smoke-$suffix.txt" >/dev/null; then
exit 1
fi
@ -129,7 +427,152 @@ spec:
exercise_root personal 01
exercise_root shared 02
printf 'WP-05 app smoke passed\n'
printf private-a >/tmp/body
size="$(wc -c </tmp/body | tr -d ' ')"
digest="$(sha256sum /tmp/body | cut -d' ' -f1)"
curl --insecure --fail --silent --show-error \
--cookie "$cookies" \
--header 'Content-Type: application/octet-stream' \
--header "X-Declared-Size: $size" \
--header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \
"$base/upload?root=personal&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000001" |
grep -F '"result":"succeeded"'
curl --insecure --fail --silent --show-error \
--cookie "$cookies" \
--header 'Content-Type: application/octet-stream' \
--header "X-Declared-Size: $size" \
--header "X-Content-SHA256: $digest" \
--header "X-CSRF-Token: $csrf" \
--data-binary @/tmp/body \
"$base/upload?root=shared&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000002" |
grep -F '"result":"succeeded"'
csrf_b="$(login "$NUTHATCH_OIDC_USER_B" b)"
test -n "$csrf_b"
curl --insecure --silent --show-error \
--cookie-jar /tmp/cookies-pkce \
--dump-header /tmp/login-pkce.headers \
"$base/login" >/tmp/login-pkce.body
pkce_authorization_url="$(
header_value /tmp/login-pkce.headers Location
)"
pkce_callback="$(
oidc_callback_for \
"$NUTHATCH_OIDC_USER_A" \
pkce \
"$pkce_authorization_url"
)"
pkce_code="$(
printf '%s' "$pkce_callback" |
sed -n 's/.*[?&]code=\([^&]*\).*/\1/p'
)"
test -n "$pkce_code"
curl --fail --silent --show-error \
"$NUTHATCH_OIDC_ISSUER/.well-known/openid-configuration" \
>/tmp/discovery.json
token_endpoint="$(json_field /tmp/discovery.json token_endpoint)"
test -n "$token_endpoint"
pkce_status="$(
curl --silent --output /tmp/pkce-error.json \
--write-out '%{http_code}' \
--user "$NUTHATCH_OIDC_CLIENT_ID:$NUTHATCH_OIDC_CLIENT_SECRET" \
--data-urlencode grant_type=authorization_code \
--data-urlencode "code=$pkce_code" \
--data-urlencode "redirect_uri=$NUTHATCH_OIDC_REDIRECT_URI" \
--data-urlencode code_verifier=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
"$token_endpoint"
)"
test "$pkce_status" -ge 400
if curl --insecure --fail --silent --show-error \
--cookie /tmp/cookies-b \
--header 'Accept: application/json' \
"$base/?root=personal" |
grep -F cross-user.txt; then
exit 1
fi
curl --insecure --fail --silent --show-error \
--cookie /tmp/cookies-b \
--header 'Accept: application/json' \
"$base/?root=shared" |
grep -F cross-user.txt
if curl --insecure --fail --silent \
--cookie "$cookies" \
--header 'X-CSRF-Token: invalid' \
--request POST \
"$base/logout" >/dev/null; then
exit 1
fi
curl --insecure --fail --silent --show-error \
--cookie "$cookies" \
--cookie-jar "$cookies" \
--dump-header /tmp/logout.headers \
--header "X-CSRF-Token: $csrf" \
--request POST \
"$base/logout" >/dev/null
grep -i '^set-cookie: nuthatch_session=' /tmp/logout.headers |
grep -i 'max-age=0' >/dev/null
grep -i '^set-cookie: nuthatch_csrf=' /tmp/logout.headers |
grep -i 'max-age=0' >/dev/null
grep -i '^set-cookie: nuthatch_oidc=' /tmp/logout.headers |
grep -i 'max-age=0' |
grep -i 'path=/oidc/callback' >/dev/null
test "$(
curl --insecure --silent --output /dev/null \
--write-out '%{http_code}' \
--cookie "$cookies" \
"$base/"
)" = 303
printf 'OIDC app smoke passed\n'
env:
- name: NUTHATCH_OIDC_PROVIDER
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_PROVIDER
- name: NUTHATCH_OIDC_ISSUER
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_ISSUER
- name: NUTHATCH_OIDC_USER_A
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_USER_A
- name: NUTHATCH_OIDC_USER_B
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_USER_B
- name: NUTHATCH_OIDC_USER_PASSWORD
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_USER_PASSWORD
- name: NUTHATCH_OIDC_CLIENT_ID
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_CLIENT_ID
- name: NUTHATCH_OIDC_REDIRECT_URI
valueFrom:
configMapKeyRef:
name: @@PROFILE_NAME@@
key: NUTHATCH_OIDC_REDIRECT_URI
- name: NUTHATCH_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: nuthatch-@@LANE@@
key: NUTHATCH_OIDC_CLIENT_SECRET
- name: NUTHATCH_OIDC_LOGIN_PAT
valueFrom:
secretKeyRef:
name: nuthatch-oidc-automation-@@LANE@@
key: NUTHATCH_OIDC_LOGIN_PAT
resources:
requests: {cpu: 50m, memory: 32Mi}
limits: {cpu: "1", memory: 128Mi}

View file

@ -36,6 +36,10 @@ render_application() {
local profile_file="$2"
local pg_conn
local redis_url
local oidc_issuer
local oidc_client_id
local oidc_additional_audiences
local oidc_client_secret
local s3_endpoint
local s3_region
local s3_bucket
@ -43,6 +47,12 @@ render_application() {
local s3_secret_key
pg_conn="$(sed -n 's/^NUTHATCH_PG_CONN=//p' "$profile_file")"
redis_url="$(sed -n 's/^NUTHATCH_REDIS_URL=//p' "$profile_file")"
oidc_issuer="$(sed -n 's/^NUTHATCH_OIDC_ISSUER=//p' "$profile_file")"
oidc_client_id="$(sed -n 's/^NUTHATCH_OIDC_CLIENT_ID=//p' "$profile_file")"
oidc_additional_audiences="$(
sed -n 's/^NUTHATCH_OIDC_ADDITIONAL_AUDIENCES=//p' "$profile_file"
)"
oidc_client_secret="$(<"$profile_file.oidc-secret")"
s3_endpoint="$(sed -n 's/^NUTHATCH_S3_ENDPOINT=//p' "$profile_file")"
s3_region="$(sed -n 's/^NUTHATCH_S3_REGION=//p' "$profile_file")"
s3_bucket="$(sed -n 's/^NUTHATCH_S3_BUCKET=//p' "$profile_file")"
@ -53,6 +63,10 @@ render_application() {
-e "s|@@APP_IMAGE@@|$APP_IMAGE|g" \
-e "s|@@PG_CONN@@|$pg_conn|g" \
-e "s|@@REDIS_URL@@|$redis_url|g" \
-e "s|@@OIDC_ISSUER@@|$oidc_issuer|g" \
-e "s|@@OIDC_CLIENT_ID@@|$oidc_client_id|g" \
-e "s|@@OIDC_ADDITIONAL_AUDIENCES@@|$oidc_additional_audiences|g" \
-e "s|@@OIDC_CLIENT_SECRET@@|$oidc_client_secret|g" \
-e "s|@@S3_ENDPOINT@@|$s3_endpoint|g" \
-e "s|@@S3_REGION@@|$s3_region|g" \
-e "s|@@S3_BUCKET@@|$s3_bucket|g" \
@ -74,7 +88,7 @@ cleanup_application() {
acquire_fixture_lock
OWNS_FIXTURE=true
setup_fixture durable coordination target
setup_fixture durable coordination oidc target
source "$RUNTIME/fixture.env"
"$NUTHATCH_REAL_NERDCTL" --namespace "$NERDCTL_NAMESPACE" build \
--file "$ROOT/Dockerfile" \
@ -104,12 +118,13 @@ while IFS=$'\t' read -r lane coordination oidc target; do
"$CURL_LOCAL_IMAGE" \
"$profile"
sed -i "s|@@LANE@@|$lane|g" "$RUNTIME/$job.yaml"
printf 'Running lane %s: coordination=%s target=%s\n' "$lane" "$coordination" "$target"
printf 'Running lane %s: coordination=%s oidc=%s target=%s\n' \
"$lane" "$coordination" "$oidc" "$target"
run_job_manifest "$job" "$RUNTIME/$job.yaml"
cleanup_application "$lane"
cleanup_lane_state "$lane" "$coordination" "$oidc" "$target"
cleanup_lane_resources "$job" "$profile"
done < <(expand_fixture_matrix coordination target)
done < <(expand_fixture_matrix coordination oidc target)
SUCCEEDED=true
printf 'Nuthatch WP-05 app gate passed\n'
printf 'Nuthatch OIDC app gate passed\n'