testé jusqu'au hardening

This commit is contained in:
Dan Allaire 2026-03-09 22:20:08 -04:00
parent ef98fd8a3f
commit 0ef19077f2
48 changed files with 1933 additions and 59 deletions

View file

@ -55,3 +55,27 @@ ansible/
```bash
ansible-playbook playbooks/site.yml
```
## Intégration supplémentaire issue de `ansible2.zip`
Cette convergence incorpore maintenant une seconde lignée orientée services :
- `postgresql`
- `keycloak`
- `forgejo`
- `powerdns_authoritative`
Playbooks ajoutés :
- `playbooks/services/phase1_dns.yml`
- `playbooks/services/phase2_identity_forge.yml`
Un exemple d'inventaire généré par OpenTofu est fourni dans `inventories/prod/examples/tofu_generated.example.yml`.
Des exemples de `host_vars` et de `vault/` sont aussi inclus pour accélérer l'adaptation.
Les playbooks de baseline et hardening ciblent `all`. Les playbooks de services ciblent directement les groupes OpenTofu : `c3_services`, `c4_forge`, `c5_runner`, `c7_tenants`.
## Inventaire OpenTofu recommandé
Le dépôt consomme un inventaire généré par OpenTofu avec un groupe racine `boreale_all`, des groupes de couche (`c3_supervision`, `c3_core`, `c4_forge`, `c5_pivot`) et un sous-arbre `tenants/c7_products`. Des métadonnées minimales par hôte sont attendues : `boreale_layer`, `boreale_role`, `boreale_env`, `boreale_managed_by`.

View file

@ -4,7 +4,8 @@ roles_path = roles
host_key_checking = False
retry_files_enabled = False
interpreter_python = auto_silent
stdout_callback = yaml
stdout_callback = ansible.builtin.default
result_format = yaml
bin_ansible_callbacks = True
timeout = 30
forks = 20

View file

@ -31,3 +31,21 @@ Le sous-arbre `ansible/` vise une baseline VM commune :
3. fermée par défaut ;
4. compatible avec l'inventaire généré par OpenTofu ;
5. prête à recevoir ensuite les rôles applicatifs C2/C3/C4/C5.
## Intégration de `ansible2.zip`
Apports retenus :
- rôle `common` utilisé comme source secondaire d'idées, déjà absorbées en grande partie par `baseline_common` + `hardening_common` + `firewall_nftables`
- rôles applicatifs `postgresql`, `keycloak`, `forgejo`, `powerdns-authoritative` intégrés au dépôt convergé
- exemple concret d'inventaire OpenTofu (`inventory/tofu_generated.yml`) conservé comme gabarit
- exemples de vault Phase 1 / Phase 2 conservés
Décision d'architecture :
- le rôle `common` de `ansible2.zip` n'est pas repris tel quel pour éviter un doublon avec la baseline convergée
- les playbooks originaux ne sont pas repris à l'identique; ils sont réémis sous `playbooks/services/` pour s'aligner avec la séparation OpenTofu → inventaire, puis Ansible → convergence + service
## Inventaire OpenTofu recommandé
Le dépôt consomme un inventaire généré par OpenTofu avec un groupe racine `boreale_all`, des groupes de couche (`c3_supervision`, `c3_core`, `c4_forge`, `c5_pivot`) et un sous-arbre `tenants/c7_products`. Des métadonnées minimales par hôte sont attendues : `boreale_layer`, `boreale_role`, `boreale_env`, `boreale_managed_by`.

View file

