Premiers rôles ansible et leurs playbooks de déploiement (Phases 1 et 2)
Some checks are pending
CI / yaml-lint (push) Waiting to run
CI / ssot-export (push) Waiting to run
CI / tests (push) Waiting to run
CI / docs (push) Waiting to run

This commit is contained in:
Dan Allaire 2025-11-01 17:57:46 -04:00
parent 51862096bb
commit bb38f846cd
58 changed files with 4024 additions and 144 deletions

387
ansible/README-PHASE2.md Normal file
View file

@ -0,0 +1,387 @@
# 🌲 Alliance Boréale - Phase 2 : Identité & Forge
Extension de l'infrastructure avec SSO (Keycloak) et Forge Git (Forgejo).
---
## 📋 SERVICES DÉPLOYÉS
| Service | VMID | FQDN | IP | Couche | Port(s) |
|---------|------|------|----|----|---------|
| **Keycloak** | 03010 | `sso.infra.chezlepro.ca` | 10.0.3.20 | C3 | 443 (HTTPS) |
| **Forgejo** | 04021 | `git.infra.chezlepro.ca` | 10.0.1.20 | C4 | 443 (HTTPS), 2222 (SSH) |
---
## 🚀 DÉPLOIEMENT PHASE 2
### 1. Créer les VMs
```bash
# VM Keycloak
VMID: 03010
RAM: 4GB (Keycloak = gourmand en Java)
CPU: 2 cores
Disk: 30GB
OS: Ubuntu 22.04 LTS
IP: 10.0.3.20
# VM Forgejo
VMID: 04021
RAM: 2GB
CPU: 2 cores
Disk: 50GB (repos Git)
OS: Ubuntu 22.04 LTS
IP: 10.0.1.20
```
### 2. Mettre à jour l'inventaire
```bash
# Copier le nouvel inventaire
cp inventories/production/hosts-phase2.yml inventories/production/hosts.yml
# Éditer les IPs si nécessaire
vi inventories/production/hosts.yml
```
### 3. Générer les secrets Phase 2
```bash
# Mots de passe
openssl rand -base64 24 # Keycloak admin
openssl rand -base64 24 # Keycloak DB
openssl rand -base64 24 # Forgejo admin
openssl rand -base64 24 # Forgejo DB
# Secrets Forgejo (64 chars hex)
openssl rand -hex 64 # secret_key
openssl rand -hex 64 # internal_token
openssl rand -hex 64 # jwt_secret
```
### 4. Mettre à jour le vault
```bash
# Éditer vault
ansible-vault edit vault/production.yml
# Ajouter les secrets Phase 2 (voir vault/production-phase2.yml.example)
```
### 5. Déployer Keycloak d'abord
```bash
# Déployer seulement Keycloak
ansible-playbook playbooks/phase2-deploy.yml --limit idp_servers --ask-vault-pass
```
### 6. Configurer Keycloak
```bash
# Accéder à Keycloak
https://sso.infra.chezlepro.ca
# Login admin (vault_keycloak_admin_password)
# Le realm "alliance-boreale" devrait être créé automatiquement
# Aller dans: Realm "alliance-boreale" → Clients → "forgejo"
```
### 7. Récupérer le Client Secret Forgejo
```
Keycloak UI:
1. Realm: alliance-boreale
2. Clients → forgejo
3. Credentials tab
4. Copier "Client Secret"
```
### 8. Ajouter le secret au vault
```bash
ansible-vault edit vault/production.yml
# Ajouter:
vault_keycloak_forgejo_client_secret: "<secret-copié-depuis-keycloak>"
```
### 9. Déployer Forgejo
```bash
# Déployer Forgejo avec SSO configuré
ansible-playbook playbooks/phase2-deploy.yml --limit forge_servers --ask-vault-pass
```
---
## ✅ VALIDATION POST-DÉPLOIEMENT
### Test Keycloak
```bash
# Accès web
curl -k https://sso.infra.chezlepro.ca/realms/alliance-boreale
# Devrait retourner du JSON avec config du realm
```
### Test Forgejo
```bash
# Accès web
curl -k https://git.infra.chezlepro.ca
# Devrait retourner HTML de la page d'accueil
# Test SSH
ssh -T -p 2222 git@git.infra.chezlepro.ca
# Devrait afficher: "Hi there, You've successfully authenticated..."
```
### Test SSO (Forgejo → Keycloak)
```
1. Aller sur https://git.infra.chezlepro.ca
2. Cliquer "Sign in with L'Alliance Boréale SSO"
3. Devrait rediriger vers Keycloak
4. Créer un compte ou se connecter
5. Devrait rediriger vers Forgejo connecté
```
---
## 👥 GESTION UTILISATEURS
### Créer des utilisateurs dans Keycloak
```
Keycloak UI:
1. Realm: alliance-boreale
2. Users → Add user
3. Remplir: username, email, first name, last name
4. Save
5. Credentials tab → Set Password
6. Temporary: OFF
7. Save
```
### Donner accès admin Forgejo
```
SSH sur serveur Forgejo:
ssh ansible@10.0.1.20
# Promouvoir utilisateur admin
sudo -u git /opt/forgejo/forgejo admin user change-password \
--username <username> \
--password <new-password> \
--config /etc/forgejo/app.ini
# Ou donner rôle admin
sudo -u git /opt/forgejo/forgejo admin user create \
--admin \
--username <username> \
--email <email> \
--password <password> \
--config /etc/forgejo/app.ini
```
---
## 📦 PREMIERS DÉPÔTS
### Créer organisation "alliance-boreale"
```
Forgejo UI:
1. Sign in via SSO
2. + → New Organization
3. Name: alliance-boreale
4. Visibility: Public
5. Create
```
### Créer dépôts essentiels
```
Dans l'organisation "alliance-boreale":
1. ansible-infrastructure
Description: Infrastructure as Code
Private: No
Initialize: Yes (README.md)
2. documentation
Description: Documentation technique
Private: No
3. registraire
Description: Registraire membres (Document 14)
Private: No
```
### Pousser ce dépôt Ansible
```bash
cd /chemin/vers/ansible
# Ajouter remote Forgejo
git remote add forgejo git@git.infra.chezlepro.ca:2222/alliance-boreale/ansible-infrastructure.git
# Pousser
git push -u forgejo main
```
---
## 🔄 INTÉGRATION CONTINUE (CI/CD)
Forgejo Actions (GitHub Actions compatible) est activé.
### Exemple workflow `.forgejo/workflows/lint.yml`
```yaml
name: Ansible Lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Ansible
run: |
pip3 install ansible ansible-lint
- name: Run ansible-lint
run: |
ansible-lint playbooks/*.yml
```
---
## 🔐 SÉCURITÉ
### Certificats SSL
Phase 2 utilise des **certificats auto-signés** par défaut.
**Pour production** : Utiliser Let's Encrypt
```yaml
# Dans group_vars/idp_servers.yml et forge_servers.yml
nginx:
ssl:
cert_source: letsencrypt
letsencrypt_email: admin@chezlepro.ca
```
Puis redéployer :
```bash
ansible-playbook playbooks/phase2-deploy.yml --tags nginx
```
### Firewall
Les règles nftables sont automatiquement configurées :
```bash
# Vérifier sur les serveurs
sudo nft list ruleset | grep -A 10 "Keycloak\|Forgejo"
```
### Backups PostgreSQL
Configurés automatiquement :
- **Keycloak DB** : Backup quotidien à 3h, rétention 14 jours
- **Forgejo DB** : Backup quotidien à 4h, rétention 14 jours
Vérifier :
```bash
ls -lh /var/backups/postgresql/
```
---
## 🐛 DÉPANNAGE
### Keycloak ne démarre pas
```bash
# Logs
sudo journalctl -u keycloak -n 100 --no-pager
# Vérifier Java
java -version # Devrait être OpenJDK 17
# Vérifier PostgreSQL
sudo -u postgres psql keycloak -c "SELECT version();"
# Rebuild Keycloak
sudo -u keycloak /opt/keycloak/bin/kc.sh build
sudo systemctl restart keycloak
```
### Forgejo ne se connecte pas à Keycloak
```bash
# Vérifier logs Forgejo
sudo journalctl -u forgejo -n 100 --no-pager
# Tester découverte OpenID
curl -k https://sso.infra.chezlepro.ca/realms/alliance-boreale/.well-known/openid-configuration
# Vérifier client secret dans app.ini
sudo grep -A 5 "oauth2.alliance-boreale" /etc/forgejo/app.ini
```
### SSH Forgejo ne fonctionne pas
```bash
# Vérifier port SSH Forgejo
sudo ss -tulnp | grep 2222
# Tester localement
ssh -T -p 2222 git@localhost
# Vérifier firewall
sudo nft list ruleset | grep 2222
# Logs SSH Forgejo
sudo journalctl -u forgejo -f | grep SSH
```
---
## 📚 RÉFÉRENCES
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Forgejo Documentation](https://forgejo.org/docs/)
- [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/)
- [OpenID Connect](https://openid.net/connect/)
---
## 🎯 PROCHAINE ÉTAPE : PHASE 3
**Phase 3 : FastAPI Pivot (C5)**
Services à déployer :
- FastAPI Admin Portal
- API Gateway
- Tenant provisioning
- Monitoring dashboard
---
**Version:** 1.0
**Date:** 2025-10-31
**Membre:** Chezlepro Inc.
**Phases:** 1 (DNS) + 2 (IdP + Forge)

411
ansible/README.md Normal file
View file

@ -0,0 +1,411 @@
# 🌲 Alliance Boréale - Ansible Automation
Infrastructure as Code pour L'Alliance Boréale - Membre: **Chezlepro Inc.**
## 📋 Table des matières
- [Prérequis](#prérequis)
- [Structure du projet](#structure-du-projet)
- [Configuration initiale](#configuration-initiale)
- [Phase 1 : DNS Fédéré](#phase-1--dns-fédéré)
- [Gestion des secrets](#gestion-des-secrets)
- [Commandes utiles](#commandes-utiles)
- [Dépannage](#dépannage)
---
## 🔧 Prérequis
### Sur votre machine de contrôle
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install ansible python3-pip sshpass
# OU via pip (recommandé pour dernière version)
pip3 install ansible ansible-lint
# Collections Ansible requises
ansible-galaxy collection install community.general
ansible-galaxy collection install community.postgresql
ansible-galaxy collection install ansible.posix
```
### Sur les serveurs cibles (VMs)
- Ubuntu 22.04 LTS ou Debian 12
- Accès root temporaire (sera désactivé après)
- Connexion SSH fonctionnelle
---
## 📁 Structure du projet
```
ansible/
├── ansible.cfg # Configuration Ansible
├── README.md # Ce fichier
├── inventories/
│ └── production/
│ ├── hosts.yml # Inventaire des serveurs
│ └── group_vars/
│ ├── all.yml # Variables globales
│ └── dns_servers.yml # Variables DNS
├── roles/
│ ├── common/ # Hardening de base
│ ├── postgresql/ # Base de données
│ └── powerdns-authoritative/ # DNS autoritaire
├── playbooks/
│ └── phase1-dns-deploy.yml # Déploiement Phase 1
└── vault/
└── production.yml # Secrets chiffrés (à créer)
```
---
## ⚙️ Configuration initiale
### 1. Générer clés SSH
```bash
# Générer une paire de clés pour Ansible
ssh-keygen -t ed25519 -f ~/.ssh/alliance_boreale_ed25519 -C "ansible@chezlepro.ca"
# Copier la clé publique sur les serveurs (temporairement en root)
ssh-copy-id -i ~/.ssh/alliance_boreale_ed25519.pub root@10.0.2.10
ssh-copy-id -i ~/.ssh/alliance_boreale_ed25519.pub root@10.0.2.11
```
### 2. Configurer l'inventaire
Éditer `inventories/production/hosts.yml` :
```yaml
# Remplacer les IPs par vos IPs publiques/internes
ns1.infra.chezlepro.ca:
ansible_host: <IP_PUBLIQUE_NS1> # ← MODIFIER ICI
ns2.infra.chezlepro.ca:
ansible_host: <IP_PUBLIQUE_NS2> # ← MODIFIER ICI
```
Éditer `inventories/production/group_vars/all.yml` :
```yaml
# Ajouter vos clés SSH publiques
ssh:
authorized_keys:
- "ssh-ed25519 AAAAC3... votre-email@example.com" # ← AJOUTER ICI
```
### 3. Créer le vault de secrets
```bash
# Créer un mot de passe pour le vault
ansible-vault create vault/production.yml
```
Contenu du vault :
```yaml
---
# Secrets pour Phase 1
vault_pdns_api_key: "changeme-api-key-secure-random"
vault_postgresql_pdns_password: "changeme-db-password-secure-random"
```
**Sauvegarder le mot de passe vault dans un gestionnaire de mots de passe !**
### 4. Tester la connectivité
```bash
# Ping tous les hôtes
ansible all -m ping
# Devrait afficher:
# ns1.infra.chezlepro.ca | SUCCESS => { "ping": "pong" }
# ns2.infra.chezlepro.ca | SUCCESS => { "ping": "pong" }
```
---
## 🚀 Phase 1 : DNS Fédéré
### Déploiement complet
```bash
# Déployer Phase 1 complète
ansible-playbook playbooks/phase1-dns-deploy.yml --ask-vault-pass
# OU avec fichier mot de passe
echo "votre-mot-de-passe-vault" > .vault_pass
chmod 600 .vault_pass
ansible-playbook playbooks/phase1-dns-deploy.yml --vault-password-file .vault_pass
```
### Déploiement par étapes (tags)
```bash
# Seulement hardening (common)
ansible-playbook playbooks/phase1-dns-deploy.yml --tags common
# Seulement PostgreSQL
ansible-playbook playbooks/phase1-dns-deploy.yml --tags postgresql
# Seulement PowerDNS
ansible-playbook playbooks/phase1-dns-deploy.yml --tags powerdns
# Mode dry-run (vérifier sans appliquer)
ansible-playbook playbooks/phase1-dns-deploy.yml --check --diff
```
### Déploiement sur un seul serveur
```bash
# Seulement ns1
ansible-playbook playbooks/phase1-dns-deploy.yml --limit ns1.infra.chezlepro.ca
# Seulement ns2
ansible-playbook playbooks/phase1-dns-deploy.yml --limit ns2.infra.chezlepro.ca
```
---
## 🔐 Gestion des secrets
### Éditer le vault
```bash
# Éditer secrets
ansible-vault edit vault/production.yml
# Voir contenu (sans éditer)
ansible-vault view vault/production.yml
# Changer mot de passe vault
ansible-vault rekey vault/production.yml
```
### Générer mots de passe sécurisés
```bash
# API Key PowerDNS (32 caractères alphanumériques)
openssl rand -base64 32
# Mot de passe PostgreSQL
openssl rand -base64 24
```
---
## 🧪 Validation post-déploiement
### Tests DNS
```bash
# Résolution SOA
dig @10.0.2.10 chezlepro.ca SOA +short
# Résolution NS
dig @10.0.2.10 chezlepro.ca NS +short
# Test DNSSEC
dig @10.0.2.10 chezlepro.ca DNSKEY +dnssec
# Test depuis Internet (après propagation)
dig @8.8.8.8 chezlepro.ca SOA +short
```
### Export DS Records (DNSSEC)
```bash
# Se connecter au DNS master
ssh ansible@10.0.2.10
# Exporter DS records
sudo pdnsutil show-zone chezlepro.ca | grep DS
# Exemple output:
# chezlepro.ca IN DS 12345 13 2 abc123...
```
**Ajouter ces DS records chez votre registraire de domaine !**
### Validation DNSSEC publique
```bash
# Avec DNSViz (après ajout DS records)
# https://dnsviz.net/d/chezlepro.ca/dnssec/
# Ou en ligne de commande
delv @8.8.8.8 chezlepro.ca SOA
# Devrait afficher: "fully validated"
```
### Vérifier services
```bash
# SSH sur serveur
ssh ansible@10.0.2.10
# Status PowerDNS
sudo systemctl status pdns
# Status PostgreSQL
sudo systemctl status postgresql
# Logs PowerDNS
sudo journalctl -u pdns -f
# Firewall rules
sudo nft list ruleset
# API PowerDNS (depuis le serveur)
curl -H "X-API-Key: <votre-api-key>" http://127.0.0.1:8081/api/v1/servers/localhost
```
---
## 🛠️ Commandes utiles
### Ansible ad-hoc
```bash
# Mettre à jour tous les packages
ansible all -b -m apt -a "upgrade=dist update_cache=yes"
# Redémarrer un service
ansible dns_servers -b -m systemd -a "name=pdns state=restarted"
# Collecter facts
ansible all -m setup
# Exécuter commande shell
ansible all -b -a "uptime"
```
### Gestion PowerDNS
```bash
# Lister zones
pdnsutil list-all-zones
# Ajouter un record
pdnsutil add-record chezlepro.ca www A 203.0.113.10 3600
# Supprimer un record
pdnsutil delete-rrset chezlepro.ca www A
# Rectifier zone (après modifications)
pdnsutil rectify-zone chezlepro.ca
# Augmenter serial SOA
pdnsutil increase-serial chezlepro.ca
# Vérifier zone
pdnsutil check-zone chezlepro.ca
```
---
## 🐛 Dépannage
### Erreur: "Failed to connect to the host"
```bash
# Vérifier connectivité
ping 10.0.2.10
# Vérifier SSH manuel
ssh -i ~/.ssh/alliance_boreale_ed25519 ansible@10.0.2.10
# Vérifier port SSH
nmap -p 22 10.0.2.10
```
### Erreur: "Permission denied (publickey)"
```bash
# Vérifier que la clé est chargée
ssh-add -l
# Ajouter la clé si nécessaire
ssh-add ~/.ssh/alliance_boreale_ed25519
# Vérifier authorized_keys sur le serveur
ssh root@10.0.2.10 "cat /home/ansible/.ssh/authorized_keys"
```
### Erreur: "Vault password required"
```bash
# Option 1: Demander interactivement
--ask-vault-pass
# Option 2: Fichier
--vault-password-file .vault_pass
# Option 3: Variable d'environnement
export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
```
### PowerDNS ne démarre pas
```bash
# Voir logs détaillés
sudo journalctl -u pdns -n 100 --no-pager
# Vérifier config
sudo pdns_server --config-check
# Tester connexion PostgreSQL
sudo -u postgres psql powerdns -c "SELECT * FROM domains;"
```
### DNS ne répond pas
```bash
# Vérifier que le port est ouvert
sudo ss -tulnp | grep :53
# Vérifier firewall
sudo nft list ruleset | grep "53"
# Tester en local d'abord
dig @127.0.0.1 chezlepro.ca SOA
# Ensuite depuis l'extérieur
dig @<IP_PUBLIQUE> chezlepro.ca SOA
```
---
## 📚 Documentation de référence
- [Nomenclature v4 (CRB-2)](../00_Nomenclature_biomimetique_autopoietique_v4_CRB2.md)
- [Document 05: Opération DNS Fédérée](/mnt/project/05_Operation_DNS_Federee.md)
- [Document 14: Structure YAML Registraire](/mnt/project/14_Structure_YAML_Registraire.md)
- [PowerDNS Documentation](https://doc.powerdns.com/authoritative/)
- [Ansible Documentation](https://docs.ansible.com/)
---
## 🆘 Support
Pour questions ou problèmes :
1. Consulter les logs : `sudo journalctl -u pdns -f`
2. Vérifier la documentation dans `/mnt/project/`
3. Contacter Cercle Opérationnel via Matrix
---
**Version:** 1.0
**Date:** 2025-10-31
**Membre:** Chezlepro Inc.
**Phase:** 1 (DNS Fédéré - C1-C2)

View file

@ -1,7 +1,59 @@
# Alliance Boréale - Ansible Configuration
# Version: 1.0
# Date: 2025-10-31
[defaults]
inventory = inventories/production/hosts
remote_user = ansible
# Inventaire
inventory = inventories/production/hosts.yml
# Comportement
host_key_checking = False
roles_path = roles
vault_password_file = ~/.alliance-vault-pass.txt
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
# Output
stdout_callback = yaml
bin_ansible_callbacks = True
display_skipped_hosts = False
display_ok_hosts = True
# Performance
forks = 10
poll_interval = 5
timeout = 30
# Privilèges
become = True
become_method = sudo
become_user = root
become_ask_pass = False
# SSH
remote_user = ansible
private_key_file = ~/.ssh/alliance_boreale_ed25519
# Logs
log_path = /var/log/ansible/ansible.log
# Roles
roles_path = roles
# Collections
collections_paths = ~/.ansible/collections:/usr/share/ansible/collections
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
[inventory]
enable_plugins = yaml, ini, host_list

View file

@ -1,9 +1,113 @@
---
# Standards Alliance Boréale
offline_mode: false
alliance_domain: alliance-boreale.org
admin_email: admin@alliance-boreale.org
# Alliance Boréale - Variables Globales
# Membre: Chezlepro Inc.
# Date: 2025-10-31
# NetBox (exemple)
netbox_domain: "{{ inventory_hostname }}.{{ alliance_domain }}"
letsencrypt_email: "{{ admin_email }}"
---
# ==========================================
# IDENTIFICATION MEMBRE
# ==========================================
member:
id: m001
slug: clp
legal_name: "Chezlepro Inc."
domain_primary: chezlepro.ca
# ==========================================
# RÉSEAU
# ==========================================
network:
internal_subnet: 10.0.0.0/8
subnets:
management: 10.0.0.0/24
dns: 10.0.2.0/24
governance: 10.0.3.0/24
forge: 10.0.1.0/24
pivot: 10.0.4.0/24
tenants: 10.0.10.0/23
# ==========================================
# SYSTÈME
# ==========================================
system:
timezone: America/Toronto
locale: en_CA.UTF-8
ntp:
enabled: true
servers:
- 0.ca.pool.ntp.org
- 1.ca.pool.ntp.org
- 2.ca.pool.ntp.org
- 3.ca.pool.ntp.org
# ==========================================
# SSH
# ==========================================
ssh:
port: 22
permit_root_login: false
password_authentication: false
pubkey_authentication: true
authorized_keys:
# Remplacer par vos clés SSH publiques
- "ssh-ed25519 AAAAC3... admin@chezlepro.ca"
# ==========================================
# UTILISATEURS
# ==========================================
users:
ansible:
name: ansible
groups: [sudo]
shell: /bin/bash
sudo_nopasswd: true
# ==========================================
# SÉCURITÉ
# ==========================================
security:
firewall: nftables
fail2ban_enabled: true
unattended_upgrades: true
firewall:
default_policy: drop
allowed_ssh_sources:
- 0.0.0.0/0 # À restreindre en production
# ==========================================
# PACKAGES DE BASE
# ==========================================
base_packages:
- vim
- htop
- curl
- wget
- git
- tree
- tmux
- python3
- python3-pip
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
- net-tools
- dnsutils
- tcpdump
# ==========================================
# MONITORING (Phase 2)
# ==========================================
monitoring:
enabled: false
prometheus_enabled: false
node_exporter_enabled: false
# ==========================================
# LOGS
# ==========================================
logging:
central_enabled: false
syslog_server: null
retention_days: 30

View file

@ -0,0 +1,165 @@
# Alliance Boréale - Variables DNS Servers
# Membre: Chezlepro Inc.
# Date: 2025-10-31
---
# ==========================================
# POWERDNS CONFIGURATION
# ==========================================
powerdns:
version: "4.8"
backend: pgsql
# API Configuration
api:
enabled: true
key: "{{ vault_pdns_api_key }}" # À définir dans vault
webserver:
enabled: true
address: 0.0.0.0
port: 8081
allow_from: 10.0.0.0/8
# DNSSEC
dnssec:
enabled: true
algorithm: ECDSAP256SHA256 # Algorithm 13
# Logging
logging:
level: 4 # Info
queries: false
details: true
# Performance
cache_ttl: 20
negquery_cache_ttl: 60
query_cache_ttl: 20
# ==========================================
# ZONES DNS
# ==========================================
dns_zones:
- name: chezlepro.ca
type: native
soa:
nameserver: ns1.infra.chezlepro.ca
email: admin.chezlepro.ca
serial: 2025103101 # YYYYMMDDNN
refresh: 3600
retry: 1800
expire: 1209600
minimum: 3600
# Nameservers
ns_records:
- ns1.infra.chezlepro.ca
- ns2.infra.chezlepro.ca
# - ns1.infra.nuagelibre.ca # Futur: secondaire croisé
# Records de base
records:
# DNS Servers
- name: ns1.infra
type: A
content: 10.0.2.10
ttl: 3600
- name: ns2.infra
type: A
content: 10.0.2.11
ttl: 3600
# Wildcards (optionnel)
# - name: "*.infra"
# type: A
# content: 10.0.0.1
# ttl: 3600
# ==========================================
# AXFR CONFIGURATION
# ==========================================
axfr:
# IPs autorisées pour AXFR
allow_from:
- 10.0.2.11 # ns2.infra.chezlepro.ca (slave local)
# - <IP_PUBLIQUE_NUL> # Futur: ns1.infra.nuagelibre.ca
# - <IP_PUBLIQUE_TLI> # Futur: ns1.infra.technolibre.ca
# TSIG Keys (optionnel, recommandé)
tsig_enabled: false
tsig_keys: []
# - name: clp-nul-xfer
# algorithm: hmac-sha256
# secret: "{{ vault_tsig_clp_nul }}"
# ==========================================
# NOTIFY CONFIGURATION
# ==========================================
notify:
also_notify:
- 10.0.2.11 # ns2.infra.chezlepro.ca
# - <IP_PUBLIQUE_NUL> # Futur
# - <IP_PUBLIQUE_TLI> # Futur
# ==========================================
# POSTGRESQL CONFIGURATION
# ==========================================
postgresql:
version: 15
listen_addresses: localhost
port: 5432
# Database pour PowerDNS
databases:
- name: powerdns
owner: pdns
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
users:
- name: pdns
password: "{{ vault_postgresql_pdns_password }}"
# role_attr_flags: CREATEDB # Optionnel
# Tuning basique
shared_buffers: 256MB
work_mem: 16MB
maintenance_work_mem: 128MB
effective_cache_size: 1GB
# Backups
backup:
enabled: true
schedule: "0 2 * * *" # 2h du matin
retention_days: 7
destination: /var/backups/postgresql
# ==========================================
# FIREWALL RULES (nftables)
# ==========================================
firewall_rules:
dns:
- port: 53
proto: udp
source: 0.0.0.0/0
comment: "DNS queries (UDP)"
- port: 53
proto: tcp
source: 0.0.0.0/0
comment: "DNS queries (TCP)"
pdns_api:
- port: 8081
proto: tcp
source: 10.0.0.0/8
comment: "PowerDNS API (internal only)"
postgresql:
- port: 5432
proto: tcp
source: 127.0.0.1/32
comment: "PostgreSQL (localhost only)"

View file

@ -0,0 +1,168 @@
# Alliance Boréale - Variables Forgejo (Forge)
# Membre: Chezlepro Inc.
# Date: 2025-10-31
---
# ==========================================
# FORGEJO CONFIGURATION
# ==========================================
forgejo:
version: "1.21" # Version stable
install_method: binary # binary | docker
# Paths
install_dir: /opt/forgejo
data_dir: /var/lib/forgejo
custom_dir: /etc/forgejo
log_dir: /var/log/forgejo
# User
user: git
group: git
# Network
hostname: git.infra.chezlepro.ca
domain: chezlepro.ca
http_port: 3000
ssh_port: 2222 # Éviter conflit avec SSH système
# Database (PostgreSQL)
db:
type: postgres
host: 127.0.0.1:5432
name: forgejo
user: forgejo
password: "{{ vault_postgresql_forgejo_password }}"
ssl_mode: disable
# Security
secret_key: "{{ vault_forgejo_secret_key }}"
internal_token: "{{ vault_forgejo_internal_token }}"
jwt_secret: "{{ vault_forgejo_jwt_secret }}"
# Admin
admin:
username: admin
password: "{{ vault_forgejo_admin_password }}"
email: admin@chezlepro.ca
# SSO (Keycloak OpenID Connect)
oauth:
enabled: true
providers:
- name: alliance-boreale
provider: openidConnect
display_name: "L'Alliance Boréale SSO"
client_id: forgejo
client_secret: "{{ vault_keycloak_forgejo_client_secret }}"
openid_connect_auto_discovery_url: "https://sso.infra.chezlepro.ca/realms/alliance-boreale/.well-known/openid-configuration"
# Features
features:
enable_actions: true # CI/CD
enable_packages: true # Container registry
enable_lfs: true # Git LFS
disable_registration: true # Inscription via SSO uniquement
require_signin: true
# ==========================================
# POSTGRESQL CONFIGURATION (Forgejo)
# ==========================================
postgresql:
version: 15
listen_addresses: localhost
port: 5432
databases:
- name: forgejo
owner: forgejo
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
users:
- name: forgejo
password: "{{ vault_postgresql_forgejo_password }}"
shared_buffers: 512MB
work_mem: 32MB
maintenance_work_mem: 256MB
effective_cache_size: 2GB
backup:
enabled: true
schedule: "0 4 * * *" # 4h du matin
retention_days: 14
destination: /var/backups/postgresql
# ==========================================
# REVERSE PROXY (Nginx)
# ==========================================
nginx:
enabled: true
ssl:
enabled: true
cert_source: self-signed # self-signed | letsencrypt | custom
vhosts:
- server_name: git.infra.chezlepro.ca
listen_port: 443
ssl: true
proxy_pass: http://127.0.0.1:3000
# Large file uploads (Git)
client_max_body_size: 512M
proxy_set_headers:
Host: $host
X-Real-IP: $remote_addr
X-Forwarded-For: $proxy_add_x_forwarded_for
X-Forwarded-Proto: $scheme
# ==========================================
# FIREWALL RULES
# ==========================================
firewall_rules:
forgejo_http:
- port: 3000
proto: tcp
source: 127.0.0.1/32
comment: "Forgejo HTTP (internal only)"
forgejo_ssh:
- port: 2222
proto: tcp
source: 0.0.0.0/0
comment: "Forgejo SSH (Git over SSH)"
nginx:
- port: 443
proto: tcp
source: 0.0.0.0/0
comment: "HTTPS (public)"
- port: 80
proto: tcp
source: 0.0.0.0/0
comment: "HTTP (redirect to HTTPS)"
# ==========================================
# INITIAL REPOSITORIES
# ==========================================
forgejo_initial_repos:
- name: ansible-infrastructure
description: "Infrastructure as Code - L'Alliance Boréale"
private: false
auto_init: true
default_branch: main
- name: documentation
description: "Documentation technique"
private: false
auto_init: true
- name: registraire
description: "Registraire des membres (Document 14)"
private: false
auto_init: true

View file

@ -0,0 +1,129 @@
# Alliance Boréale - Variables Keycloak (IdP)
# Membre: Chezlepro Inc.
# Date: 2025-10-31
---
# ==========================================
# KEYCLOAK CONFIGURATION
# ==========================================
keycloak:
version: "23.0.0" # Version LTS
install_method: standalone # standalone | docker
# Network
hostname: sso.infra.chezlepro.ca
http_port: 8080
https_port: 8443
# Admin
admin_user: admin
admin_password: "{{ vault_keycloak_admin_password }}"
# Database (PostgreSQL)
db:
vendor: postgres
host: localhost
port: 5432
database: keycloak
username: keycloak
password: "{{ vault_postgresql_keycloak_password }}"
# Realm configuration
realms:
- name: alliance-boreale
display_name: "L'Alliance Boréale"
enabled: true
# Clients (services qui utilisent SSO)
clients:
- client_id: forgejo
name: "Forgejo Git"
enabled: true
protocol: openid-connect
redirect_uris:
- "https://git.infra.chezlepro.ca/*"
web_origins:
- "https://git.infra.chezlepro.ca"
# Futurs clients Phase 3
# - client_id: fastapi-pivot
# name: "FastAPI Admin"
# - client_id: matrix
# name: "Matrix Synapse"
# ==========================================
# POSTGRESQL CONFIGURATION (Keycloak)
# ==========================================
postgresql:
version: 15
listen_addresses: localhost
port: 5432
databases:
- name: keycloak
owner: keycloak
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
users:
- name: keycloak
password: "{{ vault_postgresql_keycloak_password }}"
shared_buffers: 512MB
work_mem: 32MB
maintenance_work_mem: 256MB
effective_cache_size: 2GB
backup:
enabled: true
schedule: "0 3 * * *" # 3h du matin
retention_days: 14
destination: /var/backups/postgresql
# ==========================================
# REVERSE PROXY (Nginx)
# ==========================================
nginx:
enabled: true
# SSL/TLS
ssl:
enabled: true
cert_source: self-signed # self-signed | letsencrypt | custom
# letsencrypt_email: admin@chezlepro.ca
# Vhosts
vhosts:
- server_name: sso.infra.chezlepro.ca
listen_port: 443
ssl: true
proxy_pass: http://127.0.0.1:8080
# Headers
proxy_set_headers:
Host: $host
X-Real-IP: $remote_addr
X-Forwarded-For: $proxy_add_x_forwarded_for
X-Forwarded-Proto: $scheme
# ==========================================
# FIREWALL RULES
# ==========================================
firewall_rules:
keycloak:
- port: 8080
proto: tcp
source: 127.0.0.1/32
comment: "Keycloak HTTP (internal only)"
nginx:
- port: 443
proto: tcp
source: 0.0.0.0/0
comment: "HTTPS (public)"
- port: 80
proto: tcp
source: 0.0.0.0/0
comment: "HTTP (redirect to HTTPS)"

View file

@ -1,11 +0,0 @@
[netbox]
netbox-prod ansible_host=203.0.113.10 ansible_user=ansible
[powerdns]
# À remplir plus tard
[keycloak]
# À remplir plus tard
[proxmox]
# À remplir plus tard

View file

@ -0,0 +1,89 @@
# Alliance Boréale - Inventaire Production
# Membre: Chezlepro Inc. (clp)
# Domaine: chezlepro.ca
# Date: 2025-10-31
# Nomenclature: v4 (CRB-2)
---
all:
children:
# ========================================
# COUCHE 2 : RÉSEAU & DNS
# ========================================
dns_servers:
hosts:
ns1.infra.chezlepro.ca:
ansible_host: 10.0.2.10
# ansible_host: <IP_PUBLIQUE> # Décommenter si accès direct
vmid: 02001
vm_name: clp-infra-dns-master-prod-01
dns_role: master
dns_zone_primary: chezlepro.ca
ns2.infra.chezlepro.ca:
ansible_host: 10.0.2.11
# ansible_host: <IP_PUBLIQUE> # Décommenter si accès direct
vmid: 02002
vm_name: clp-infra-dns-slave-prod-01
dns_role: slave
dns_zone_primary: chezlepro.ca
vars:
# Variables communes DNS
pdns_backend: pgsql
pdns_dnssec_enabled: true
pdns_api_enabled: true
pdns_webserver_enabled: true
pdns_webserver_port: 8081
# ========================================
# GROUPES LOGIQUES
# ========================================
dns_masters:
hosts:
ns1.infra.chezlepro.ca:
dns_slaves:
hosts:
ns2.infra.chezlepro.ca:
# PostgreSQL (pour PowerDNS)
postgresql_servers:
hosts:
ns1.infra.chezlepro.ca:
ns2.infra.chezlepro.ca:
# ==========================================
# VARIABLES GLOBALES
# ==========================================
vars:
# Membre
member_slug: clp
member_legal_name: "Chezlepro Inc."
member_domain: chezlepro.ca
# Réseau
network_internal_subnet: 10.0.0.0/8
network_dns_subnet: 10.0.2.0/24
# Ansible
ansible_user: ansible
ansible_become: true
ansible_python_interpreter: /usr/bin/python3
# Timezone
timezone: America/Toronto
# NTP
ntp_servers:
- 0.ca.pool.ntp.org
- 1.ca.pool.ntp.org
- 2.ca.pool.ntp.org
# SSH
ssh_port: 22
ssh_permit_root_login: false
ssh_password_authentication: false
# Sécurité
firewall_backend: nftables

View file

@ -0,0 +1,161 @@
# Alliance Boréale - Playbook Phase 1
# Déploiement DNS Fédéré (Couches C1-C2)
# Membre: Chezlepro Inc.
# Date: 2025-10-31
---
- name: "🌲 Phase 1 - Préparation des VMs (Common)"
hosts: all
become: true
gather_facts: true
pre_tasks:
- name: "🔍 Vérifier connectivité SSH"
ansible.builtin.ping:
tags: always
- name: "📋 Afficher informations hôte"
ansible.builtin.debug:
msg: |
Hôte: {{ inventory_hostname }}
IP: {{ ansible_host }}
VMID: {{ vmid | default('N/A') }}
Rôle DNS: {{ dns_role | default('N/A') }}
tags: always
roles:
- role: common
tags: common
post_tasks:
- name: "✅ Common role terminé"
ansible.builtin.debug:
msg: "✅ Hardening complété sur {{ inventory_hostname }}"
tags: always
# ============================================
- name: "🗄️ Phase 1 - PostgreSQL pour DNS"
hosts: dns_servers
become: true
gather_facts: true
roles:
- role: postgresql
tags: postgresql
post_tasks:
- name: "✅ PostgreSQL terminé"
ansible.builtin.debug:
msg: "✅ PostgreSQL {{ postgresql.version }} configuré sur {{ inventory_hostname }}"
tags: always
# ============================================
- name: "🌐 Phase 1 - PowerDNS Master"
hosts: dns_masters
become: true
gather_facts: true
serial: 1 # Un à la fois pour éviter race conditions
roles:
- role: powerdns-authoritative
tags: powerdns
post_tasks:
- name: "✅ PowerDNS Master terminé"
ansible.builtin.debug:
msg: "✅ PowerDNS MASTER configuré sur {{ inventory_hostname }}"
tags: always
# ============================================
- name: "🌐 Phase 1 - PowerDNS Slave"
hosts: dns_slaves
become: true
gather_facts: true
roles:
- role: powerdns-authoritative
tags: powerdns
post_tasks:
- name: "✅ PowerDNS Slave terminé"
ansible.builtin.debug:
msg: "✅ PowerDNS SLAVE configuré sur {{ inventory_hostname }}"
tags: always
# ============================================
- name: "🧪 Phase 1 - Tests de validation"
hosts: dns_masters
become: false
gather_facts: false
tasks:
- name: "🧪 Test résolution DNS (zone primaire)"
ansible.builtin.command:
cmd: "dig @{{ ansible_host }} {{ member.domain_primary }} SOA +short"
delegate_to: localhost
register: dns_test
changed_when: false
- name: "✅ Résultat test DNS"
ansible.builtin.debug:
msg: "✅ DNS fonctionne: {{ dns_test.stdout }}"
- name: "🧪 Test AXFR (zone transfer)"
ansible.builtin.command:
cmd: "dig @{{ ansible_host }} {{ member.domain_primary }} AXFR +short"
delegate_to: localhost
register: axfr_test
changed_when: false
failed_when: false
- name: "✅ Résultat AXFR"
ansible.builtin.debug:
msg: |
{% if axfr_test.rc == 0 %}
✅ AXFR fonctionne ({{ axfr_test.stdout_lines | length }} lignes)
{% else %}
⚠️ AXFR refusé (normal si ACL strictes configurées)
{% endif %}
# ============================================
- name: "📋 Phase 1 - Récapitulatif"
hosts: localhost
gather_facts: false
tasks:
- name: "🎉 Phase 1 terminée avec succès!"
ansible.builtin.debug:
msg: |
═══════════════════════════════════════════════
🌲 PHASE 1 - DNS FÉDÉRÉ DÉPLOYÉ
═══════════════════════════════════════════════
✅ Serveurs DNS configurés:
{% for host in groups['dns_servers'] %}
- {{ host }} ({{ hostvars[host]['dns_role'] | upper }})
{% endfor %}
🌐 Zone primaire: {{ hostvars[groups['dns_masters'][0]]['member']['domain_primary'] }}
📋 Prochaines étapes:
1. Tester résolution DNS:
dig @{{ hostvars[groups['dns_masters'][0]]['ansible_host'] }} {{ hostvars[groups['dns_masters'][0]]['member']['domain_primary'] }} SOA
2. Exporter DS records pour DNSSEC:
ssh {{ hostvars[groups['dns_masters'][0]]['ansible_user'] }}@{{ hostvars[groups['dns_masters'][0]]['ansible_host'] }}
sudo pdnsutil show-zone {{ hostvars[groups['dns_masters'][0]]['member']['domain_primary'] }} | grep DS
3. Ajouter DS records chez votre registraire de domaine
4. Valider DNSSEC:
dig +dnssec {{ hostvars[groups['dns_masters'][0]]['member']['domain_primary'] }} @8.8.8.8
5. Mettre à jour Registraire YAML:
registraire/membres/m001-chezlepro.yml
6. Passer à Phase 2 (Keycloak + Forgejo)
═══════════════════════════════════════════════
tags: always

View file

@ -0,0 +1,178 @@
# Alliance Boréale - Playbook Phase 2
# Déploiement Identité & Forge (Couches C3-C4)
# Membre: Chezlepro Inc.
# Date: 2025-10-31
---
- name: "🌲 Phase 2 - Préparation VMs (Common)"
hosts: phase2_servers
become: true
gather_facts: true
pre_tasks:
- name: "🔍 Vérifier connectivité SSH"
ansible.builtin.ping:
tags: always
- name: "📋 Afficher informations hôte"
ansible.builtin.debug:
msg: |
Hôte: {{ inventory_hostname }}
IP: {{ ansible_host }}
VMID: {{ vmid | default('N/A') }}
tags: always
roles:
- role: common
tags: common
post_tasks:
- name: "✅ Common terminé"
ansible.builtin.debug:
msg: "✅ Hardening complété sur {{ inventory_hostname }}"
# ============================================
- name: "🔑 Phase 2 - Keycloak (IdP)"
hosts: idp_servers
become: true
gather_facts: true
roles:
- role: postgresql
tags: postgresql
- role: keycloak
tags: keycloak
post_tasks:
- name: "✅ Keycloak terminé"
ansible.builtin.debug:
msg: |
✅ Keycloak déployé sur {{ inventory_hostname }}
URL: https://{{ keycloak.hostname }}
Admin: {{ keycloak.admin_user }}
⚠️ IMPORTANT: Note client secret for Forgejo
tags: always
# ============================================
- name: "🔨 Phase 2 - Forgejo (Forge)"
hosts: forge_servers
become: true
gather_facts: true
roles:
- role: postgresql
tags: postgresql
- role: forgejo
tags: forgejo
post_tasks:
- name: "✅ Forgejo terminé"
ansible.builtin.debug:
msg: |
✅ Forgejo déployé sur {{ inventory_hostname }}
URL: https://{{ forgejo.hostname }}
SSH: git@{{ forgejo.hostname }}:{{ forgejo.ssh_port }}
Admin: {{ forgejo.admin.username }}
tags: always
# ============================================
- name: "🧪 Phase 2 - Tests d'intégration"
hosts: localhost
gather_facts: false
tasks:
- name: "🧪 Test Keycloak availability"
ansible.builtin.uri:
url: "https://{{ hostvars[groups['idp_servers'][0]]['keycloak']['hostname'] }}/realms/alliance-boreale"
validate_certs: false
status_code: 200
register: keycloak_test
delegate_to: localhost
changed_when: false
- name: "✅ Keycloak OK"
ansible.builtin.debug:
msg: "✅ Keycloak répond correctement"
when: keycloak_test.status == 200
- name: "🧪 Test Forgejo availability"
ansible.builtin.uri:
url: "https://{{ hostvars[groups['forge_servers'][0]]['forgejo']['hostname'] }}"
validate_certs: false
status_code: 200
register: forgejo_test
delegate_to: localhost
changed_when: false
- name: "✅ Forgejo OK"
ansible.builtin.debug:
msg: "✅ Forgejo répond correctement"
when: forgejo_test.status == 200
# ============================================
- name: "📋 Phase 2 - Récapitulatif"
hosts: localhost
gather_facts: false
tasks:
- name: "🎉 Phase 2 terminée avec succès!"
ansible.builtin.debug:
msg: |
═══════════════════════════════════════════════
🌲 PHASE 2 - IDENTITÉ & FORGE DÉPLOYÉES
═══════════════════════════════════════════════
✅ Services déployés:
🔑 KEYCLOAK (SSO)
URL: https://{{ hostvars[groups['idp_servers'][0]]['keycloak']['hostname'] }}
Admin: {{ hostvars[groups['idp_servers'][0]]['keycloak']['admin_user'] }}
Realm: alliance-boreale
🔨 FORGEJO (Forge Git)
URL: https://{{ hostvars[groups['forge_servers'][0]]['forgejo']['hostname'] }}
SSH: git@{{ hostvars[groups['forge_servers'][0]]['forgejo']['hostname'] }}:{{ hostvars[groups['forge_servers'][0]]['forgejo']['ssh_port'] }}
Admin: {{ hostvars[groups['forge_servers'][0]]['forgejo']['admin_username'] }}
📋 Configuration SSO:
1. Se connecter à Keycloak:
https://{{ hostvars[groups['idp_servers'][0]]['keycloak']['hostname'] }}
2. Aller dans Realm "alliance-boreale"
3. Clients → "forgejo" → Credentials
Copier le "Client Secret"
4. Ajouter ce secret dans vault:
ansible-vault edit vault/production.yml
vault_keycloak_forgejo_client_secret: "<secret>"
5. Redéployer Forgejo:
ansible-playbook playbooks/phase2-deploy.yml --tags forgejo
📋 Premiers pas:
1. Créer utilisateurs dans Keycloak
2. Se connecter à Forgejo via SSO
3. Créer organisations/repos:
- alliance-boreale/ansible-infrastructure
- alliance-boreale/documentation
- alliance-boreale/registraire
4. Pousser ce dépôt Ansible vers Forgejo:
git remote add origin git@{{ hostvars[groups['forge_servers'][0]]['forgejo']['hostname'] }}:{{ hostvars[groups['forge_servers'][0]]['forgejo']['ssh_port'] }}/alliance-boreale/ansible-infrastructure.git
git push -u origin main
5. Configurer CI/CD (Forgejo Actions)
6. Passer à Phase 3 (FastAPI Pivot)
═══════════════════════════════════════════════
tags: always

View file

@ -0,0 +1,47 @@
# Alliance Boréale - Rôle Common - Default Variables
# Date: 2025-10-31
---
# Ces valeurs sont écrasées par group_vars/all.yml
# Elles servent de fallback si group_vars absent
base_packages:
- vim
- htop
- curl
- git
- python3
system:
timezone: America/Toronto
locale: en_CA.UTF-8
ntp:
enabled: true
servers:
- 0.ca.pool.ntp.org
- 1.ca.pool.ntp.org
ssh:
port: 22
permit_root_login: false
password_authentication: false
pubkey_authentication: true
users:
ansible:
name: ansible
groups: [sudo]
shell: /bin/bash
sudo_nopasswd: true
security:
firewall: nftables
fail2ban_enabled: true
unattended_upgrades: true
firewall:
default_policy: drop
network:
internal_subnet: 10.0.0.0/8

View file

@ -0,0 +1,23 @@
# Alliance Boréale - Rôle Common - Handlers
# Date: 2025-10-31
---
- name: restart chrony
ansible.builtin.systemd:
name: chrony
state: restarted
- name: restart sshd
ansible.builtin.systemd:
name: sshd
state: restarted
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded
- name: restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted

View file

@ -0,0 +1,22 @@
# Alliance Boréale - Rôle Common - Logging Configuration
# Date: 2025-10-31
---
- name: "📊 Ensure rsyslog is installed"
ansible.builtin.apt:
name: rsyslog
state: present
- name: "📊 Configure log rotation"
ansible.builtin.template:
src: logrotate-ansible.j2
dest: /etc/logrotate.d/ansible
owner: root
group: root
mode: '0644'
- name: "✅ Start and enable rsyslog"
ansible.builtin.systemd:
name: rsyslog
state: started
enabled: true

View file

@ -0,0 +1,72 @@
# Alliance Boréale - Rôle Common
# Couche: C1 (Physique)
# Objectif: Hardening post-création VM
# Date: 2025-10-31
---
- name: "🔍 Gather facts"
ansible.builtin.setup:
tags: always
- name: "📦 Update apt cache"
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
tags: packages
- name: "📦 Install base packages"
ansible.builtin.apt:
name: "{{ base_packages }}"
state: present
tags: packages
- name: "🕐 Configure timezone"
community.general.timezone:
name: "{{ system.timezone }}"
tags: system
- name: "🕐 Configure NTP (chrony)"
ansible.builtin.import_tasks: ntp.yml
when: ntp.enabled | default(true)
tags: ntp
- name: "👤 Create ansible user"
ansible.builtin.user:
name: "{{ users.ansible.name }}"
groups: "{{ users.ansible.groups }}"
shell: "{{ users.ansible.shell }}"
create_home: true
state: present
tags: users
- name: "👤 Configure sudo for ansible user"
ansible.builtin.lineinfile:
path: /etc/sudoers.d/ansible
line: "{{ users.ansible.name }} ALL=(ALL) NOPASSWD:ALL"
create: true
mode: '0440'
validate: 'visudo -cf %s'
when: users.ansible.sudo_nopasswd | default(true)
tags: users
- name: "🔐 Configure SSH"
ansible.builtin.import_tasks: ssh.yml
tags: ssh
- name: "🔥 Configure firewall (nftables)"
ansible.builtin.import_tasks: nftables.yml
when: security.firewall == 'nftables'
tags: firewall
- name: "📊 Configure logging"
ansible.builtin.import_tasks: logging.yml
tags: logging
- name: "🔒 Security hardening"
ansible.builtin.import_tasks: security.yml
tags: security
- name: "✅ Common role completed"
ansible.builtin.debug:
msg: "✅ VM {{ inventory_hostname }} hardened successfully"
tags: always

View file

@ -0,0 +1,35 @@
# Alliance Boréale - Rôle Common - nftables Configuration
# Date: 2025-10-31
---
- name: "📦 Install nftables"
ansible.builtin.apt:
name:
- nftables
- python3-nftables
state: present
- name: "🔥 Deploy nftables base rules"
ansible.builtin.template:
src: nftables.conf.j2
dest: /etc/nftables.conf
owner: root
group: root
mode: '0644'
notify: reload nftables
- name: "✅ Enable and start nftables"
ansible.builtin.systemd:
name: nftables
state: started
enabled: true
- name: "🚫 Ensure iptables is not running"
ansible.builtin.systemd:
name: "{{ item }}"
state: stopped
enabled: false
loop:
- iptables
- ip6tables
failed_when: false

View file

@ -0,0 +1,23 @@
# Alliance Boréale - Rôle Common - NTP Configuration
# Date: 2025-10-31
---
- name: "📦 Install chrony"
ansible.builtin.apt:
name: chrony
state: present
- name: "⚙️ Configure chrony"
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: '0644'
notify: restart chrony
- name: "✅ Start and enable chrony"
ansible.builtin.systemd:
name: chrony
state: started
enabled: true

View file

@ -0,0 +1,77 @@
# Alliance Boréale - Rôle Common - Security Hardening
# Date: 2025-10-31
---
- name: "🔒 Disable unnecessary services"
ansible.builtin.systemd:
name: "{{ item }}"
state: stopped
enabled: false
loop:
- bluetooth
- cups
failed_when: false
- name: "🔒 Configure sysctl security parameters"
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { key: 'net.ipv4.conf.all.rp_filter', value: '1' }
- { key: 'net.ipv4.conf.default.rp_filter', value: '1' }
- { key: 'net.ipv4.icmp_echo_ignore_broadcasts', value: '1' }
- { key: 'net.ipv4.conf.all.accept_source_route', value: '0' }
- { key: 'net.ipv4.conf.default.accept_source_route', value: '0' }
- { key: 'net.ipv4.conf.all.send_redirects', value: '0' }
- { key: 'net.ipv4.conf.default.send_redirects', value: '0' }
- { key: 'net.ipv4.tcp_syncookies', value: '1' }
- { key: 'net.ipv4.conf.all.log_martians', value: '1' }
- { key: 'kernel.randomize_va_space', value: '2' }
- name: "🔒 Install fail2ban"
ansible.builtin.apt:
name: fail2ban
state: present
when: security.fail2ban_enabled | default(true)
- name: "🔒 Configure fail2ban"
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
content: |
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
owner: root
group: root
mode: '0644'
when: security.fail2ban_enabled | default(true)
notify: restart fail2ban
- name: "🔒 Enable unattended upgrades"
ansible.builtin.apt:
name: unattended-upgrades
state: present
when: security.unattended_upgrades | default(true)
- name: "🔒 Configure automatic security updates"
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
content: |
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
owner: root
group: root
mode: '0644'
when: security.unattended_upgrades | default(true)

View file

@ -0,0 +1,32 @@
# Alliance Boréale - Rôle Common - SSH Configuration
# Date: 2025-10-31
---
- name: "📦 Ensure OpenSSH server is installed"
ansible.builtin.apt:
name: openssh-server
state: present
- name: "⚙️ Configure SSH daemon"
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0644'
validate: '/usr/sbin/sshd -t -f %s'
notify: restart sshd
- name: "🔑 Add authorized keys for ansible user"
ansible.posix.authorized_key:
user: "{{ users.ansible.name }}"
key: "{{ item }}"
state: present
loop: "{{ ssh.authorized_keys }}"
when: ssh.authorized_keys is defined
- name: "✅ Start and enable SSH"
ansible.builtin.systemd:
name: sshd
state: started
enabled: true

View file

@ -0,0 +1,25 @@
# Alliance Boréale - Chrony Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# NTP Servers
{% for server in ntp.servers %}
pool {{ server }} iburst
{% endfor %}
# Record rate at which system clock gains/losses time
driftfile /var/lib/chrony/chrony.drift
# Allow system to step clock if offset is large
makestep 1.0 3
# Enable kernel synchronization of RTC
rtcsync
# Serve time even if not synchronized
local stratum 10
# Log files
logdir /var/log/chrony
log measurements statistics tracking

View file

@ -0,0 +1,74 @@
#!/usr/sbin/nft -f
# Alliance Boréale - nftables Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# Flush all rules
flush ruleset
# Main table for filtering
table inet filter {
# Input chain (incoming traffic)
chain input {
type filter hook input priority filter; policy {{ firewall.default_policy | default('drop') }};
# Allow established/related connections
ct state established,related accept
# Allow loopback
iif lo accept
# Drop invalid packets
ct state invalid drop
# Allow ICMP (ping)
ip protocol icmp icmp type { echo-request, echo-reply } limit rate 10/second accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, echo-reply, nd-neighbor-solicit, nd-neighbor-advert } limit rate 10/second accept
# Allow SSH
tcp dport {{ ssh.port | default(22) }} ct state new limit rate 5/minute accept
# Allow DNS (if DNS server)
{% if 'dns_servers' in group_names %}
udp dport 53 accept
tcp dport 53 accept
{% endif %}
# Allow PowerDNS API (internal only)
{% if 'dns_servers' in group_names %}
ip saddr {{ network.internal_subnet | default('10.0.0.0/8') }} tcp dport 8081 accept
{% endif %}
# Log dropped packets (rate limited)
limit rate 5/minute log prefix "[nftables INPUT DROP] " level info
# Default: drop
}
# Forward chain (routed traffic)
chain forward {
type filter hook forward priority filter; policy drop;
# Log forwarded packets (if any)
# log prefix "[nftables FORWARD] " level info
}
# Output chain (outgoing traffic)
chain output {
type filter hook output priority filter; policy accept;
# Allow all outgoing by default
}
}
# NAT table (if needed in future)
# table ip nat {
# chain prerouting {
# type nat hook prerouting priority dstnat; policy accept;
# }
#
# chain postrouting {
# type nat hook postrouting priority srcnat; policy accept;
# }
# }

View file

@ -0,0 +1,53 @@
# Alliance Boréale - SSH Daemon Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# Network
Port {{ ssh.port | default(22) }}
AddressFamily inet
ListenAddress 0.0.0.0
# Authentication
PermitRootLogin {{ 'yes' if ssh.permit_root_login | default(false) else 'no' }}
PubkeyAuthentication {{ 'yes' if ssh.pubkey_authentication | default(true) else 'no' }}
PasswordAuthentication {{ 'yes' if ssh.password_authentication | default(false) else 'no' }}
PermitEmptyPasswords no
ChallengeResponseAuthentication no
# Security
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# Ciphers and algorithms
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
# Login grace time
LoginGraceTime 60
MaxAuthTries 3
MaxSessions 10
# Forwarding
X11Forwarding no
AllowTcpForwarding yes
PermitTunnel no
# Keep alive
ClientAliveInterval 300
ClientAliveCountMax 2
# Logging
SyslogFacility AUTH
LogLevel VERBOSE
# Subsystems
Subsystem sftp /usr/lib/openssh/sftp-server
# Allow specific users
AllowUsers {{ users.ansible.name | default('ansible') }}
# Banner (optional)
# Banner /etc/ssh/banner

View file

@ -0,0 +1,18 @@
# Alliance Boréale - Forgejo Handlers
# Date: 2025-10-31
---
- name: restart forgejo
ansible.builtin.systemd:
name: forgejo
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded

View file

@ -0,0 +1,98 @@
# Alliance Boréale - Forgejo Tasks (database, nginx, firewall)
# Date: 2025-10-31
---
# ==========================================
# DATABASE.YML
# ==========================================
- name: "🗄️ Ensure PostgreSQL is installed"
ansible.builtin.include_role:
name: postgresql
when: "'postgresql_servers' in group_names"
- name: "🗄️ Create Forgejo database"
community.postgresql.postgresql_db:
name: "{{ forgejo.db.name }}"
encoding: UTF8
state: present
become: true
become_user: postgres
- name: "👤 Create Forgejo database user"
community.postgresql.postgresql_user:
name: "{{ forgejo.db.user }}"
password: "{{ forgejo.db.password }}"
state: present
become: true
become_user: postgres
no_log: true
- name: "🔐 Grant privileges to Forgejo user"
community.postgresql.postgresql_privs:
database: "{{ forgejo.db.name }}"
roles: "{{ forgejo.db.user }}"
type: database
privs: ALL
state: present
become: true
become_user: postgres
# ==========================================
# NGINX.YML
# ==========================================
- name: "📦 Ensure Nginx is installed"
ansible.builtin.apt:
name: nginx
state: present
- name: "🔐 Generate self-signed SSL certificate"
ansible.builtin.command:
cmd: >
openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout /etc/ssl/private/{{ forgejo.hostname }}.key
-out /etc/ssl/certs/{{ forgejo.hostname }}.crt
-subj "/C=CA/ST=Quebec/L=Montreal/O=Chezlepro/CN={{ forgejo.hostname }}"
creates: "/etc/ssl/certs/{{ forgejo.hostname }}.crt"
when: nginx.ssl.cert_source == 'self-signed'
- name: "⚙️ Configure Nginx for Forgejo"
ansible.builtin.template:
src: nginx-forgejo.conf.j2
dest: /etc/nginx/sites-available/forgejo
owner: root
group: root
mode: '0644'
notify: reload nginx
- name: "🔗 Enable Nginx site"
ansible.builtin.file:
src: /etc/nginx/sites-available/forgejo
dest: /etc/nginx/sites-enabled/forgejo
state: link
notify: reload nginx
- name: "✅ Start and enable Nginx"
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
# ==========================================
# FIREWALL.YML
# ==========================================
- name: "🔥 Configure firewall for Forgejo"
ansible.builtin.blockinfile:
path: /etc/nftables.conf
marker: "# {mark} ANSIBLE MANAGED - Forgejo"
insertbefore: "# Log dropped packets"
block: |
# Forgejo - Internal HTTP (backend only)
ip saddr 127.0.0.1 tcp dport {{ forgejo.http_port }} accept comment "Forgejo HTTP"
# Forgejo - SSH (Git over SSH)
tcp dport {{ forgejo.ssh_port }} accept comment "Forgejo SSH"
# Nginx - HTTPS (public)
tcp dport 443 accept comment "HTTPS (Forgejo via Nginx)"
tcp dport 80 accept comment "HTTP redirect"
notify: reload nftables

View file

@ -0,0 +1,131 @@
# Alliance Boréale - Rôle Forgejo
# Couche: C4 (Forge & Mutualisation)
# Objectif: Déployer Forge Git avec SSO Keycloak
# Date: 2025-10-31
---
- name: "📦 Install dependencies"
ansible.builtin.apt:
name:
- git
- nginx
- openssl
- python3-psycopg2
state: present
update_cache: true
tags: forgejo
- name: "👤 Create git user"
ansible.builtin.user:
name: "{{ forgejo.user }}"
system: true
shell: /bin/bash
home: "{{ forgejo.data_dir }}"
create_home: true
tags: forgejo
- name: "📁 Create forgejo directories"
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ forgejo.user }}"
group: "{{ forgejo.group }}"
mode: '0755'
loop:
- "{{ forgejo.install_dir }}"
- "{{ forgejo.custom_dir }}"
- "{{ forgejo.log_dir }}"
- "{{ forgejo.data_dir }}/git"
- "{{ forgejo.data_dir }}/repositories"
tags: forgejo
- name: "📥 Download Forgejo binary"
ansible.builtin.get_url:
url: "https://codeberg.org/forgejo/forgejo/releases/download/v{{ forgejo.version }}/forgejo-{{ forgejo.version }}-linux-amd64"
dest: "{{ forgejo.install_dir }}/forgejo"
owner: "{{ forgejo.user }}"
group: "{{ forgejo.group }}"
mode: '0755'
tags: forgejo
- name: "🗄️ Configure PostgreSQL for Forgejo"
ansible.builtin.import_tasks: database.yml
tags: forgejo
- name: "⚙️ Configure Forgejo"
ansible.builtin.template:
src: app.ini.j2
dest: "{{ forgejo.custom_dir }}/app.ini"
owner: "{{ forgejo.user }}"
group: "{{ forgejo.group }}"
mode: '0640'
notify: restart forgejo
tags: forgejo
- name: "⚙️ Create systemd service"
ansible.builtin.template:
src: forgejo.service.j2
dest: /etc/systemd/system/forgejo.service
owner: root
group: root
mode: '0644'
notify: restart forgejo
tags: forgejo
- name: "✅ Start and enable Forgejo"
ansible.builtin.systemd:
name: forgejo
state: started
enabled: true
daemon_reload: true
tags: forgejo
- name: "⏳ Wait for Forgejo to be ready"
ansible.builtin.wait_for:
port: "{{ forgejo.http_port }}"
host: 127.0.0.1
timeout: 60
tags: forgejo
- name: "🌐 Configure Nginx reverse proxy"
ansible.builtin.import_tasks: nginx.yml
tags: forgejo
- name: "👑 Create admin user"
ansible.builtin.command:
cmd: >
{{ forgejo.install_dir }}/forgejo admin user create
--username {{ forgejo.admin.username }}
--password {{ forgejo.admin.password }}
--email {{ forgejo.admin.email }}
--admin
--config {{ forgejo.custom_dir }}/app.ini
creates: "{{ forgejo.data_dir }}/.admin_created"
become: true
become_user: "{{ forgejo.user }}"
register: admin_created
no_log: true
tags: forgejo
- name: "✅ Mark admin as created"
ansible.builtin.file:
path: "{{ forgejo.data_dir }}/.admin_created"
state: touch
owner: "{{ forgejo.user }}"
group: "{{ forgejo.group }}"
mode: '0644'
when: admin_created.changed
tags: forgejo
- name: "🔥 Configure firewall"
ansible.builtin.import_tasks: firewall.yml
tags: forgejo
- name: "✅ Forgejo role completed"
ansible.builtin.debug:
msg: |
✅ Forgejo deployed on {{ inventory_hostname }}
URL: https://{{ forgejo.hostname }}
SSH: git@{{ forgejo.hostname }}:{{ forgejo.ssh_port }}
Admin: {{ forgejo.admin.username }}
tags: forgejo

View file

@ -0,0 +1,100 @@
# Alliance Boréale - Forgejo Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
APP_NAME = L'Alliance Boréale - Forge Git
RUN_MODE = prod
RUN_USER = {{ forgejo.user }}
[server]
DOMAIN = {{ forgejo.hostname }}
HTTP_PORT = {{ forgejo.http_port }}
ROOT_URL = https://{{ forgejo.hostname }}/
DISABLE_SSH = false
SSH_DOMAIN = {{ forgejo.hostname }}
SSH_PORT = {{ forgejo.ssh_port }}
SSH_LISTEN_PORT = {{ forgejo.ssh_port }}
START_SSH_SERVER = true
LFS_START_SERVER = {{ forgejo.features.enable_lfs | lower }}
OFFLINE_MODE = false
[database]
DB_TYPE = {{ forgejo.db.type }}
HOST = {{ forgejo.db.host }}
NAME = {{ forgejo.db.name }}
USER = {{ forgejo.db.user }}
PASSWD = {{ forgejo.db.password }}
SSL_MODE = {{ forgejo.db.ssl_mode }}
CHARSET = utf8mb4
LOG_SQL = false
[repository]
ROOT = {{ forgejo.data_dir }}/repositories
DEFAULT_BRANCH = main
PREFERRED_LICENSES = MIT,Apache-2.0,GPL-3.0
DISABLE_HTTP_GIT = false
ACCESS_CONTROL_ALLOW_ORIGIN = https://{{ forgejo.hostname }}
[security]
INSTALL_LOCK = true
SECRET_KEY = {{ forgejo.secret_key }}
INTERNAL_TOKEN = {{ forgejo.internal_token }}
PASSWORD_HASH_ALGO = argon2
MIN_PASSWORD_LENGTH = 12
[service]
DISABLE_REGISTRATION = {{ forgejo.features.disable_registration | lower }}
REQUIRE_SIGNIN_VIEW = {{ forgejo.features.require_signin | lower }}
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DEFAULT_KEEP_EMAIL_PRIVATE = true
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply@{{ forgejo.domain }}
[mailer]
ENABLED = false
[session]
PROVIDER = file
PROVIDER_CONFIG = {{ forgejo.data_dir }}/sessions
[picture]
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false
[log]
MODE = console, file
LEVEL = Info
ROOT_PATH = {{ forgejo.log_dir }}
[git]
MAX_GIT_DIFF_LINES = 10000
MAX_GIT_DIFF_LINE_CHARACTERS = 5000
MAX_GIT_DIFF_FILES = 100
[actions]
ENABLED = {{ forgejo.features.enable_actions | lower }}
[packages]
ENABLED = {{ forgejo.features.enable_packages | lower }}
{% if forgejo.oauth.enabled %}
# OAuth2 / OpenID Connect (Keycloak)
[oauth2_client]
REGISTER_EMAIL_CONFIRM = false
ENABLE_AUTO_REGISTRATION = true
USERNAME = preferred_username
UPDATE_AVATAR = true
ACCOUNT_LINKING = auto
{% for provider in forgejo.oauth.providers %}
[oauth2.{{ provider.name }}]
ENABLED = true
PROVIDER = {{ provider.provider }}
CLIENT_ID = {{ provider.client_id }}
CLIENT_SECRET = {{ provider.client_secret }}
OPENID_CONNECT_AUTO_DISCOVERY_URL = {{ provider.openid_connect_auto_discovery_url }}
{% endfor %}
{% endif %}

View file

@ -0,0 +1,91 @@
# Alliance Boréale - Forgejo Systemd Service
# Generated by Ansible
[Unit]
Description=Forgejo Git Service
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User={{ forgejo.user }}
Group={{ forgejo.group }}
WorkingDirectory={{ forgejo.data_dir }}
ExecStart={{ forgejo.install_dir }}/forgejo web --config {{ forgejo.custom_dir }}/app.ini
Restart=on-failure
RestartSec=10
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths={{ forgejo.data_dir }} {{ forgejo.log_dir }}
# Limits
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
---SEPARATOR---
# Alliance Boréale - Nginx Configuration for Forgejo
# Generated by Ansible
upstream forgejo_backend {
server 127.0.0.1:{{ forgejo.http_port }};
}
server {
listen 80;
server_name {{ forgejo.hostname }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ forgejo.hostname }};
ssl_certificate /etc/ssl/certs/{{ forgejo.hostname }}.crt;
ssl_certificate_key /etc/ssl/private/{{ forgejo.hostname }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Large uploads (Git LFS)
client_max_body_size 512M;
location / {
proxy_pass http://forgejo_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (for Actions)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
---SEPARATOR---
# Alliance Boréale - Forgejo Handlers
# Date: 2025-10-31
---
- name: restart forgejo
ansible.builtin.systemd:
name: forgejo
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded

View file

@ -0,0 +1,36 @@
# Alliance Boréale - Nginx Forgejo
# Generated by Ansible
upstream forgejo_backend {
server 127.0.0.1:{{ forgejo.http_port }};
}
server {
listen 80;
server_name {{ forgejo.hostname }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ forgejo.hostname }};
ssl_certificate /etc/ssl/certs/{{ forgejo.hostname }}.crt;
ssl_certificate_key /etc/ssl/private/{{ forgejo.hostname }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 512M;
location / {
proxy_pass http://forgejo_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

View file

@ -0,0 +1,18 @@
# Alliance Boréale - Keycloak Handlers
# Date: 2025-10-31
---
- name: restart keycloak
ansible.builtin.systemd:
name: keycloak
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded

View file

@ -0,0 +1,140 @@
# Alliance Boréale - Keycloak Tasks (database, nginx, realms, firewall)
# Date: 2025-10-31
---
# ==========================================
# DATABASE.YML
# ==========================================
- name: "🗄️ Ensure PostgreSQL is installed"
ansible.builtin.include_role:
name: postgresql
when: "'postgresql_servers' in group_names"
- name: "🗄️ Create Keycloak database"
community.postgresql.postgresql_db:
name: "{{ keycloak.db.database }}"
encoding: UTF8
state: present
become: true
become_user: postgres
- name: "👤 Create Keycloak database user"
community.postgresql.postgresql_user:
name: "{{ keycloak.db.username }}"
password: "{{ keycloak.db.password }}"
state: present
become: true
become_user: postgres
no_log: true
- name: "🔐 Grant privileges to Keycloak user"
community.postgresql.postgresql_privs:
database: "{{ keycloak.db.database }}"
roles: "{{ keycloak.db.username }}"
type: database
privs: ALL
state: present
become: true
become_user: postgres
# ==========================================
# NGINX.YML
# ==========================================
- name: "📦 Ensure Nginx is installed"
ansible.builtin.apt:
name: nginx
state: present
- name: "🔐 Generate self-signed SSL certificate"
ansible.builtin.command:
cmd: >
openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout /etc/ssl/private/{{ keycloak.hostname }}.key
-out /etc/ssl/certs/{{ keycloak.hostname }}.crt
-subj "/C=CA/ST=Quebec/L=Montreal/O=Chezlepro/CN={{ keycloak.hostname }}"
creates: "/etc/ssl/certs/{{ keycloak.hostname }}.crt"
when: nginx.ssl.cert_source == 'self-signed'
- name: "⚙️ Configure Nginx for Keycloak"
ansible.builtin.template:
src: nginx-keycloak.conf.j2
dest: /etc/nginx/sites-available/keycloak
owner: root
group: root
mode: '0644'
notify: reload nginx
- name: "🔗 Enable Nginx site"
ansible.builtin.file:
src: /etc/nginx/sites-available/keycloak
dest: /etc/nginx/sites-enabled/keycloak
state: link
notify: reload nginx
- name: "🚫 Remove default Nginx site"
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: "✅ Start and enable Nginx"
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
# ==========================================
# REALMS.YML
# ==========================================
- name: "🏰 Check if realm exists"
ansible.builtin.uri:
url: "http://127.0.0.1:{{ keycloak.http_port }}/admin/realms/{{ item.name }}"
method: GET
user: "{{ keycloak.admin_user }}"
password: "{{ keycloak.admin_password }}"
force_basic_auth: true
status_code: [200, 404]
loop: "{{ keycloak.realms }}"
register: realm_check
changed_when: false
no_log: true
- name: "🏰 Create realm configuration file"
ansible.builtin.template:
src: realm-config.json.j2
dest: "/tmp/realm-{{ item.name }}.json"
owner: keycloak
group: keycloak
mode: '0640'
loop: "{{ keycloak.realms }}"
when: realm_check.results[0].status == 404
- name: "🏰 Import realm"
ansible.builtin.command:
cmd: >
/opt/keycloak/bin/kc.sh import
--file /tmp/realm-{{ item.name }}.json
--override false
become: true
become_user: keycloak
loop: "{{ keycloak.realms }}"
when: realm_check.results[0].status == 404
register: realm_import
changed_when: "'imported' in realm_import.stdout"
# ==========================================
# FIREWALL.YML
# ==========================================
- name: "🔥 Configure firewall for Keycloak"
ansible.builtin.blockinfile:
path: /etc/nftables.conf
marker: "# {mark} ANSIBLE MANAGED - Keycloak"
insertbefore: "# Log dropped packets"
block: |
# Keycloak - Internal HTTP (backend only)
ip saddr 127.0.0.1 tcp dport {{ keycloak.http_port }} accept comment "Keycloak HTTP"
# Nginx - HTTPS (public)
tcp dport 443 accept comment "HTTPS (Keycloak via Nginx)"
tcp dport 80 accept comment "HTTP redirect"
notify: reload nftables

View file

@ -0,0 +1,140 @@
# Alliance Boréale - Rôle Keycloak
# Couche: C3 (Gouvernance & Supervision)
# Objectif: Déployer IdP fédéré (SSO)
# Date: 2025-10-31
---
- name: "📦 Install dependencies"
ansible.builtin.apt:
name:
- openjdk-17-jre-headless
- python3-pip
- python3-psycopg2
- nginx
- openssl
state: present
update_cache: true
tags: keycloak
- name: "👤 Create keycloak user"
ansible.builtin.user:
name: keycloak
system: true
shell: /bin/false
home: /opt/keycloak
create_home: false
tags: keycloak
- name: "📁 Create keycloak directories"
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: keycloak
group: keycloak
mode: '0755'
loop:
- /opt/keycloak
- /var/log/keycloak
tags: keycloak
- name: "📥 Download Keycloak"
ansible.builtin.get_url:
url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak.version }}/keycloak-{{ keycloak.version }}.tar.gz"
dest: "/tmp/keycloak-{{ keycloak.version }}.tar.gz"
mode: '0644'
tags: keycloak
- name: "📦 Extract Keycloak"
ansible.builtin.unarchive:
src: "/tmp/keycloak-{{ keycloak.version }}.tar.gz"
dest: /opt/keycloak
remote_src: true
owner: keycloak
group: keycloak
extra_opts: [--strip-components=1]
creates: /opt/keycloak/bin/kc.sh
tags: keycloak
- name: "🗄️ Configure PostgreSQL for Keycloak"
ansible.builtin.import_tasks: database.yml
tags: keycloak
- name: "⚙️ Configure Keycloak"
ansible.builtin.template:
src: keycloak.conf.j2
dest: /opt/keycloak/conf/keycloak.conf
owner: keycloak
group: keycloak
mode: '0640'
notify: restart keycloak
tags: keycloak
- name: "🔨 Build Keycloak"
ansible.builtin.command:
cmd: /opt/keycloak/bin/kc.sh build
become: true
become_user: keycloak
args:
creates: /opt/keycloak/lib/quarkus/quarkus-application.dat
tags: keycloak
- name: "⚙️ Create systemd service"
ansible.builtin.template:
src: keycloak.service.j2
dest: /etc/systemd/system/keycloak.service
owner: root
group: root
mode: '0644'
notify: restart keycloak
tags: keycloak
- name: "✅ Start and enable Keycloak"
ansible.builtin.systemd:
name: keycloak
state: started
enabled: true
daemon_reload: true
tags: keycloak
- name: "⏳ Wait for Keycloak to be ready"
ansible.builtin.wait_for:
port: "{{ keycloak.http_port }}"
host: 127.0.0.1
timeout: 120
tags: keycloak
- name: "🌐 Configure Nginx reverse proxy"
ansible.builtin.import_tasks: nginx.yml
tags: keycloak
- name: "👑 Create admin user"
ansible.builtin.command:
cmd: >
/opt/keycloak/bin/kcadm.sh config credentials
--server http://localhost:{{ keycloak.http_port }}
--realm master
--user {{ keycloak.admin_user }}
--password {{ keycloak.admin_password }}
become: true
become_user: keycloak
register: admin_created
changed_when: false
failed_when: false
no_log: true
tags: keycloak
- name: "🏰 Configure realms and clients"
ansible.builtin.import_tasks: realms.yml
tags: keycloak
- name: "🔥 Configure firewall"
ansible.builtin.import_tasks: firewall.yml
tags: keycloak
- name: "✅ Keycloak role completed"
ansible.builtin.debug:
msg: |
✅ Keycloak deployed on {{ inventory_hostname }}
URL: https://{{ keycloak.hostname }}
Admin: {{ keycloak.admin_user }}
tags: keycloak

View file

@ -0,0 +1,25 @@
# Alliance Boréale - Keycloak Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# Network
hostname={{ keycloak.hostname }}
http-enabled=true
http-port={{ keycloak.http_port }}
http-host=0.0.0.0
proxy=edge
# Database
db=postgres
db-url=jdbc:postgresql://{{ keycloak.db.host }}/{{ keycloak.db.database }}
db-username={{ keycloak.db.username }}
db-password={{ keycloak.db.password }}
# Logging
log-level=INFO
log-console-output=default
# Health
health-enabled=true
metrics-enabled=true

View file

@ -0,0 +1,83 @@
# Alliance Boréale - Keycloak Systemd Service
# Generated by Ansible
[Unit]
Description=Keycloak Identity Provider
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=keycloak
Group=keycloak
WorkingDirectory=/opt/keycloak
ExecStart=/opt/keycloak/bin/kc.sh start
Restart=on-failure
RestartSec=10
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/keycloak /opt/keycloak/data
# Limits
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
---SEPARATOR---
# Alliance Boréale - Nginx Configuration for Keycloak
# Generated by Ansible
upstream keycloak_backend {
server 127.0.0.1:{{ keycloak.http_port }};
}
# HTTP -> HTTPS redirect
server {
listen 80;
server_name {{ keycloak.hostname }};
return 301 https://$server_name$request_uri;
}
# HTTPS
server {
listen 443 ssl http2;
server_name {{ keycloak.hostname }};
# SSL
ssl_certificate /etc/ssl/certs/{{ keycloak.hostname }}.crt;
ssl_certificate_key /etc/ssl/private/{{ keycloak.hostname }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Logging
access_log /var/log/nginx/keycloak-access.log;
error_log /var/log/nginx/keycloak-error.log;
# Proxy settings
location / {
proxy_pass http://keycloak_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Buffers
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Timeouts
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
}
}

View file

@ -0,0 +1,30 @@
# Alliance Boréale - Nginx Keycloak
# Generated by Ansible
upstream keycloak_backend {
server 127.0.0.1:{{ keycloak.http_port }};
}
server {
listen 80;
server_name {{ keycloak.hostname }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ keycloak.hostname }};
ssl_certificate /etc/ssl/certs/{{ keycloak.hostname }}.crt;
ssl_certificate_key /etc/ssl/private/{{ keycloak.hostname }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://keycloak_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View file

@ -0,0 +1,48 @@
{
"realm": "{{ item.name }}",
"displayName": "{{ item.display_name }}",
"enabled": {{ item.enabled | lower }},
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"clients": [
{% for client in item.clients %}
{
"clientId": "{{ client.client_id }}",
"name": "{{ client.name }}",
"enabled": {{ client.enabled | lower }},
"protocol": "{{ client.protocol }}",
"publicClient": false,
"redirectUris": {{ client.redirect_uris | to_json }},
"webOrigins": {{ client.web_origins | to_json }},
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
}{{ "," if not loop.last else "" }}
{% endfor %}
]
}
---SEPARATOR---
# Alliance Boréale - Keycloak Handlers
# Date: 2025-10-31
---
- name: restart keycloak
ansible.builtin.systemd:
name: keycloak
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded

View file

@ -1,38 +0,0 @@
Role Name
=========
A brief description of the role goes here.
Requirements
------------
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
Role Variables
--------------
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
Dependencies
------------
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
Example Playbook
----------------
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
- hosts: servers
roles:
- { role: username.rolename, x: 42 }
License
-------
BSD
Author Information
------------------
An optional section for the role authors to include contact information, or a website (HTML is not allowed).

View file

@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# defaults file for netbox

View file

@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# handlers file for netbox

View file

@ -1,35 +0,0 @@
#SPDX-License-Identifier: MIT-0
galaxy_info:
author: your name
description: your role description
company: your company (optional)
# If the issue tracker for your role is not on github, uncomment the
# next line and provide a value
# issue_tracker_url: http://example.com/issue/tracker
# Choose a valid license ID from https://spdx.org - some suggested licenses:
# - BSD-3-Clause (default)
# - MIT
# - GPL-2.0-or-later
# - GPL-3.0-only
# - Apache-2.0
# - CC-BY-4.0
license: license (GPL-2.0-or-later, MIT, etc)
min_ansible_version: 2.1
# If this a Container Enabled role, provide the minimum Ansible Container version.
# min_ansible_container_version:
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
# if you add dependencies to this list.

View file

@ -1,17 +0,0 @@
---
- name: Vérifier que le rôle est lancé sur un hôte netbox
assert:
that: "'netbox' in group_names"
msg: "Ce rôle ne s'applique qu'aux hôtes du groupe [netbox]"
- name: Inclure les variables chiffrées (Vault)
include_vars:
file: ../../inventories/production/group_vars/vault.yml
when: vault_postgres_pass is not defined
- name: Message de bienvenue
debug:
msg: |
Déploiement NetBox sur {{ inventory_hostname }}
Domaine : {{ netbox_domain }}
Mode offline : {{ offline_mode }}

View file

@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
localhost

View file

@ -1,6 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
- hosts: localhost
remote_user: root
roles:
- netbox

View file

@ -1,3 +0,0 @@
#SPDX-License-Identifier: MIT-0
---
# vars file for netbox

View file

@ -0,0 +1,22 @@
# Alliance Boréale - Rôle PostgreSQL - Default Variables
# Date: 2025-10-31
---
postgresql:
version: 15
listen_addresses: localhost
port: 5432
shared_buffers: 256MB
work_mem: 16MB
maintenance_work_mem: 128MB
effective_cache_size: 1GB
databases: []
users: []
backup:
enabled: true
schedule: "0 2 * * *"
retention_days: 7
destination: /var/backups/postgresql

View file

@ -0,0 +1,13 @@
# Alliance Boréale - Rôle PostgreSQL - Handlers
# Date: 2025-10-31
---
- name: restart postgresql
ansible.builtin.systemd:
name: postgresql
state: restarted
- name: reload postgresql
ansible.builtin.systemd:
name: postgresql
state: reloaded

View file

@ -0,0 +1,114 @@
# Alliance Boréale - Rôle PostgreSQL
# Couche: C3 (dépendance pour PowerDNS)
# Objectif: Déployer PostgreSQL pour backend PowerDNS
# Date: 2025-10-31
---
- name: "📦 Install PostgreSQL and dependencies"
ansible.builtin.apt:
name:
- postgresql-{{ postgresql.version }}
- postgresql-contrib-{{ postgresql.version }}
- python3-psycopg2
state: present
update_cache: true
tags: postgresql
- name: "⚙️ Configure PostgreSQL"
ansible.builtin.template:
src: postgresql.conf.j2
dest: "/etc/postgresql/{{ postgresql.version }}/main/postgresql.conf"
owner: postgres
group: postgres
mode: '0644'
notify: restart postgresql
tags: postgresql
- name: "⚙️ Configure pg_hba.conf"
ansible.builtin.template:
src: pg_hba.conf.j2
dest: "/etc/postgresql/{{ postgresql.version }}/main/pg_hba.conf"
owner: postgres
group: postgres
mode: '0640'
notify: restart postgresql
tags: postgresql
- name: "✅ Ensure PostgreSQL is started"
ansible.builtin.systemd:
name: postgresql
state: started
enabled: true
tags: postgresql
- name: "🗄️ Create PostgreSQL databases"
community.postgresql.postgresql_db:
name: "{{ item.name }}"
encoding: "{{ item.encoding | default('UTF8') }}"
lc_collate: "{{ item.lc_collate | default('en_US.UTF-8') }}"
lc_ctype: "{{ item.lc_ctype | default('en_US.UTF-8') }}"
state: present
loop: "{{ postgresql.databases }}"
become: true
become_user: postgres
tags: postgresql
- name: "👤 Create PostgreSQL users"
community.postgresql.postgresql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
state: present
loop: "{{ postgresql.users }}"
become: true
become_user: postgres
no_log: true
tags: postgresql
- name: "🔐 Grant database privileges"
community.postgresql.postgresql_privs:
database: "{{ item.name }}"
roles: "{{ item.owner }}"
type: database
privs: ALL
state: present
loop: "{{ postgresql.databases }}"
become: true
become_user: postgres
tags: postgresql
- name: "📊 Create backup directory"
ansible.builtin.file:
path: "{{ postgresql.backup.destination }}"
state: directory
owner: postgres
group: postgres
mode: '0750'
when: postgresql.backup.enabled | default(true)
tags: postgresql
- name: "📊 Configure backup cron job"
ansible.builtin.cron:
name: "PostgreSQL backup"
user: postgres
minute: "0"
hour: "2"
job: "pg_dumpall | gzip > {{ postgresql.backup.destination }}/postgres-$(date +\\%Y\\%m\\%d).sql.gz"
state: present
when: postgresql.backup.enabled | default(true)
tags: postgresql
- name: "🗑️ Configure backup retention (delete old backups)"
ansible.builtin.cron:
name: "PostgreSQL backup cleanup"
user: postgres
minute: "30"
hour: "2"
job: "find {{ postgresql.backup.destination }} -name 'postgres-*.sql.gz' -mtime +{{ postgresql.backup.retention_days }} -delete"
state: present
when: postgresql.backup.enabled | default(true)
tags: postgresql
- name: "✅ PostgreSQL role completed"
ansible.builtin.debug:
msg: "✅ PostgreSQL {{ postgresql.version }} configured on {{ inventory_hostname }}"
tags: postgresql

View file

@ -0,0 +1,24 @@
# Alliance Boréale - PostgreSQL Client Authentication
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# TYPE DATABASE USER ADDRESS METHOD
# Local connections
local all postgres peer
local all all peer
# IPv4 local connections
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections
host all all ::1/128 scram-sha-256
# Internal network (if needed later)
# host all all 10.0.0.0/8 scram-sha-256
# Replication (for future use)
# local replication all peer
# host replication all 127.0.0.1/32 scram-sha-256
# host replication all ::1/128 scram-sha-256

View file

@ -0,0 +1,38 @@
# Alliance Boréale - PostgreSQL Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# Network
listen_addresses = '{{ postgresql.listen_addresses | default("localhost") }}'
port = {{ postgresql.port | default(5432) }}
# Memory
shared_buffers = {{ postgresql.shared_buffers | default('256MB') }}
work_mem = {{ postgresql.work_mem | default('16MB') }}
maintenance_work_mem = {{ postgresql.maintenance_work_mem | default('128MB') }}
effective_cache_size = {{ postgresql.effective_cache_size | default('1GB') }}
# Write-Ahead Log
wal_level = replica
max_wal_size = 1GB
min_wal_size = 80MB
# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = 1d
log_rotation_size = 10MB
log_line_prefix = '%m [%p] %q%u@%d '
log_timezone = 'America/Toronto'
# Locale
datestyle = 'iso, mdy'
timezone = 'America/Toronto'
lc_messages = 'en_CA.UTF-8'
lc_monetary = 'en_CA.UTF-8'
lc_numeric = 'en_CA.UTF-8'
lc_time = 'en_CA.UTF-8'
default_text_search_config = 'pg_catalog.english'

View file

@ -0,0 +1,18 @@
# Alliance Boréale - PowerDNS Handlers
# Date: 2025-10-31
---
- name: restart powerdns
ansible.builtin.systemd:
name: pdns
state: restarted
- name: reload powerdns
ansible.builtin.systemd:
name: pdns
state: reloaded
- name: reload nftables
ansible.builtin.systemd:
name: nftables
state: reloaded

View file

@ -0,0 +1,57 @@
# Alliance Boréale - PowerDNS Firewall & Validation
# Date: 2025-10-31
---
# ==========================================
# FIREWALL.YML
# ==========================================
- name: "🔥 Ensure nftables rules include DNS"
ansible.builtin.blockinfile:
path: /etc/nftables.conf
marker: "# {mark} ANSIBLE MANAGED - PowerDNS"
insertbefore: "# Log dropped packets"
block: |
# PowerDNS - DNS queries
udp dport 53 accept comment "DNS queries (UDP)"
tcp dport 53 accept comment "DNS queries (TCP)"
# PowerDNS - API (internal only)
ip saddr {{ network.internal_subnet | default('10.0.0.0/8') }} tcp dport 8081 accept comment "PowerDNS API"
notify: reload nftables
# ==========================================
# VALIDATE.YML
# ==========================================
- name: "🧪 Wait for PowerDNS to be ready"
ansible.builtin.wait_for:
port: 53
host: "{{ ansible_host }}"
timeout: 30
- name: "🧪 Test DNS resolution (localhost)"
ansible.builtin.command:
cmd: "dig @127.0.0.1 {{ dns_zones[0].name }} SOA +short"
register: dns_test_local
changed_when: false
failed_when: dns_test_local.rc != 0
- name: "🧪 Display DNS test result"
ansible.builtin.debug:
msg: "✅ DNS resolution working: {{ dns_test_local.stdout }}"
- name: "🧪 Check PowerDNS API (if enabled)"
ansible.builtin.uri:
url: "http://127.0.0.1:8081/api/v1/servers/localhost"
headers:
X-API-Key: "{{ powerdns.api.key }}"
return_content: true
register: api_test
when: powerdns.api.enabled | default(true)
failed_when: false
- name: "🧪 Display PowerDNS version"
ansible.builtin.debug:
msg: "✅ PowerDNS API responding: {{ api_test.json.version | default('N/A') }}"
when:
- powerdns.api.enabled | default(true)
- api_test.status == 200

View file

@ -0,0 +1,90 @@
# Alliance Boréale - Rôle PowerDNS Authoritative
# Couche: C2 (Réseau & DNS)
# Objectif: Déployer PowerDNS autoritaire (MASTER ou SLAVE)
# Date: 2025-10-31
---
- name: "🔍 Determine PowerDNS role (master/slave)"
ansible.builtin.set_fact:
pdns_is_master: "{{ inventory_hostname in groups['dns_masters'] }}"
pdns_is_slave: "{{ inventory_hostname in groups['dns_slaves'] }}"
tags: always
- name: "📦 Add PowerDNS repository"
ansible.builtin.import_tasks: repo.yml
tags: powerdns
- name: "📦 Install PowerDNS packages"
ansible.builtin.apt:
name:
- pdns-server
- pdns-backend-pgsql
- pdns-tools
state: present
update_cache: true
tags: powerdns
- name: "⚙️ Configure PowerDNS"
ansible.builtin.template:
src: pdns.conf.j2
dest: /etc/powerdns/pdns.conf
owner: root
group: pdns
mode: '0640'
notify: restart powerdns
tags: powerdns
- name: "⚙️ Configure PowerDNS PostgreSQL backend"
ansible.builtin.template:
src: pdns.d/pdns.local.gpgsql.conf.j2
dest: /etc/powerdns/pdns.d/pdns.local.gpgsql.conf
owner: root
group: pdns
mode: '0640'
notify: restart powerdns
tags: powerdns
- name: "🗄️ Import PowerDNS PostgreSQL schema"
ansible.builtin.import_tasks: schema.yml
when: pdns_is_master
tags: powerdns
- name: "🌐 Configure DNS zones (MASTER only)"
ansible.builtin.import_tasks: zones.yml
when: pdns_is_master
tags: powerdns
- name: "🔁 Configure AXFR (SLAVE only)"
ansible.builtin.import_tasks: slave.yml
when: pdns_is_slave
tags: powerdns
- name: "🔐 Configure DNSSEC"
ansible.builtin.import_tasks: dnssec.yml
when:
- pdns_is_master
- powerdns.dnssec.enabled | default(true)
tags: powerdns
- name: "🔥 Configure firewall for DNS"
ansible.builtin.import_tasks: firewall.yml
tags: powerdns
- name: "✅ Start and enable PowerDNS"
ansible.builtin.systemd:
name: pdns
state: started
enabled: true
tags: powerdns
- name: "🧪 Validate PowerDNS configuration"
ansible.builtin.import_tasks: validate.yml
tags: powerdns
- name: "✅ PowerDNS role completed"
ansible.builtin.debug:
msg: |
✅ PowerDNS configured on {{ inventory_hostname }}
Role: {{ 'MASTER' if pdns_is_master else 'SLAVE' }}
Zones: {{ dns_zones | map(attribute='name') | list if pdns_is_master else 'N/A' }}
tags: powerdns

View file

@ -0,0 +1,25 @@
# Alliance Boréale - PowerDNS Repository Setup
# Date: 2025-10-31
---
- name: "🔑 Add PowerDNS GPG key"
ansible.builtin.apt_key:
url: https://repo.powerdns.com/FD380FBB-pub.asc
state: present
- name: "📦 Add PowerDNS repository"
ansible.builtin.apt_repository:
repo: "deb [arch=amd64] http://repo.powerdns.com/{{ ansible_distribution | lower }} {{ ansible_distribution_release }}-auth-48 main"
state: present
filename: pdns
- name: "📋 Set PowerDNS package preferences"
ansible.builtin.copy:
dest: /etc/apt/preferences.d/pdns
content: |
Package: pdns-*
Pin: origin repo.powerdns.com
Pin-Priority: 600
owner: root
group: root
mode: '0644'

View file

@ -0,0 +1,149 @@
# Alliance Boréale - PowerDNS Schema Import
# Date: 2025-10-31
---
# Ce fichier contient: schema.yml, zones.yml, slave.yml, dnssec.yml
# ==========================================
# SCHEMA.YML - Import PostgreSQL schema
# ==========================================
- name: "🗄️ Check if PowerDNS schema exists"
community.postgresql.postgresql_query:
db: powerdns
query: "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'domains');"
become: true
become_user: postgres
register: pdns_schema_check
changed_when: false
- name: "🗄️ Download PowerDNS schema"
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/PowerDNS/pdns/rel/auth-4.8.x/modules/gpgsqlbackend/schema.pgsql.sql
dest: /tmp/pdns-schema.sql
mode: '0644'
when: not pdns_schema_check.query_result[0].exists
- name: "🗄️ Import PowerDNS schema"
community.postgresql.postgresql_db:
db: powerdns
state: restore
target: /tmp/pdns-schema.sql
become: true
become_user: postgres
when: not pdns_schema_check.query_result[0].exists
# ==========================================
# ZONES.YML - Create DNS zones (MASTER)
# ==========================================
- name: "🌐 Create DNS zones"
ansible.builtin.command:
cmd: "pdnsutil create-zone {{ item.name }}"
loop: "{{ dns_zones }}"
register: zone_create
changed_when: "'created' in zone_create.stdout"
failed_when:
- zone_create.rc != 0
- "'already exists' not in zone_create.stderr"
- name: "🌐 Set zone kind to NATIVE"
ansible.builtin.command:
cmd: "pdnsutil set-kind {{ item.name }} NATIVE"
loop: "{{ dns_zones }}"
changed_when: false
- name: "🌐 Configure SOA records"
ansible.builtin.command:
cmd: >
pdnsutil replace-rrset {{ item.name }} @ SOA
"{{ item.soa.nameserver }} {{ item.soa.email }}
{{ item.soa.serial }} {{ item.soa.refresh }} {{ item.soa.retry }}
{{ item.soa.expire }} {{ item.soa.minimum }}"
loop: "{{ dns_zones }}"
changed_when: false
- name: "🌐 Add NS records"
ansible.builtin.command:
cmd: "pdnsutil add-record {{ item.0.name }} @ NS {{ item.1 }}"
loop: "{{ dns_zones | subelements('ns_records') }}"
register: ns_add
changed_when: false
failed_when:
- ns_add.rc != 0
- "'already exists' not in ns_add.stderr"
- name: "🌐 Add A/AAAA records"
ansible.builtin.command:
cmd: "pdnsutil add-record {{ item.0.name }} {{ item.1.name }} {{ item.1.type }} {{ item.1.content }} {{ item.1.ttl | default(3600) }}"
loop: "{{ dns_zones | subelements('records', skip_missing=True) }}"
register: record_add
changed_when: false
failed_when:
- record_add.rc != 0
- "'already exists' not in record_add.stderr"
- name: "🌐 Rectify zones"
ansible.builtin.command:
cmd: "pdnsutil rectify-zone {{ item.name }}"
loop: "{{ dns_zones }}"
changed_when: false
# ==========================================
# SLAVE.YML - Configure AXFR for slaves
# ==========================================
- name: "🔁 Configure supermaster for AXFR"
community.postgresql.postgresql_query:
db: powerdns
query: >
INSERT INTO supermasters (ip, nameserver, account)
VALUES ('{{ hostvars[groups['dns_masters'][0]]['ansible_host'] }}',
'{{ inventory_hostname }}',
'default')
ON CONFLICT DO NOTHING;
become: true
become_user: postgres
- name: "🔁 Enable slave mode in PowerDNS"
ansible.builtin.lineinfile:
path: /etc/powerdns/pdns.conf
regexp: '^slave='
line: 'slave=yes'
notify: restart powerdns
# ==========================================
# DNSSEC.YML - Configure DNSSEC
# ==========================================
- name: "🔐 Enable DNSSEC for zones"
ansible.builtin.command:
cmd: "pdnsutil secure-zone {{ item.name }}"
loop: "{{ dns_zones }}"
register: dnssec_secure
changed_when: "'secured' in dnssec_secure.stdout"
failed_when:
- dnssec_secure.rc != 0
- "'already' not in dnssec_secure.stderr"
- name: "🔐 Configure NSEC3"
ansible.builtin.command:
cmd: "pdnsutil set-nsec3 {{ item.name }} '1 0 10 ab' narrow"
loop: "{{ dns_zones }}"
changed_when: false
- name: "🔐 Rectify zones after DNSSEC"
ansible.builtin.command:
cmd: "pdnsutil rectify-zone {{ item.name }}"
loop: "{{ dns_zones }}"
changed_when: false
- name: "🔐 Export DS records"
ansible.builtin.command:
cmd: "pdnsutil show-zone {{ item.name }}"
loop: "{{ dns_zones }}"
register: ds_records
changed_when: false
- name: "🔐 Display DS records for parent zone"
ansible.builtin.debug:
msg: |
⚠️ IMPORTANT: Add these DS records to your domain registrar:
{{ ds_records.results | map(attribute='stdout') | join('\n') }}
when: ds_records.results | length > 0

View file

@ -0,0 +1,70 @@
# Alliance Boréale - PowerDNS Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Role: {{ 'MASTER' if pdns_is_master else 'SLAVE' }}
# Date: {{ ansible_date_time.iso8601 }}
# ==========================================
# BACKEND
# ==========================================
launch=gpgsql
include-dir=/etc/powerdns/pdns.d
# ==========================================
# NETWORK
# ==========================================
local-address=0.0.0.0
local-port=53
local-ipv6=
# ==========================================
# MODE
# ==========================================
master={{ 'yes' if pdns_is_master else 'no' }}
slave={{ 'yes' if pdns_is_slave else 'no' }}
superslave={{ 'no' }}
# ==========================================
# API & WEBSERVER
# ==========================================
api={{ 'yes' if powerdns.api.enabled | default(true) else 'no' }}
api-key={{ powerdns.api.key | default('changeme') }}
webserver={{ 'yes' if powerdns.webserver.enabled | default(true) else 'no' }}
webserver-address={{ powerdns.webserver.address | default('0.0.0.0') }}
webserver-port={{ powerdns.webserver.port | default(8081) }}
webserver-allow-from={{ powerdns.webserver.allow_from | default('10.0.0.0/8') }}
# ==========================================
# DNSSEC
# ==========================================
dnssec={{ 'on' if powerdns.dnssec.enabled | default(true) else 'off' }}
# ==========================================
# LOGGING
# ==========================================
log-dns-queries={{ 'yes' if powerdns.logging.queries | default(false) else 'no' }}
log-dns-details={{ 'yes' if powerdns.logging.details | default(true) else 'no' }}
loglevel={{ powerdns.logging.level | default(4) }}
# ==========================================
# PERFORMANCE
# ==========================================
cache-ttl={{ powerdns.cache_ttl | default(20) }}
negquery-cache-ttl={{ powerdns.negquery_cache_ttl | default(60) }}
query-cache-ttl={{ powerdns.query_cache_ttl | default(20) }}
# ==========================================
# SECURITY
# ==========================================
setuid=pdns
setgid=pdns
chroot=/var/spool/powerdns
disable-axfr={{ 'no' }}
allow-axfr-ips={{ axfr.allow_from | join(',') if axfr.allow_from is defined else '127.0.0.1' }}
also-notify={{ notify.also_notify | join(',') if notify.also_notify is defined else '' }}
# ==========================================
# SOA
# ==========================================
default-soa-content=ns1.infra.{{ member.domain_primary }} admin.{{ member.domain_primary }} 0 3600 1800 1209600 3600

View file

@ -0,0 +1,12 @@
# Alliance Boréale - PowerDNS PostgreSQL Backend
# Generated by Ansible - DO NOT EDIT MANUALLY
# Host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}
# PostgreSQL connection
gpgsql-host=/var/run/postgresql
gpgsql-port=5432
gpgsql-dbname=powerdns
gpgsql-user=pdns
gpgsql-password={{ vault_postgresql_pdns_password }}
gpgsql-dnssec=yes

View file

@ -1,13 +0,0 @@
---
- name: Déployer les services de l'Alliance Boréale
hosts: all
become: yes
vars:
offline_mode: "{{ offline_mode | default(false) }}"
roles:
- role: netbox
when: "'netbox' in group_names"
# - role: powerdns
# when: "'powerdns' in group_names"
# - role: keycloak
# when: "'keycloak' in group_names"

View file

@ -0,0 +1,54 @@
# Alliance Boréale - Vault Production (PHASE 1 + 2 EXEMPLE)
# ⚠️ NE PAS UTILISER TEL QUEL - À CHIFFRER AVEC ansible-vault
# Date: 2025-10-31
---
# ==========================================
# PHASE 1 - DNS
# ==========================================
vault_postgresql_pdns_password: "changeme-pdns-db-password"
vault_pdns_api_key: "changeme-pdns-api-key"
# ==========================================
# PHASE 2 - KEYCLOAK
# ==========================================
vault_keycloak_admin_password: "changeme-keycloak-admin-password"
vault_postgresql_keycloak_password: "changeme-keycloak-db-password"
# Client secret for Forgejo (à récupérer depuis Keycloak UI)
vault_keycloak_forgejo_client_secret: "changeme-will-be-generated-by-keycloak"
# ==========================================
# PHASE 2 - FORGEJO
# ==========================================
vault_forgejo_admin_password: "changeme-forgejo-admin-password"
vault_postgresql_forgejo_password: "changeme-forgejo-db-password"
# Secrets Forgejo (générer avec openssl)
vault_forgejo_secret_key: "changeme-64-chars-secret-key-here-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
vault_forgejo_internal_token: "changeme-64-chars-internal-token-here-xxxxxxxxxxxxxxxxxxxxxxxx"
vault_forgejo_jwt_secret: "changeme-64-chars-jwt-secret-here-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ==========================================
# COMMANDES POUR GÉNÉRER LES SECRETS:
# ==========================================
# Mots de passe (24 chars)
# openssl rand -base64 24
# API Keys / Secrets (32 chars)
# openssl rand -base64 32
# Forgejo secrets (64 chars hex)
# openssl rand -hex 64
# ==========================================
# WORKFLOW:
# ==========================================
# 1. Copier vault/production.yml.example → vault/production.yml
# 2. Générer tous les secrets avec openssl
# 3. Déployer Keycloak (phase2-deploy.yml --tags keycloak)
# 4. Récupérer client secret depuis Keycloak UI
# 5. Ajouter vault_keycloak_forgejo_client_secret
# 6. Déployer Forgejo (phase2-deploy.yml --tags forgejo)
# 7. Chiffrer: ansible-vault encrypt vault/production.yml

View file

@ -0,0 +1,41 @@
# Alliance Boréale - Vault Production (EXEMPLE NON CHIFFRÉ)
# ⚠️ NE PAS UTILISER TEL QUEL - À CHIFFRER AVEC ansible-vault
# Date: 2025-10-31
---
# ==========================================
# POSTGRESQL
# ==========================================
vault_postgresql_pdns_password: "changeme-generate-secure-password-here"
# ==========================================
# POWERDNS
# ==========================================
vault_pdns_api_key: "changeme-generate-secure-api-key-here"
# ==========================================
# TSIG KEYS (Phase 2 - AXFR croisés)
# ==========================================
# vault_tsig_clp_nul: "base64-encoded-secret-here"
# vault_tsig_clp_tli: "base64-encoded-secret-here"
# ==========================================
# POUR GÉNÉRER DES SECRETS SÉCURISÉS:
# ==========================================
# openssl rand -base64 32 # Pour API keys
# openssl rand -base64 24 # Pour mots de passe
# tsig-keygen clp-nul-xfer # Pour TSIG keys
# ==========================================
# COMMANDES POUR CRÉER LE VRAI VAULT:
# ==========================================
# 1. Copier ce fichier:
# cp vault/production.yml.example vault/production.yml
#
# 2. Éditer et remplacer tous les "changeme-*":
# vi vault/production.yml
#
# 3. Chiffrer avec ansible-vault:
# ansible-vault encrypt vault/production.yml
#
# 4. Sauvegarder le mot de passe vault dans un gestionnaire sécurisé!