setup gitea as oidc provider for tool vault

This commit is contained in:
2024-09-27 18:21:52 +02:00
parent 1332def067
commit 407bf12165
24 changed files with 655 additions and 20 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.terraform
.terraform.*
.DS_Store
node_modules/

View File

@@ -58,7 +58,8 @@ issues: http://example.com/issue/tracker
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry',
# and '.git' are always filtered. Mutually exclusive with 'manifest'
build_ignore: []
build_ignore:
- playwright/
# A dict controlling use of manifest directives used in building the collection artifact. The key 'directives' is a
# list of MANIFEST.in style

View File

@@ -0,0 +1,16 @@
---
- name: hashicorp_vault
# hosts: raspberries:&local
hosts: localhost
# debugger: on_failed
vars_prompt:
- name: gitea_admin_password
prompt: Enter gitea admin password
unsafe: true # password can contain uncommon chars such as '{'
tasks:
- name: Setup Hashicorp Vault
include_role:
name: hashicorp_vault

View File

@@ -1,6 +0,0 @@
---
- name: pgbouncer
hosts: raspberries:&local
tasks:
- ansible.builtin.ping:

View File

@@ -1,6 +0,0 @@
---
- name: prometheus
hosts: raspberries:&local
tasks:
- ansible.builtin.ping:

View File

@@ -0,0 +1,10 @@
vault_unseal_keys_path: ~/.arcodange/cluster-keys.json
vault_unseal_keys_shares: 1
vault_unseal_keys_key_threshold: 1 # keys_key_threshold <= keys_shares
vault_address: https://vault.arcodange.duckdns.org
vault_oidc_gitea_setupGiteaAppJS: '{{ role_path }}/files/playwright_setupGiteaApp.js'
gitea_admin_user: arcodange@gmail.com
gitea_admin_password: "{{ undef(hint='You must specify the gitea user admin password') }}"

View File

@@ -0,0 +1,94 @@
terraform {
backend "gcs" {
bucket = "arcodange-tf"
prefix = "tools/hashicorp_vault/gitea_oidc"
}
}
variable "vault_address" {
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
type = string
}
variable "admin_email" {
type = string
default = "arcodange@gmail.com"
}
variable "gitea_app" {
type = object({
url = optional(string, "https://gitea.arcodange.duckdns.org/")
id = string
secret = string
description = optional(string, "Arcodange Gitea Auth")
})
}
terraform {
required_providers {
vault = {
source = "vault"
version = "4.4.0"
}
}
}
provider vault {
address = var.vault_address
token = var.vault_token
}
resource "vault_jwt_auth_backend" "gitea" {
description = var.gitea_app.description
default_role = "default"
path = "gitea"
type = "oidc"
oidc_discovery_url = var.gitea_app.url
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 = []
audit_non_hmac_response_keys = []
default_lease_ttl = "768h"
listing_visibility = "unauth"
max_lease_ttl = "768h"
passthrough_request_headers = []
token_type = "default-service"
}
}
resource "vault_jwt_auth_backend_role" "gitea" {
backend = vault_jwt_auth_backend.gitea.path
role_name = "default"
token_policies = ["default"]
user_claim = "email"
role_type = "oidc"
allowed_redirect_uris = [
"${var.vault_address}/ui/vault/auth/gitea/oidc/callback",
]
}
data "vault_policy_document" "admin" {
rule {
path = "*"
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
description = "admin privileges"
}
}
resource "vault_policy" "admin" {
name = "admin"
policy = data.vault_policy_document.admin.hcl
}
resource "vault_identity_entity" "admin" {
name = var.admin_email
policies = [vault_policy.admin.name]
}
resource "vault_identity_entity_alias" "admin" {
name = var.admin_email
mount_accessor = vault_jwt_auth_backend.gitea.accessor
canonical_id = vault_identity_entity.admin.id
}

View File