@ -0,0 +1,66 @@
# ══════════════════════════════════════════════════════════════════
# Inventaire Ansible — généré par OpenTofu
# NE PAS MODIFIER MANUELLEMENT — regénéré à chaque tofu apply
# Forme cible recommandée pour l'Alliance Boréale
# ══════════════════════════════════════════════════════════════════
---
all:
children:
boreale_all:
children:
c3_supervision:
hosts:
c3-icinga-01:
ansible_host: 192.168.10.31
icinga_role: master
boreale_layer: c3
boreale_role: icinga
boreale_env: prod
boreale_managed_by: opentofu
c3_core:
hosts:
c3-netbox-01:
ansible_host: 192.168.10.30
boreale_layer: c3
boreale_role: netbox
boreale_env: prod
boreale_managed_by: opentofu
c4_forge:
hosts:
c4-forgejo-01:
ansible_host: 192.168.10.40
boreale_layer: c4
boreale_role: forgejo
boreale_env: prod
boreale_managed_by: opentofu
c5_pivot:
hosts:
c5-runner-01:
ansible_host: 192.168.10.50
boreale_layer: c5
boreale_role: runner
boreale_env: prod
boreale_managed_by: opentofu
tenants:
children:
c7_products:
hosts:
c7-87-16-01:
ansible_host: 192.168.12.45
boreale_layer: c7
boreale_role: tenant_app
boreale_env: prod
boreale_managed_by: opentofu
c7-life-noc-01:
ansible_host: 192.168.12.40
boreale_layer: c7
boreale_role: tenant_app
boreale_env: prod
boreale_managed_by: opentofu
vars:
ansible_ssh_private_key_file: ~/.ssh/id_ed25519_ansible_chezlepro
ansible_user: ansible

View file

@ -1,21 +1,17 @@
---
# Paramètres globaux de la baseline commune Alliance Boréale
# Les hôtes et groupes sont générés par OpenTofu dans inventories/prod/hosts.yml
ab_domain: example.internal
ab_domain: chezlepro.local
ab_dns_servers:
- 192.0.2.10
- 192.0.2.20
- 192.168.10.30
ab_admin_networks:
- 192.0.2.0/24
ab_ntp_server: c2-opnsense-01.example.internal
ab_ansible_user: ansible
ab_ansible_ssh_key_path: ~/.ssh/id_ed25519_ansible
- 192.168.10.0/24
- 192.168.12.0/24
ab_ntp_server: 192.168.10.1
ab_timezone: America/Montreal
ab_enable_wazuh_agent: false
ansible_user: "{{ ab_ansible_user }}"
ansible_ssh_private_key_file: "{{ ab_ansible_ssh_key_path }}"
# Baseline commune
baseline_common_enabled: true
baseline_common_upgrade: true
@ -44,7 +40,7 @@ baseline_common_packages:
- fail2ban
- debian-archive-keyring
a_baseline_common_manage_sources: false
baseline_common_manage_sources: false
# Hardening commun
hardening_common_enabled: true
@ -69,18 +65,19 @@ firewall_nftables_policy_forward: drop
firewall_nftables_policy_output: accept
firewall_nftables_allowed_tcp_in:
- dport: 22
src: 192.0.2.0/24
src: 192.168.10.0/24
- dport: 22
src: 192.168.12.0/24
firewall_nftables_allowed_udp_in: []
firewall_nftables_allow_icmp: true
# Monitoring commun
monitoring_icinga_agent_enabled: true
monitoring_icinga_agent_master: c3-icinga-01.example.internal
monitoring_icinga_agent_master: "{{ hostvars['c3-icinga-01'].ansible_host | default('c3-icinga-01') }}"
monitoring_icinga_agent_parent_zone: master
monitoring_icinga_agent_zone: "{{ inventory_hostname }}"
# Wazuh optionnel
security_wazuh_agent_enabled: "{{ ab_enable_wazuh_agent }}"
security_wazuh_agent_manager: c3-wazuh-01.example.internal
security_wazuh_agent_manager: c3-wazuh-01
security_wazuh_agent_registration_password: ""

View file

@ -0,0 +1,2 @@
---
monitoring_icinga_agent_enabled: false

View file

@ -0,0 +1,2 @@
---
ab_enable_wazuh_agent: true

View file

@ -0,0 +1,2 @@
---
# Override example for product-facing tenant nodes

View file

@ -0,0 +1,20 @@
---
keycloak:
hostname: sso.example.internal
admin_user: admin
admin_password: "{{ vault_keycloak_admin_password }}"
db_name: keycloak
db_user: keycloak
db_password: "{{ vault_keycloak_db_password }}"
http_port: 8080
https_enabled: false
postgresql:
version: 15
listen_addresses: localhost
databases:
- name: keycloak
owner: keycloak
users:
- name: keycloak
password: "{{ vault_keycloak_db_password }}"

