- Add input validation: username regex + email format check in pipeline - Quote variables in .provision-env to prevent shell injection - Remove dead source command (each Woodpecker command is separate shell) - Use jq to build JSON payloads (prevents injection via group names) - Clean up git-crypt key on failure (use ; instead of &&) - Add Kyverno ndots lifecycle ignore to webhook-handler deployment
160 lines
7 KiB
YAML
160 lines
7 KiB
YAML
when:
|
|
event: manual
|
|
|
|
clone:
|
|
git:
|
|
image: woodpeckerci/plugin-git
|
|
settings:
|
|
attempts: 5
|
|
backoff: 10s
|
|
|
|
steps:
|
|
- name: validate-inputs
|
|
image: alpine
|
|
commands:
|
|
- |
|
|
if [ -z "$USERNAME" ] || [ -z "$EMAIL" ]; then
|
|
echo "ERROR: USERNAME and EMAIL variables are required"
|
|
echo "Trigger with: POST /api/repos/1/pipelines {branch:master, variables:{USERNAME:x, EMAIL:y}}"
|
|
exit 1
|
|
fi
|
|
# Validate username: lowercase alphanumeric + dash/underscore, 2-63 chars
|
|
if ! echo "$USERNAME" | grep -qE '^[a-z0-9][a-z0-9_-]{0,61}[a-z0-9]$'; then
|
|
echo "ERROR: USERNAME must be 2-63 chars, lowercase alphanumeric/dash/underscore"
|
|
exit 1
|
|
fi
|
|
# Validate email: basic format check
|
|
if ! echo "$EMAIL" | grep -qE '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
|
echo "ERROR: EMAIL must be a valid email address"
|
|
exit 1
|
|
fi
|
|
echo "Provisioning user: $USERNAME ($EMAIL)"
|
|
echo "export PROVISION_USERNAME='$USERNAME'" > .provision-env
|
|
echo "export PROVISION_EMAIL='$EMAIL'" >> .provision-env
|
|
|
|
- name: prepare
|
|
image: alpine
|
|
commands:
|
|
- "apk update && apk add jq curl git git-crypt"
|
|
# git-crypt for secrets/ directory
|
|
- |
|
|
curl -k https://10.0.20.100:6443/api/v1/namespaces/woodpecker/configmaps/git-crypt-key \
|
|
-H "Authorization:Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
|
|
| jq -r .data.key | base64 -d > /tmp/key
|
|
- "git-crypt unlock /tmp/key; rm -f /tmp/key"
|
|
# Vault: authenticate via K8s service account JWT
|
|
- |
|
|
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
VAULT_TOKEN=$(curl -s -X POST http://vault-active.vault.svc.cluster.local:8200/v1/auth/kubernetes/login \
|
|
-d "{\"role\":\"ci\",\"jwt\":\"$SA_TOKEN\"}" | jq -r .auth.client_token)
|
|
echo "export VAULT_TOKEN=$VAULT_TOKEN" > .vault-env
|
|
echo "export VAULT_ADDR=http://vault-active.vault.svc.cluster.local:8200" >> .vault-env
|
|
|
|
- name: update-vault-kv
|
|
image: alpine
|
|
commands:
|
|
- "apk update && apk add jq curl"
|
|
# Read current platform secret
|
|
- |
|
|
. .provision-env && . .vault-env
|
|
CURRENT=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
|
|
"$VAULT_ADDR/v1/secret/data/platform" | jq -r '.data.data')
|
|
|
|
# Parse current k8s_users (stored as JSON string)
|
|
CURRENT_USERS=$(echo "$CURRENT" | jq -r '.k8s_users')
|
|
|
|
# Check if user already exists
|
|
if echo "$CURRENT_USERS" | jq -e --arg u "$PROVISION_USERNAME" '.[$u]' >/dev/null 2>&1; then
|
|
echo "User $PROVISION_USERNAME already exists in k8s_users — skipping Vault KV update"
|
|
exit 0
|
|
fi
|
|
|
|
# Add new user with convention defaults
|
|
UPDATED_USERS=$(echo "$CURRENT_USERS" | jq --arg u "$PROVISION_USERNAME" --arg e "$PROVISION_EMAIL" \
|
|
'. + {($u): {"role":"namespace-owner","email":$e,"namespaces":[$u],"domains":[],"quota":{"cpu_requests":"2","memory_requests":"4Gi","memory_limits":"8Gi","pods":"20"}}}')
|
|
|
|
# Write back full platform secret with updated k8s_users (as JSON string)
|
|
PAYLOAD=$(echo "$CURRENT" | jq --arg users "$UPDATED_USERS" '.k8s_users = $users')
|
|
|
|
curl -s -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
|
|
"$VAULT_ADDR/v1/secret/data/platform" \
|
|
-d "{\"data\": $PAYLOAD}" | jq .
|
|
|
|
echo "Added $PROVISION_USERNAME to k8s_users in Vault"
|
|
|
|
- name: create-authentik-groups
|
|
image: alpine
|
|
commands:
|
|
- "apk update && apk add jq curl"
|
|
- |
|
|
source .provision-env && source .vault-env
|
|
|
|
# Get Authentik API token from Vault
|
|
AUTHENTIK_TOKEN=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
|
|
"$VAULT_ADDR/v1/secret/data/viktor" | jq -r '.data.data.authentik_api_token')
|
|
AUTHENTIK_URL="https://authentik.viktorbarzin.me"
|
|
|
|
# Create sops-USERNAME group if it doesn't exist
|
|
SOPS_GROUP="sops-$PROVISION_USERNAME"
|
|
EXISTING=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
"$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results | length')
|
|
|
|
if [ "$EXISTING" = "0" ]; then
|
|
GROUP_PAYLOAD=$(jq -n --arg name "$SOPS_GROUP" '{"name": $name, "is_superuser": false}')
|
|
GROUP_PK=$(curl -s -X POST -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
"$AUTHENTIK_URL/api/v3/core/groups/" \
|
|
-d "$GROUP_PAYLOAD" | jq -r '.pk')
|
|
echo "Created Authentik group $SOPS_GROUP (pk=$GROUP_PK)"
|
|
else
|
|
GROUP_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
"$AUTHENTIK_URL/api/v3/core/groups/?name=$SOPS_GROUP" | jq -r '.results[0].pk')
|
|
echo "Authentik group $SOPS_GROUP already exists (pk=$GROUP_PK)"
|
|
fi
|
|
|
|
# Find the user by username
|
|
USER_PK=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
"$AUTHENTIK_URL/api/v3/core/users/?username=$PROVISION_USERNAME" | jq -r '.results[0].pk')
|
|
|
|
if [ "$USER_PK" = "null" ] || [ -z "$USER_PK" ]; then
|
|
echo "WARNING: User $PROVISION_USERNAME not found in Authentik — group assignment skipped"
|
|
echo "The user may not have signed up yet. Groups will need manual assignment."
|
|
exit 0
|
|
fi
|
|
|
|
# Add user to sops group
|
|
CURRENT_MEMBERS=$(curl -s -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
"$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" | jq -r '.users')
|
|
UPDATED_MEMBERS=$(echo "$CURRENT_MEMBERS" | jq --argjson uid "$USER_PK" '. + [$uid] | unique')
|
|
|
|
curl -s -X PATCH -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
"$AUTHENTIK_URL/api/v3/core/groups/$GROUP_PK/" \
|
|
-d "{\"users\": $UPDATED_MEMBERS}" | jq .
|
|
|
|
echo "Added user $PROVISION_USERNAME (pk=$USER_PK) to group $SOPS_GROUP"
|
|
|
|
- name: notify-apply-needed
|
|
image: curlimages/curl
|
|
commands:
|
|
- |
|
|
. .provision-env
|
|
echo "User $PROVISION_USERNAME added to Vault KV and Authentik sops group."
|
|
echo "Manual step needed: apply vault + rbac + woodpecker stacks."
|
|
echo " cd stacks/vault && ../../scripts/tg apply --non-interactive"
|
|
echo " cd stacks/rbac && ../../scripts/tg apply --non-interactive"
|
|
echo " cd stacks/woodpecker && ../../scripts/tg apply --non-interactive"
|
|
|
|
- name: slack
|
|
image: curlimages/curl
|
|
commands:
|
|
- |
|
|
. .provision-env 2>/dev/null || true
|
|
curl -s -X POST -H 'Content-type: application/json' \
|
|
--data "{\"channel\":\"general\",\"text\":\"Woodpecker CI: User provisioned — $PROVISION_USERNAME added to Vault KV + Authentik. Run: cd stacks/vault && ../../scripts/tg apply --non-interactive && cd ../rbac && ../../scripts/tg apply --non-interactive\"}" \
|
|
"$SLACK_WEBHOOK" || true
|
|
environment:
|
|
SLACK_WEBHOOK:
|
|
from_secret: slack_webhook
|
|
when:
|
|
status: [success, failure]
|