feat(provision): automated user provisioning via Authentik webhook
- Expand CI Vault policy: write secret/data/platform + Transit SOPS keys - Add Woodpecker provision-user.yml pipeline (manual event, API-triggered) - Add env vars to webhook-handler deployment for Woodpecker/Authentik integration - Update add-user skill with automated flow documentation - Update Woodpecker repo ID list in CLAUDE.md
This commit is contained in:
parent
82b9dd9e8a
commit
fd130971aa
5 changed files with 287 additions and 22 deletions
196
.woodpecker/provision-user.yml
Normal file
196
.woodpecker/provision-user.yml
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
when:
|
||||
event: manual
|
||||
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
attempts: 5
|
||||
backoff: 10s
|
||||
|
||||
steps:
|
||||
- name: validate-inputs
|
||||
image: alpine
|
||||
commands:
|
||||
- |
|
||||
if [ -z "${CI_PIPELINE_VARIABLE_USERNAME}" ] || [ -z "${CI_PIPELINE_VARIABLE_EMAIL}" ]; then
|
||||
echo "ERROR: USERNAME and EMAIL variables are required"
|
||||
echo "Trigger with: POST /api/repos/{id}/pipelines {branch:master, variables:{USERNAME:x, EMAIL:y}}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Provisioning user: ${CI_PIPELINE_VARIABLE_USERNAME} (${CI_PIPELINE_VARIABLE_EMAIL})"
|
||||
# Write vars to shared file for subsequent steps
|
||||
echo "export PROVISION_USERNAME=${CI_PIPELINE_VARIABLE_USERNAME}" > .provision-env
|
||||
echo "export PROVISION_EMAIL=${CI_PIPELINE_VARIABLE_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 /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"
|
||||
- "source .provision-env && source .vault-env"
|
||||
# Read current platform secret
|
||||
- |
|
||||
source .provision-env && source .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_PK=$(curl -s -X POST -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$AUTHENTIK_URL/api/v3/core/groups/" \
|
||||
-d "{\"name\": \"$SOPS_GROUP\", \"is_superuser\": false}" | 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: terragrunt-apply
|
||||
image: alpine
|
||||
backend_options:
|
||||
kubernetes:
|
||||
resources:
|
||||
requests:
|
||||
memory: 3Gi
|
||||
limits:
|
||||
memory: 6Gi
|
||||
commands:
|
||||
- "apk update && apk add curl unzip git openssh-client python3 py3-pip py3-yaml"
|
||||
# Install sops
|
||||
- "wget -qO /usr/local/bin/sops https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64"
|
||||
- "chmod 755 /usr/local/bin/sops"
|
||||
# Install Terraform
|
||||
- "wget -qO /tmp/terraform.zip https://releases.hashicorp.com/terraform/1.5.7/terraform_1.5.7_linux_amd64.zip"
|
||||
- "unzip -o /tmp/terraform.zip -d /usr/local/bin/ && chmod 755 /usr/local/bin/terraform"
|
||||
# Install Terragrunt
|
||||
- "wget -qO /usr/local/bin/terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/v0.99.4/terragrunt_linux_amd64"
|
||||
- "chmod 755 /usr/local/bin/terragrunt"
|
||||
# Source Vault token
|
||||
- "source .vault-env"
|
||||
# Apply stacks sequentially: vault → rbac → cloudflared → woodpecker
|
||||
- |
|
||||
source .vault-env
|
||||
export VAULT_ADDR
|
||||
export VAULT_TOKEN
|
||||
for stack in vault rbac cloudflared woodpecker; do
|
||||
echo "=== Applying stack: $stack ==="
|
||||
cd "stacks/$stack"
|
||||
# Decrypt state
|
||||
../../scripts/state-sync decrypt "$stack" || true
|
||||
# Apply
|
||||
terragrunt apply --non-interactive -auto-approve -backup=-
|
||||
# Encrypt state
|
||||
../../scripts/state-sync encrypt "$stack" || true
|
||||
cd ../..
|
||||
echo "=== Done: $stack ==="
|
||||
done
|
||||
|
||||
- name: commit-and-push
|
||||
image: alpine
|
||||
commands:
|
||||
- "apk update && apk add openssh-client git git-crypt"
|
||||
- "mkdir -p ~/.ssh && ssh-keyscan -H github.com >> ~/.ssh/known_hosts"
|
||||
- "chmod 400 secrets/deploy_key"
|
||||
- |
|
||||
. .provision-env
|
||||
# Only add state files
|
||||
git add state/ || true
|
||||
git remote set-url origin git@github.com:ViktorBarzin/infra.git
|
||||
git commit -m "feat(provision): auto-provision user ${PROVISION_USERNAME} [CI SKIP]" || echo "No changes to commit"
|
||||
GIT_SSH_COMMAND='ssh -i ./secrets/deploy_key -o IdentitiesOnly=yes' git pull --rebase origin master || true
|
||||
GIT_SSH_COMMAND='ssh -i ./secrets/deploy_key -o IdentitiesOnly=yes' git push origin master
|
||||
when:
|
||||
status: [success, failure]
|
||||
|
||||
- name: slack
|
||||
image: curlimages/curl
|
||||
commands:
|
||||
- |
|
||||
curl -s -X POST -H 'Content-type: application/json' \
|
||||
--data "{\"channel\":\"general\",\"text\":\"Woodpecker CI: User provisioning for ${CI_PIPELINE_VARIABLE_USERNAME:-unknown} ${CI_PIPELINE_STATUS}\"}" \
|
||||
"$SLACK_WEBHOOK" || true
|
||||
environment:
|
||||
SLACK_WEBHOOK:
|
||||
from_secret: slack_webhook
|
||||
when:
|
||||
status: [success, failure]
|
||||
Loading…
Add table
Add a link
Reference in a new issue