View file

@ -0,0 +1,20 @@
---
pdns:
api_key: "{{ vault_pdns_api_key }}"
api_enabled: true
webserver_address: 0.0.0.0
webserver_allow_from:
- 127.0.0.1
gpgsql_dbname: pdns
gpgsql_user: pdns
gpgsql_password: "{{ vault_postgresql_pdns_password }}"
postgresql:
version: 15
listen_addresses: localhost
databases:
- name: pdns
owner: pdns
users:
- name: pdns
password: "{{ vault_postgresql_pdns_password }}"

View file

@ -0,0 +1,23 @@
---
forgejo:
hostname: git.example.internal
ssh_port: 2222
admin_username: admin
admin_email: admin@example.internal
admin_password: "{{ vault_forgejo_admin_password }}"
db_name: forgejo
db_user: forgejo
db_password: "{{ vault_forgejo_db_password }}"
secret_key: "{{ vault_forgejo_secret_key }}"
internal_token: "{{ vault_forgejo_internal_token }}"
jwt_secret: "{{ vault_forgejo_jwt_secret }}"
postgresql:
version: 15
listen_addresses: localhost
databases:
- name: forgejo
owner: forgejo
users:
- name: forgejo
password: "{{ vault_forgejo_db_password }}"

View file

@ -1,4 +1,69 @@
all:
children:
boreale_all:
hosts: {}
# ══════════════════════════════════════════════════════════════════
# Inventaire Ansible — généré par OpenTofu
# NE PAS MODIFIER MANUELLEMENT — regénéré à chaque tofu apply
# ══════════════════════════════════════════════════════════════════
---
"all":
"children":
"boreale_all":
"children":
"c2_edge":
"hosts": {}
"c3_core":
"hosts":
"c3-netbox-01":
"ansible_host": "192.168.10.30"
"boreale_env": null
"boreale_layer": "c3"
"boreale_managed_by": "opentofu"
"boreale_role": "netbox"
"c3_identity":
"hosts": {}
"c3_supervision":
"hosts":
"c3-icinga-01":
"ansible_host": "192.168.10.31"
"boreale_env": null
"boreale_layer": "c3"
"boreale_managed_by": "opentofu"
"boreale_role": "icinga"
"icinga_role": "master"
"c4_forge":
"hosts":
"c4-forgejo-01":
"ansible_host": "192.168.10.40"
"boreale_env": null
"boreale_layer": "c4"
"boreale_managed_by": "opentofu"
"boreale_role": "forgejo"
"c5_pivot":
"hosts":
"c5-runner-01":
"ansible_host": "192.168.10.50"
"boreale_env": null
"boreale_layer": "c5"
"boreale_managed_by": "opentofu"
"boreale_role": "runner"
"tenants":
"children":
"c6_services":
"hosts": {}
"c7_products":
"hosts":
"c7-87-16-01":
"ansible_host": "192.168.12.45"
"boreale_env": null
"boreale_layer": "c7"
"boreale_managed_by": "opentofu"
"boreale_role": "87-16"
"c7-life-noc-01":
"ansible_host": "192.168.12.40"
"boreale_env": null
"boreale_layer": "c7"
"boreale_managed_by": "opentofu"
"boreale_role": "life-noc"
"c8_knowledge":
"hosts": {}
"vars":
"ansible_ssh_private_key_file": "~/.ssh/id_ed25519_ansible_chezlepro"
"ansible_user": "ansible"

View file

@ -0,0 +1,11 @@
---
- name: Services C3 coeur / réseau
hosts: c3_core
become: true
roles:
- role: baseline_common
- role: hardening_common
- role: firewall_nftables
- role: monitoring_icinga_agent
- role: postgresql
- role: powerdns_authoritative

View file

@ -0,0 +1,22 @@
---
- name: Services C3 coeur / identité
hosts: c3_core
become: true
roles:
- role: baseline_common
- role: hardening_common
- role: firewall_nftables
- role: monitoring_icinga_agent
- role: postgresql
- role: keycloak
- name: Services C4 forge
hosts: c4_forge
become: true
roles:
- role: baseline_common
- role: hardening_common
- role: firewall_nftables
- role: monitoring_icinga_agent
- role: postgresql
- role: forgejo