@@ -0,0 +1,94 @@
import { chromium } from 'playwright';
/*
Initialisation
*/
const username = process.env.GITEA_USER;
const password = process.env.GITEA_PASSWORD;
const debug = Boolean(process.env.DEBUG);
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.duckdns.org';
if (!username || !password) {
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
process.exit(1);
}
const browser = await chromium.launch({
headless: true,
locale: "gb-GB", // before login gitea use gb-GB locale, after login it choose user locale
logger: {
isEnabled: (name, severity) => debug,
log: (name, severity, message, args) => console.warn(`${severity}| ${name} :: ${message} __ ${args}`)
},
});
const context = await browser.newContext({locale: "gb-GB"});
const page = await context.newPage();
async function doLogin() {
await page.goto(giteaAddress);
await page.click('text=Sign In');
await page.fill('input[name="user_name"]', username);
await page.fill('input[name="password"]', password);
await page.click('button:has-text("Sign In")');
await page.waitForURL(giteaAddress);
}
async function isLoggedIn() {
return (
await page.locator('text=Sign In').count() === 0
&& await page.locator('.user-menu > .ui.header strong').count() > 0
)
}
// async function getLoggedUsername() {
// const loggedInUser = await page.innerText('.user-menu > .ui.header strong');
// if (debug) console.warn(`Connecté en tant que : ${loggedInUser}`);
// return loggedInUser;
// }
async function setupApp() {
const appName = process.env.GITEA_APP_NAME || 'Arcodange Hashicorp Vault';
await page.goto(`${giteaAddress}/admin/applications`);
const applicationsPanel = await page.locator('.admin-setting-content');
const applicationNameClass = await applicationsPanel.getByText('Git Credential Manager').getAttribute('class');
const appNames = await applicationsPanel.locator(`.${applicationNameClass}`).allInnerTexts();
const app = {};
if (appNames.includes(appName)) {
console.warn('app found');
const appElem = await applicationsPanel.locator(`.${applicationNameClass}`).getByText(appName).locator('xpath=../..');
await appElem.highlight();
await appElem.locator('a.button').click();
await page.waitForURL( new RegExp(`${giteaAddress}/admin/applications/oauth2/\\d+$`) );
await applicationsPanel.locator('form[action$="/regenerate_secret"] > button').click();
} else {
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`
].join('\n'));
await applicationsPanel.locator('form[action="/admin/applications/oauth2"] > button').dblclick()
await page.waitForURL(`${giteaAddress}/admin/applications/oauth2`);
}
app.id = await applicationsPanel.locator('input[id="client-id"]').getAttribute('value');
app.secret = await applicationsPanel.locator('input[id="client-secret"]').getAttribute('value');
return app;
}
if (! await isLoggedIn()) await doLogin();
// let _ = await getLoggedUsername()
const app = await setupApp();
console.log(JSON.stringify(app));
await browser.close();

View File

@@ -0,0 +1,45 @@
# Usage: prompt for gitea_admin_password in the calling playbook
# vars_prompt:
# - name: gitea_admin_password
# prompt: Enter gitea admin password
# unsafe: true # password can contain uncommon chars such as '{'
- set_fact:
playwright_script: '{{ vault_oidc_gitea_setupGiteaAppJS }}'
playwright_env:
GITEA_USER: '{{ gitea_admin_user }}'
GITEA_PASSWORD: '{{ gitea_admin_password }}'
VAULT_ADDRESS: '{{ vault_address }}'
- include_role:
name: arcodange.factory.playwright
- set_fact:
gite_app: '{{ playwright_job.stdout | from_json }}'
volume_name: tofu-{{ ansible_date_time.iso8601.replace(':','-') }}
- name: use tofu to provision vault
block:
- shell: docker volume create {{ volume_name }}
- shell: >-
docker run --rm
-v {{ volume_name }}:/tofu -w /tofu
-v {{ role_path }}/files/hashicorp_vault.tf:/tofu/hashicorp_vault.tf
-v ~/.config/gcloud:/root/.config/gcloud
--entrypoint=''
ghcr.io/opentofu/opentofu:latest
{{ command }}
loop:
- tofu init -no-color
- >-
tofu apply -auto-approve -no-color
-var='gitea_app={{ gite_app | to_json }}'
-var='vault_address={{ vault_address }}'
-var='vault_token={{ vault_root_token }}'
loop_control:
loop_var: command
always:
- shell: docker volume rm {{ volume_name }}

View File

@@ -0,0 +1,34 @@
- name: list vault servers to unseal
ansible.builtin.command:
kubectl -n tools get pods -l app.kubernetes.io/instance=hashicorp-vault -l component=server -o jsonpath="{.items[*]['.metadata.name']}"
register: vault_servers_cmd
changed_when: False
- ansible.builtin.set_fact:
vault_servers: '{{ vault_servers_cmd.stdout.split() }}'
- ansible.builtin.shell:
kubectl -n tools exec {{ vault_servers[0] }} -- vault operator init -status
register: vault_init_status
failed_when: vault_init_status.rc == 1
changed_when: False
- when: vault_init_status.rc == 2
block:
- ansible.builtin.debug:
msg: not initialized
- ansible.builtin.command: >-
kubectl -n tools exec {{ vault_servers[0] }} -- vault operator init
-format=json
-key-shares={{ vault_unseal_keys_shares }}
-key-threshold={{ vault_unseal_keys_key_threshold }}
register: vault_keys_cmd
- ansible.builtin.file:
state: directory
path: '{{ vault_unseal_keys_path | dirname }}'
mode: '700'
- ansible.builtin.copy:
content: '{{ vault_keys_cmd.stdout }}'
dest: '{{ vault_unseal_keys_path }}'
mode: '600'

View File

@@ -0,0 +1,15 @@
- name: Init (first time only)
include_tasks: init.yml
- name: Unseal (required on reboot)
include_tasks: unseal.yml
- block:
- name: Generate root token
include_tasks: new_root_token.yml
- name: Setup gitea oidc auth
include_tasks: gitea_oidc_auth.yml
always:
- name: Revoke root token
include_tasks: revoke_token.yml
vars:
vault_token_to_revoke: '{{ vault_root_token }}'

View File

@@ -0,0 +1,32 @@
- name: 'Generate New Root Token'
include_tasks: vault_cmd.yml
vars:
vault_cmd: '{{ item.cmd }}'
vault_cmd_output_var: '{{ item.save }}'
vault_cmd_json_attr: '{{ item.json_attr | default("") }}'
vault_cmd_can_fail: '{{ item.can_fail | default(false) }}'
vault_unseal_keys: "{{ (lookup('ansible.builtin.file', vault_unseal_keys_path) | from_json)['unseal_keys_b64'] }}"
cmds:
- cmd: vault token revoke -self
save: false
can_fail: true
- cmd: vault operator generate-root -generate-otp
save: vault_generate_root_otp
- cmd: !unsafe vault operator generate-root -cancel -otp {{ vault_generate_root_otp }}
- cmd: !unsafe vault operator generate-root -init -otp {{ vault_generate_root_otp }}
save: vault_generate_root_nonce
json_attr: 'nonce'
- |- # not yet tested with vault_unseal_keys_key_threshold > 1 (saving not available encoded_root_token might break)
{{
vault_unseal_keys[:vault_unseal_keys_key_threshold] | map('regex_replace', '(.+)', '{
"cmd": "vault operator generate-root -nonce {{ vault_generate_root_nonce }} \1",
"save": "vault_generate_root_encoded_token",
"json_attr": "encoded_root_token"
}') | map("from_json") | list
}}
- cmd: !unsafe vault operator generate-root -decode {{ vault_generate_root_encoded_token }} -otp {{ vault_generate_root_otp }}
save: vault_root_token
- cmd: !unsafe vault login {{ vault_root_token }}
save: false
loop: '{{ cmds | flatten }}'

View File

@@ -0,0 +1,3 @@
- shell: >-
kubectl exec -n tools hashicorp-vault-0 --
vault token revoke {{ vault_token_to_revoke | default('-self') }}

View File

@@ -0,0 +1,23 @@
- debug:
msg: reading from {{ vault_unseal_keys_path }}
- ansible.builtin.set_fact:
vault_keys: "{{ lookup('ansible.builtin.file', vault_unseal_keys_path) | from_json }}"
- ansible.builtin.command:
kubectl -n tools exec {{ vault_server__key[0] }} -- vault operator unseal {{ vault_server__key[1] }}
loop: '{{ vault_servers | product(vault_keys["unseal_keys_b64"]) }}'
loop_control:
loop_var: vault_server__key
label: 'unsealing {{ vault_server__key[0] }}'
- block:
- ansible.builtin.command:
kubectl -n tools exec {{ vault_servers[0] }} -- vault login {{ vault_keys["root_token"] }}
- name: Revoke root token
include_tasks: revoke_token.yml
vars:
vault_token_to_revoke: '-self'
rescue:
- debug:
msg: 'initial root token already revoked'

View File

@@ -0,0 +1,23 @@
- set_fact:
vault_cmd_interpolated: '{{ vault_cmd }}'
- name: 'variable interpolation in vault_cmd'
set_fact:
vault_cmd_interpolated: "{{ vault_cmd_interpolated | regex_replace('\\{\\{ *' + varname + ' *\\}\\}', lookup('vars', varname, default='')) }}"
loop: "{{ vault_cmd | regex_findall('\\{\\{ *([a-zA-Z_][a-zA-Z0-9_]*) *\\}\\}') }}"
loop_control:
loop_var: varname
- ansible.builtin.shell: >-
kubectl exec -n tools hashicorp-vault-0 -- {{ 'env VAULT_FORMAT=json' if vault_cmd_json_attr != '' else '' }}
{{ vault_cmd_interpolated }}
register: vault_cmd_output
ignore_errors: '{{ vault_cmd_can_fail }}'
- when: vault_cmd_output_var is not false
ansible.builtin.set_fact:
'{{ vault_cmd_output_var | default("vault_cmd_out") }}' : |-
{{ vault_cmd_output.stdout if not vault_cmd_json_attr else
(vault_cmd_output.stdout | from_json)[vault_cmd_json_attr]
}}

View File

@@ -1,6 +1,3 @@
---
- name: pgbouncer
ansible.builtin.import_playbook: pgbouncer.yml
- name: prometheus
ansible.builtin.import_playbook: prometheus.yml
- name: hashicorp_vault
ansible.builtin.import_playbook: hashicorp_vault.yml

View File

@@ -0,0 +1,4 @@
playwright_script: '{{ {{ role_path }}/files/loginGitea.js }}'
playwright_use_docker: true
playwright_version: '1.47.0'
playwright_docker_image: playwright:{{ playwright_version }}

View File

@@ -0,0 +1,25 @@
# Utiliser l'image officielle Node.js avec Playwright
FROM mcr.microsoft.com/playwright:v1.38.0-jammy
ARG playwright_version=1.47.0
# Définir le répertoire de travail
WORKDIR /app
# Copier les fichiers package.json et package-lock.json
COPY v${playwright_version}/package*.json ./
# Installer les dépendances Node.js
RUN npm install
# Installer les navigateurs nécessaires pour Playwright
RUN npx playwright install
# Copier le script par défaut
COPY loginGitea.js ./script.js
# Commande pour exécuter le script
CMD ["node", "script.js"]
# RUN WITH
# docker run -v $PWD/loginGitea.js:/app/loginGitea.js --rm playwright-gitea sh -c "sed 's/headless: false/headless: true/' loginGitea.js | node --input-type=module"

View File

@@ -0,0 +1,55 @@
import { chromium } from 'playwright';
/*
Initialisation
*/
const username = process.env.GITEA_USER;
const password = process.env.GITEA_PASSWORD;
const debug = Boolean(process.env.DEBUG);
const vaultAddress = process.env.VAULT_ADDRESS || 'http://localhost:8200';
const giteaAddress = process.env.GITEA_ADDRESS || 'https://gitea.arcodange.duckdns.org';
if (!username || !password) {
console.error('Veuillez définir les variables d\'environnement GITEA_USER et GITEA_PASSWORD.');
process.exit(1);
}
const browser = await chromium.launch({
headless: true,
locale: "gb-GB", // before login gitea use gb-GB locale, after login it choose user locale
logger: {
isEnabled: (name, severity) => debug,
log: (name, severity, message, args) => console.warn(`${severity}| ${name} :: ${message} __ ${args}`)
},
});
const context = await browser.newContext({locale: "gb-GB"});
const page = await context.newPage();
async function doLogin() {
await page.goto(giteaAddress);
await page.click('text=Sign In');
await page.fill('input[name="user_name"]', username);
await page.fill('input[name="password"]', password);
await page.click('button:has-text("Sign In")');
await page.waitForURL(giteaAddress);
}
async function isLoggedIn() {
return (
await page.locator('text=Sign In').count() === 0
&& await page.locator('.user-menu > .ui.header strong').count() > 0
)
}
async function getLoggedUsername() {
const loggedInUser = await page.innerText('.user-menu > .ui.header strong');
if (debug) console.warn(`Connecté en tant que : ${loggedInUser}`);
return loggedInUser;
}
if (! await isLoggedIn()) await doLogin();
const giteaUser = await getLoggedUsername()
console.log(JSON.stringify({giteaUser}));
await browser.close();

View File

@@ -0,0 +1,70 @@
{
"name": "playwright",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"playwright": "^1.47.0",
"typescript": "^5.6.2"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz",
"integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==",
"dependencies": {
"playwright-core": "1.47.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz",
"integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/typescript": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "playwright",
"version": "1.0.0",
"main": "loginGitea.js",
"type": "module",
"scripts": {
"test": "node loginGitea.js"
},
"keywords": [],
"author": "arcodange@gmail.com",
"license": "ISC",
"description": "",
"dependencies": {
"playwright": "^1.47.0"
}
}

View File

@@ -0,0 +1,25 @@
- when: playwright_use_docker
block:
- name: Build {{ playwright_docker_image }} docker image
community.docker.docker_image_build:
name: '{{ playwright_docker_image }}'
path: '{{ role_path }}/files/'
args:
playwright_version: '{{ playwright_version }}'
- name: run {{ playwright_script | basename }}
vars:
cmd_env: '{{ playwright_env | default({}) }}'
env_arguments: >-
{% for e in (cmd_env.keys() | zip( cmd_env.values() ) | map('join', '=') ) %}
-e {{ e }}
{% endfor %}
ansible.builtin.shell: >-
docker run
-v {{ playwright_script }}:/app/script.js
{{ env_arguments }}
--rm
{{ playwright_docker_image }}
# sh -c "sed 's/headless: *false/headless: true/' script.js | node --input-type=module"
register: playwright_job

View File

@@ -0,0 +1,56 @@
# [Bases](./README.md)
## Tools
### Hashicorp Vault
>[!WARNING]
>L'unsealKey, le vaultRootToken initial et l'authentification au backend terraform sont pour le moment configurés sur le controleur ansible (Macbook Pro).
>[!NOTE]
> Vault est déployé via [argo cd](https://gitea.arcodange.duckdns.org/arcodange-org/tools/src/branch/main/hashicorp-vault)
```mermaid
%%{init: { 'logLevel': 'debug', 'theme': 'base',
'sequence': {
'showSequenceNumbers': true,
'mirrorActors': false
}
}}%%
sequenceDiagram
participant Ansible
participant Gitea
participant Vault
Note right of Vault: Argo CD App <br> versioned in Gitea
rect rgb(191, 223, 255)
Ansible ->> Gitea : setupAdminAccount(adminPassword)
Ansible ->> Vault : init
activate Vault
Vault -->> Ansible : (unsealKey, vaultRootToken)
deactivate Vault
Ansible ->> Vault: unseal(unsealKey)
Ansible ->> Vault: revoke vaultRootToken
rect rgb(255, 266, 255)
Ansible ->> Gitea : setupApp(adminPassword)
activate Gitea
Note left of Gitea: docker playwright
deactivate Gitea
Gitea -->> Ansible : app(id,secret)
Ansible ->> Vault : generate vaultRootToken
Ansible ->> Vault : enable oidc auth backend with app(id,secret) <br> give admin policy to admin user
activate Vault
Note left of Vault: docker tofu(vaultRootToken)
deactivate Vault
Ansible ->> Vault: revoke vaultRootToken
end
end
```

View File

@@ -9,11 +9,18 @@
- [x] setup postgres
- [x] setup gitea
- [x] setup mail alert
- [ ] [setup gitea runners, Argo CD](./03_cicd_gitea_action_argocd.md)
- [x] [setup gitea runners, Argo CD](./03_cicd_gitea_action_argocd.md)
- [x] sync git repo with github/gitlab
- [ ] docker hub
- [ ] gitea packages
- [x] gitea packages
- [ ] devsecops tools
- [x] [hashicorp vault](./04_tool_hashicorp_vault.md)
- [ ] terrakube
- [ ] prometheus/grafana
- [ ] ansible AWX
- [ ] setup hello world web app
- [ ] manage postgres credentials
- [ ] protect public endpoint (crowdsec)
> [!NOTE]
> Reference: [Arcodange _**Factory**_ Ansible Collection](/ansible/arcodange/factory/README.md)