diff --git a/ansible/arcodange/factory/inventory/group_vars/hard_disk/gitea.yml b/ansible/arcodange/factory/inventory/group_vars/hard_disk/gitea.yml index c0da59f..5a6fb75 100644 --- a/ansible/arcodange/factory/inventory/group_vars/hard_disk/gitea.yml +++ b/ansible/arcodange/factory/inventory/group_vars/hard_disk/gitea.yml @@ -21,7 +21,7 @@ gitea: external: true services: gitea: - image: gitea/gitea:1.22.1 + image: gitea/gitea:1.22.2 container_name: gitea restart: always environment: @@ -41,6 +41,7 @@ gitea: GITEA__mailer__SMTP_PORT: 465 GITEA__mailer__PASSWD: '{{ gitea_vault.GITEA__mailer__PASSWD }}' GITEA__server__SSH_PORT: 2222 + GITEA__server__SSH_DOMAIN: "{{ lookup('dig', groups.gitea[0]) }}" GITEA__server__SSH_LISTEN_PORT: 22 networks: - gitea diff --git a/ansible/arcodange/factory/playbooks/03_cicd.yml b/ansible/arcodange/factory/playbooks/03_cicd.yml index 28ce3e7..f35817f 100644 --- a/ansible/arcodange/factory/playbooks/03_cicd.yml +++ b/ansible/arcodange/factory/playbooks/03_cicd.yml @@ -37,7 +37,7 @@ - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro extra_hosts: - gitea.arcodange.duckdns.org: '{{ lookup("dig", groups.gitea[0]) }}' + gitea.arcodange.duckdns.org: '{{ lookup("dig", "gitea.arcodange.duckdns.org") }}' configs: - config.yaml configs: diff --git a/ansible/arcodange/factory/playbooks/setup/gitea.yml b/ansible/arcodange/factory/playbooks/setup/gitea.yml index 240ad4c..a6f0dbc 100644 --- a/ansible/arcodange/factory/playbooks/setup/gitea.yml +++ b/ansible/arcodange/factory/playbooks/setup/gitea.yml @@ -192,4 +192,10 @@ body_format: json body: image: "{{ gitea_org_avatar_img['content'] }}" - status_code: 204 \ No newline at end of file + status_code: 204 + + post_tasks: + - include_role: + name: arcodange.factory.gitea_token + vars: + gitea_token_delete: true \ No newline at end of file diff --git a/ansible/arcodange/factory/playbooks/tools/hashicorp_vault.yml b/ansible/arcodange/factory/playbooks/tools/hashicorp_vault.yml index 94733ba..67777cf 100644 --- a/ansible/arcodange/factory/playbooks/tools/hashicorp_vault.yml +++ b/ansible/arcodange/factory/playbooks/tools/hashicorp_vault.yml @@ -8,9 +8,25 @@ - name: gitea_admin_password prompt: Enter gitea admin password unsafe: true # password can contain uncommon chars such as '{' + + roles: + - arcodange.factory.gitea_token tasks: - name: Setup Hashicorp Vault include_role: - name: hashicorp_vault \ No newline at end of file + name: hashicorp_vault + + - name: share VAULT CA + block: + + - name: read traefik CA + include_role: + name: arcodange.factory.traefik_certs + + post_tasks: + - include_role: + name: arcodange.factory.gitea_token + vars: + gitea_token_delete: true \ No newline at end of file diff --git a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/hashicorp_vault.tf b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/hashicorp_vault.tf index c1f1ee4..0e9c287 100644 --- a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/hashicorp_vault.tf +++ b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/hashicorp_vault.tf @@ -25,6 +25,10 @@ variable "gitea_app" { }) } +# kubectl -n kube-system exec $(kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik -o jsonpath="{.items[0]['.metadata.name']}") -- cat /data/acme.json | jq '(.letsencrypt.Certificates | map(select(.domain.main=="arcodange.duckdns.org")))[0]' | jq '.certificate' -r | base64 -d | openssl x509 +variable "ca_pem" { + type = string +} terraform { required_providers { vault = { @@ -45,9 +49,11 @@ resource "vault_jwt_auth_backend" "gitea" { path = "gitea" type = "oidc" oidc_discovery_url = var.gitea_app.url + oidc_discovery_ca_pem = var.ca_pem oidc_client_id = var.gitea_app.id oidc_client_secret = var.gitea_app.secret bound_issuer = var.gitea_app.url + tune { allowed_response_headers = [] audit_non_hmac_request_keys = [] @@ -65,13 +71,50 @@ resource "vault_jwt_auth_backend_role" "gitea" { role_name = "default" token_policies = ["default"] + user_claim = "email" role_type = "oidc" allowed_redirect_uris = [ + "http://localhost:8250/oidc/callback", # for command line login "${var.vault_address}/ui/vault/auth/gitea/oidc/callback", + "https://webapp.arcodange.duckdns.org/oauth-callback", ] } +resource "vault_jwt_auth_backend" "gitea_jwt" { + description = var.gitea_app.description + default_role = "gitea_cicd" + path = "gitea_jwt" + type = "jwt" + oidc_discovery_url = var.gitea_app.url + oidc_discovery_ca_pem = var.ca_pem + bound_issuer = var.gitea_app.url + + tune { + allowed_response_headers = [] + audit_non_hmac_request_keys = [] + audit_non_hmac_response_keys = [] + default_lease_ttl = "15m" + listing_visibility = "hidden" + max_lease_ttl = "15m" + passthrough_request_headers = [] + token_type = "default-batch" + } +} + +resource "vault_jwt_auth_backend_role" "gitea_jwt_cicd" { + backend = vault_jwt_auth_backend.gitea_jwt.path + role_name = "gitea_cicd" + token_policies = ["default"] + + bound_audiences = [ + var.gitea_app.id, + ] + + user_claim = "email" + role_type = "jwt" +} + data "vault_policy_document" "admin" { rule { path = "*" @@ -87,8 +130,29 @@ resource "vault_identity_entity" "admin" { name = var.admin_email policies = [vault_policy.admin.name] } -resource "vault_identity_entity_alias" "admin" { +resource "vault_identity_entity_alias" "admin_gitea" { name = var.admin_email mount_accessor = vault_jwt_auth_backend.gitea.accessor canonical_id = vault_identity_entity.admin.id +} +resource "vault_identity_entity_alias" "admin_gitea_jwt" { + name = var.admin_email + mount_accessor = vault_jwt_auth_backend.gitea_jwt.accessor + canonical_id = vault_identity_entity.admin.id +} + +resource "vault_mount" "kvv1" { + path = "kvv1" + type = "kv" + options = { version = "1" } + description = "KV Version 1 secret engine mount" +} + +resource "vault_kv_secret" "google_credential" { + path = "${vault_mount.kvv1.path}/google/credentials" + data_json = jsonencode( + { + credentials = file("~/.config/gcloud/application_default_credentials.json") + } + ) } \ No newline at end of file diff --git a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/playwright_setupGiteaApp.js b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/playwright_setupGiteaApp.js index 60dfe68..d6ec3e1 100644 --- a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/playwright_setupGiteaApp.js +++ b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/files/playwright_setupGiteaApp.js @@ -72,7 +72,9 @@ async function setupApp() { console.warn('app not found'); await applicationsPanel.locator('input[name="application_name"]').fill(appName); await applicationsPanel.locator('textarea[name="redirect_uris"]').fill([ - `${vaultAddress}/ui/vault/auth/gitea/oidc/callback` + 'http://localhost:8250/oidc/callback', // for command line login + `${vaultAddress}/ui/vault/auth/gitea/oidc/callback`, + 'https://webapp.arcodange.duckdns.org/oauth-callback', ].join('\n')); await applicationsPanel.locator('form[action="/admin/applications/oauth2"] > button').dblclick() diff --git a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml index 0b0d4a3..31baad7 100644 --- a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml +++ b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/tasks/gitea_oidc_auth.yml @@ -15,8 +15,11 @@ - include_role: name: arcodange.factory.playwright +- include_role: + name: arcodange.factory.traefik_certs + - set_fact: - gite_app: '{{ playwright_job.stdout | from_json }}' + gitea_app: '{{ playwright_job.stdout | from_json }}' volume_name: tofu-{{ ansible_date_time.iso8601.replace(':','-') }} @@ -31,15 +34,35 @@ --entrypoint='' ghcr.io/opentofu/opentofu:latest {{ command }} + register: last_tofu_command loop: - tofu init -no-color - >- tofu apply -auto-approve -no-color - -var='gitea_app={{ gite_app | to_json }}' + -var='gitea_app={{ gitea_app | to_json }}' -var='vault_address={{ vault_address }}' -var='vault_token={{ vault_root_token }}' + -var='ca_pem={{ traefik_cert_pem }}' loop_control: loop_var: command + extended: true + label: tofu command n°{{ ansible_loop.index0 }} always: - - shell: docker volume rm {{ volume_name }} \ No newline at end of file + - shell: docker volume rm {{ volume_name }} + - #when: last_tofu_command.stderr | length > 0 + debug: + var: last_tofu_command + # msg: '{{ last_tofu_command.stderr }}' + +- include_role: + name: arcodange.factory.gitea_secret + vars: + gitea_secret_name: vault_oauth__sh_b64 + gitea_secret_value: >- + {{ lookup('ansible.builtin.template', 'oidc_jwt_token.sh.j2', template_vars = { + 'GITEA_BASE_URL': 'https://gitea.arcodange.duckdns.org', + 'OIDC_CLIENT_ID': gitea_app.id, + 'OIDC_CLIENT_SECRET': gitea_app.secret, + }) | b64encode }} + gitea_owner_type: 'org' # value != 'user' \ No newline at end of file diff --git a/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/templates/oidc_jwt_token.sh.j2 b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/templates/oidc_jwt_token.sh.j2 new file mode 100644 index 0000000..00ca013 --- /dev/null +++ b/ansible/arcodange/factory/playbooks/tools/roles/hashicorp_vault/templates/oidc_jwt_token.sh.j2 @@ -0,0 +1,107 @@ +#!/bin/bash +set -eu + +# Variables à ajuster selon ta configuration +CLIENT_ID="{{ OIDC_CLIENT_ID }}" +CLIENT_SECRET="{{ OIDC_CLIENT_SECRET }}" +REDIRECT_URI="{{ OIDC_CLIENT_CALLBACK | default('https://webapp.arcodange.duckdns.org/oauth-callback') }}" # Redirige ici après l'authentification +AUTH_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.duckdns.org') }}/login/oauth/authorize" +TOKEN_URL="{{ GITEA_BASE_URL | default('https://gitea.arcodange.duckdns.org') }}/login/oauth/access_token" +ISSUER="https://gitea.arcodange.duckdns.org/" +# SCOPE="openid email profile groups" # Scope que tu souhaites obtenir - profile groups +SCOPE="email openid read:user" # Scope que tu souhaites obtenir - profile groups +set +u +STATE="$RANDOM-$RANDOM-$RANDOM-$RANDOM-$RANDOM-$RANDOM" # Anti-CSRF (valeur aléatoire) +set -u +CODE="" + +MAX_ATTEMPTS=100 # Nombre maximum de tentatives +INTERVAL=5 # Intervalle en secondes entre chaque tentative + +# Fonction pour effectuer le polling +poll_state() { + local attempt=1 + + while [ $attempt -le $MAX_ATTEMPTS ]; do + #echo "Tentative $attempt/$MAX_ATTEMPTS: Requête à l'endpoint /retrieve pour state=$STATE..." + + # Effectuer la requête GET + RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/response_body "https://webapp.arcodange.duckdns.org/retrieve?state=$STATE") + HTTP_CODE=$(tail -n1 <<< "$RESPONSE") + + if [ "$HTTP_CODE" == "200" ]; then + CODE=$(cat /tmp/response_body) + echo '' + return 0 + elif [ "$HTTP_CODE" == "404" ]; then + echo -n "." + else + echo "Erreur lors de la requête (HTTP $HTTP_CODE)." + return 1 + fi + + # Attendre avant de refaire une requête + sleep $INTERVAL + attempt=$((attempt + 1)) + done + + echo "Échec après $MAX_ATTEMPTS tentatives, code non trouvé." + return 1 +} + +# 1. Rediriger l'utilisateur vers l'URL d'authentification +echo "Ouvrez le lien suivant dans votre navigateur pour vous authentifier dans Gitea:" +echo "$AUTH_URL?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&response_type=code&scope=$(sed 's/ /%20/g' <<<$SCOPE)&state=$STATE" + + +# Device Flow simulation (non implémenté par Gitea) + +if [ -n ${GITHUB_ACTOR:-} ]; then + >&2 echo "hello $GITHUB_ACTOR :)" + poll_state +else + # 2. L'utilisateur doit coller le code ici + # stty -echo + printf "Collez le code d'autorisation (code+state) ici après authentification: " + read CODE_STATE + # stty echo + CODE=`awk '{print $2}' <<< $CODE_STATE` + p_STATE=`awk '{print $4}' <<< $CODE_STATE` + + if [ $STATE != $p_STATE ]; then + >&2 echo 'mauvais csrf (state)' + exit 1 + fi +fi + +# 3. Échanger le code d'autorisation contre un access token +RESPONSE=$(curl -s -X POST $TOKEN_URL \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "grant_type=authorization_code" \ + -d "scope=$SCOPE" \ + -d "code=$CODE" \ + -d "redirect_uri=$REDIRECT_URI") + +# 4. Extraire l'access token de la réponse +ACCESS_TOKEN=$(echo $RESPONSE | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') +ID_TOKEN=$(echo $RESPONSE | sed -n 's/.*"id_token":"\([^"]*\)".*/\1/p') + +# 5. Vérifier si le token a été obtenu et l'afficher +if [ -z "$ACCESS_TOKEN" ]; then + echo "Erreur: Impossible d'obtenir un token d'accès." + echo "Réponse de l'API: $RESPONSE" + exit 1 +fi + +if [ -n ${GITHUB_ACTOR:-} ]; then + >&2 echo "using `cut -d '.' -f 2 <<< $ID_TOKEN | base64 -d 2>/dev/null | jq '.email'` account" + echo "::add-mask::$ACCESS_TOKEN" + echo "::add-mask::$ID_TOKEN" + echo "id_token=$ID_TOKEN" >> $GITHUB_OUTPUT + echo "access_token=$ACCESS_TOKEN" >> $GITHUB_OUTPUT +else + echo -n "$ACCESS_TOKEN" > .access_token + echo -n "$ID_TOKEN" > .id_token +fi diff --git a/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml b/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml index 92f71ac..a07a87e 100644 --- a/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml +++ b/ansible/arcodange/factory/roles/gitea_repo/defaults/main.yml @@ -6,4 +6,4 @@ gitea_username: arcodange gitea_organization: arcodange-org # URL de base du serveur Gitea -gitea_base_url: http://{{ groups.gitea[0] }}:3000 +gitea_base_url: http://{{ groups.gitea[0] }}:3000 \ No newline at end of file diff --git a/ansible/arcodange/factory/roles/gitea_repo/tasks/main.yml b/ansible/arcodange/factory/roles/gitea_repo/tasks/main.yml index 6cccfa9..2dcd952 100644 --- a/ansible/arcodange/factory/roles/gitea_repo/tasks/main.yml +++ b/ansible/arcodange/factory/roles/gitea_repo/tasks/main.yml @@ -1,8 +1,3 @@ -- name: Generate Gitea Token - when: gitea_api_token is undefined - include_role: - name: arcodange.factory.gitea_token - - name: Vérifier si le dépôt existe dans Gitea uri: url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_organization }}/{{ gitea_repo_name }}" diff --git a/ansible/arcodange/factory/roles/gitea_secret/tasks/main.yml b/ansible/arcodange/factory/roles/gitea_secret/tasks/main.yml index e6fdf76..78e09cd 100644 --- a/ansible/arcodange/factory/roles/gitea_secret/tasks/main.yml +++ b/ansible/arcodange/factory/roles/gitea_secret/tasks/main.yml @@ -1,7 +1,3 @@ -- name: Generate Gitea Token - include_role: - name: arcodange.factory.gitea_token - - name: Préparer l'URL de l'API pour mettre à jour ou ajouter un secret set_fact: gitea_api_url: | diff --git a/ansible/arcodange/factory/roles/gitea_sync/defaults/main.yml b/ansible/arcodange/factory/roles/gitea_sync/defaults/main.yml index 507389f..5bc0185 100644 --- a/ansible/arcodange/factory/roles/gitea_sync/defaults/main.yml +++ b/ansible/arcodange/factory/roles/gitea_sync/defaults/main.yml @@ -2,4 +2,6 @@ gitea_username: arcodange gitea_organization: arcodange-org # URL de base du serveur Gitea -gitea_base_url: http://{{ groups.gitea[0] }}:3000 \ No newline at end of file +gitea_base_url: http://{{ groups.gitea[0] }}:3000 + +gitea_token_fact_name: arcodange_factory_gitea_sync_token \ No newline at end of file diff --git a/ansible/arcodange/factory/roles/gitea_sync/tasks/main.yml b/ansible/arcodange/factory/roles/gitea_sync/tasks/main.yml index b8e15ca..a54b5df 100644 --- a/ansible/arcodange/factory/roles/gitea_sync/tasks/main.yml +++ b/ansible/arcodange/factory/roles/gitea_sync/tasks/main.yml @@ -16,10 +16,6 @@ status_code: 200 register: gitlab_repos -- name: Generate Gitea Token - include_role: - name: arcodange.factory.gitea_token - - name: Lister les dépôts de l'organisation Gitea uri: url: "{{ gitea_base_url }}/api/v1/orgs/{{ gitea_organization }}/repos" diff --git a/ansible/arcodange/factory/roles/gitea_token/tasks/main.yml b/ansible/arcodange/factory/roles/gitea_token/tasks/main.yml index e161c49..7f5b66e 100644 --- a/ansible/arcodange/factory/roles/gitea_token/tasks/main.yml +++ b/ansible/arcodange/factory/roles/gitea_token/tasks/main.yml @@ -1,7 +1,10 @@ # to see generated tokens # go to https://gitea.arcodange.duckdns.org/user/settings/applications -- when: lookup('ansible.builtin.varnames', '^' ~ gitea_token_fact_name ~ '$') | length == 0 or gitea_token_delete +- when: >- + lookup('ansible.builtin.varnames', '^' ~ gitea_token_fact_name ~ '$') | length == 0 + or lookup('vars', gitea_token_fact_name) == 'deleted' + or gitea_token_delete block: - &createTokenTask @@ -46,5 +49,11 @@ msg: 'WARN: gitea_api_token required when gitea_token_delete or gitea_token_replace is true' - ansible.builtin.set_fact: - '{{ gitea_token_fact_name }}': '{{ (gitea_api_token_cmd.rc == 0) | ternary(gitea_api_token_cmd.stdout, gitea_api_token_cmd_bis.stdout) }}' - when: not gitea_token_delete \ No newline at end of file + '{{ gitea_token_fact_name }}': >- + {{ + 'deleted' if gitea_token_delete else + ( + (gitea_api_token_cmd.rc == 0) + | ternary(gitea_api_token_cmd.stdout, gitea_api_token_cmd_bis.stdout) + ) + }} \ No newline at end of file diff --git a/ansible/arcodange/factory/roles/traefik_certs/defaults/main.yml b/ansible/arcodange/factory/roles/traefik_certs/defaults/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/ansible/arcodange/factory/roles/traefik_certs/tasks/main.yml b/ansible/arcodange/factory/roles/traefik_certs/tasks/main.yml new file mode 100644 index 0000000..003a0c8 --- /dev/null +++ b/ansible/arcodange/factory/roles/traefik_certs/tasks/main.yml @@ -0,0 +1,11 @@ +- when: traefik_certs_pem is not defined + block: + - shell: >- + kubectl -n kube-system exec + $(kubectl -n kube-system get pod -l app.kubernetes.io/name=traefik + -o jsonpath="{.items[0]['.metadata.name']}") -- + cat /data/acme.json | jq '(.letsencrypt.Certificates | map(select(.domain.main=="arcodange.duckdns.org")))[0]' + | jq '.certificate' -r | base64 -d | openssl x509 + register: traefik_certs_cmd + - set_fact: + traefik_cert_pem: '{{ traefik_certs_cmd.stdout }}' \ No newline at end of file diff --git a/doc/adr/04_tool_hashicorp_vault.md b/doc/adr/04_tool_hashicorp_vault.md index 264b26d..5d1b1a6 100644 --- a/doc/adr/04_tool_hashicorp_vault.md +++ b/doc/adr/04_tool_hashicorp_vault.md @@ -35,7 +35,7 @@ sequenceDiagram Ansible ->> Vault: unseal(unsealKey) Ansible ->> Vault: revoke vaultRootToken - rect rgb(255, 266, 255) + rect rgb(255, 255, 255) Ansible ->> Gitea : setupApp(adminPassword) activate Gitea @@ -53,4 +53,19 @@ sequenceDiagram end end + + rect rgb(180,150,100) + Ansible ->> Vault : share Google Credentials for open tofu GCS backend + Ansible ->> Gitea : gives oidc auth script for vault + activate Gitea + rect rgb(255,255,255) + Gitea ->> Vault: auth with oidc auth script + create actor Admin AS Admin User + Gitea ->> Admin: poll for Admin login + Note left of Admin: copy paste link
generated by
oidc auth script + Vault ->> Gitea: Google Credentials for open tofu GCS backend + Gitea ->> Vault: configure vault with open tofu + end + deactivate Gitea + end ``` \ No newline at end of file