View file

@ -0,0 +1,10 @@
---
- name: Services C5 pivot / exécution
hosts: c5_pivot
become: true
roles:
- role: baseline_common
- role: hardening_common
- role: firewall_nftables
- role: monitoring_icinga_agent
- role: security_wazuh_agent

View file

@ -0,0 +1,10 @@
---
- name: Services tenants C7
hosts: c7_products
become: true
roles:
- role: baseline_common
- role: hardening_common
- role: firewall_nftables
- role: monitoring_icinga_agent
- role: security_wazuh_agent

View file

@ -2,3 +2,4 @@
collections:
- name: ansible.posix
- name: community.general
- name: community.postgresql

View file

@ -5,5 +5,5 @@ firewall_nftables_allow_icmp: true
firewall_nftables_policy_input: drop
firewall_nftables_policy_forward: drop
firewall_nftables_policy_output: accept
firewall_nftables_allowed_tcp_in: []
firewall_nftables_allowed_tcp_in: [22,443]
firewall_nftables_allowed_udp_in: []

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

@ -20,12 +20,21 @@
validate: '/usr/sbin/sshd -T -f %s'
notify: Reload ssh
- name: Créer le répertoire des drop-ins journald
ansible.builtin.file:
path: /etc/systemd/journald.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Déployer le drop-in journald
ansible.builtin.template:
src: journald-boreale.conf.j2
dest: /etc/systemd/journald.conf.d/99-boreale.conf
mode: '0644'
when: hardening_common_journald_enabled | bool
owner: root
group: root
mode: "0644"
notify: Restart journald
- name: Installer fail2ban

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

@ -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,20 +1,34 @@
---
- name: Gate
ansible.builtin.meta: end_host
when: not security_wazuh_agent_enabled | bool
when: not (security_wazuh_agent_enabled | bool)
- name: Installer l'agent Wazuh depuis APT natif si disponible
- name: Tenter l'installation de l'agent Wazuh
ansible.builtin.apt:
name: wazuh-agent
state: present
update_cache: true
failed_when: false
- name: Vérifier la présence du répertoire Wazuh
ansible.builtin.stat:
path: /var/ossec/etc
register: wazuh_ossec_dir
- name: Wazuh absent sur cet hôte, on saute la configuration
ansible.builtin.debug:
msg: "Wazuh n'est pas installé; configuration ignorée sur cet hôte."
when: not wazuh_ossec_dir.stat.exists
- name: Stopper le rôle Wazuh sur cet hôte si l'agent est absent
ansible.builtin.meta: end_host
when: not wazuh_ossec_dir.stat.exists
- name: Déployer ossec.conf minimal
ansible.builtin.template:
src: ossec.conf.j2
dest: /var/ossec/etc/ossec.conf
mode: '0640'
mode: "0640"
when: security_wazuh_agent_manager | length > 0
notify: Restart wazuh-agent

View file

@ -1,33 +0,0 @@
# ══════════════════════════════════════════════════════════════════
# Inventaire Ansible — généré par OpenTofu
# NE PAS MODIFIER MANUELLEMENT — regénéré à chaque tofu apply
# ══════════════════════════════════════════════════════════════════
---
"all":
"children":
"c3_monitoring":
"hosts":
"c3-icinga-01":
"ansible_host": "192.168.10.31"
"icinga_role": "master"
"c3_services":
"hosts":
"c3-netbox-01":
"ansible_host": "192.168.10.30"
"c4_forge":
"hosts":
"c4-forgejo-01":
"ansible_host": "192.168.10.40"
"c5_runner":
"hosts":
"c5-runner-01":
"ansible_host": "192.168.10.50"
"c7_tenants":
"hosts":
"c7-87-16-01":
"ansible_host": "192.168.12.45"
"c7-life-noc-01":
"ansible_host": "192.168.12.40"
"vars":
"ansible_ssh_private_key_file": "~/.ssh/id_ed25519_ansible_chezlepro"
"ansible_user": "ansible"