first
Ansible - controle du code / lint (push) Successful in 36s
Ansible - controle du code / promouvoir (push) Failing after 3s

This commit is contained in:
hcornet committed 2026-09-18 13:16:35 +02:00
1 parent c551c87ebf
commit 0e24df336c
28 files changed
+899 -415

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
profile: production
offline: true
exclude_paths:
- .gitea/
- collections/ansible_collections/
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.j2 text eol=lf
*.sh text eol=lf
+94
View File
@@ -0,0 +1,94 @@
---
# Gitea valide, Semaphore exécute : aucun playbook n'est joué ici.
# Semaphore suit la branche « valide », que seul ce workflow fait avancer,
# depuis « main » et après un contrôle réussi.
name: Ansible - controle du code
on:
push:
branches: ["**"]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Récupérer le dépôt
uses: actions/checkout@v4
- name: Installer les outils
run: |
pip3 install --quiet --break-system-packages ansible-core ansible-lint yamllint
ansible-galaxy collection install -r collections/requirements.yml
- name: yamllint
run: yamllint --strict .
- name: ansible-lint
run: ansible-lint
- name: Vérification de syntaxe du playbook
run: |
ansible-playbook site.yml --syntax-check
ansible-playbook site.yml --list-tasks > /dev/null
- name: Rendu de daemon.json (valeurs par défaut et toutes options)
run: |
set -e
d=roles/docker_hardening
rendre() {
ansible localhost -m ansible.builtin.template \
-a "src=$d/templates/daemon.json.j2 dest=$1" \
-e @$d/defaults/main.yml -e @group_vars/all/main.yml ${2:+-e "$2"} > /dev/null
python3 -m json.tool "$1" > /dev/null && echo "$1 : JSON valide"
}
rendre /tmp/daemon-defaut.json
rendre /tmp/daemon-complet.json '{"docker_hardening_default_address_pools":[{"base":"172.20.0.0/14","size":24}],
"docker_hardening_insecure_registries":["registry.example"],"docker_hardening_registry_mirrors":["https://mirror.example"],
"docker_hardening_userns_remap":"default","docker_hardening_data_root":"/srv/docker"}'
- name: Rendu et syntaxe du script DOCKER-USER
run: |
set -e
d=roles/docker_hardening
ansible localhost -m ansible.builtin.template \
-a "src=$d/templates/docker-user-firewall.sh.j2 dest=/tmp/docker-user-firewall" \
-e @$d/defaults/main.yml -e @group_vars/all/main.yml \
-e '{"docker_hardening_firewall_interfaces":["eth0"],"docker_hardening_firewall_log_drops":true,
"docker_hardening_firewall_public_ports":[{"port":443,"proto":"tcp"},{"port":443,"proto":"udp"}],
"docker_hardening_firewall_allowed_sources_v6":["fd00::/8"]}' > /dev/null
sh -n /tmp/docker-user-firewall && echo "Script DOCKER-USER : syntaxe valide"
promouvoir:
# Avance la branche « valide » (suivie par Semaphore) sur le commit validé de « main ».
needs: lint
if: gitea.ref == 'refs/heads/main' && gitea.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Récupérer le dépôt
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN_PROMOTION }}
- name: Pousser main vers valide
run: |
git config user.name "gitea-actions"
git config user.email "[email protected]"
git push origin HEAD:refs/heads/valide
echo "Branche valide -> $(git rev-parse --short HEAD)"
- name: Déclencher Semaphore (facultatif)
if: ${{ vars.SEMAPHORE_URL != '' }}
env:
SEMAPHORE_URL: ${{ vars.SEMAPHORE_URL }}
SEMAPHORE_PROJECT_ID: ${{ vars.SEMAPHORE_PROJECT_ID }}
SEMAPHORE_TEMPLATE_ID: ${{ vars.SEMAPHORE_TEMPLATE_ID }}
SEMAPHORE_TOKEN: ${{ secrets.SEMAPHORE_TOKEN }}
run: |
code=$(curl -sS -o /tmp/reponse.json -w '%{http_code}' \
-H "Authorization: Bearer ${SEMAPHORE_TOKEN}" -H "Content-Type: application/json" \
-X POST "${SEMAPHORE_URL}/api/project/${SEMAPHORE_PROJECT_ID}/tasks" \
-d "{\"template_id\": ${SEMAPHORE_TEMPLATE_ID}, \"dry_run\": true, \"message\": \"Gitea ${GITHUB_SHA::7}\"}")
echo "Semaphore : HTTP ${code}, tâche $(sed -n 's/.*"id":\([0-9]*\).*/\1/p' /tmp/reponse.json | head -1)"
[ "$code" = "201" ] || [ "$code" = "200" ]
+2 -415
View File
@@ -1,416 +1,3 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
*.retry
.cache/
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
+20
View File
@@ -0,0 +1,20 @@
---
extends: default
ignore: |
.gitea/
collections/ansible_collections/
rules:
line-length:
max: 160
truthy:
allowed-values: ["true", "false"]
comments:
min-spaces-from-content: 1
comments-indentation: false
braces:
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
+33
View File
@@ -0,0 +1,33 @@
# Chaîne Gitea → Semaphore
Gitea valide, Semaphore exécute. Semaphore ne suit pas `main` mais la branche
**`valide`**, que seul le workflow Gitea fait avancer :
```
push sur main ──► lint : yamllint, ansible-lint (profil production), syntax-check,
list-tasks, rendu de daemon.json (JSON valide, 2 scénarios),
rendu du script DOCKER-USER (sh -n)
│ succès
▼
promouvoir : main ──► valide (+ déclenchement Semaphore, facultatif)
│
▼
Semaphore (dépôt sur la branche « valide ») exécute site.yml
```
Un commit qui casse le contrôle n'atteint jamais `valide`, donc jamais Semaphore.
## Côté Gitea
- secret `GITEA_TOKEN_PROMOTION` : jeton d'un compte autorisé à pousser sur `valide`
(protéger la branche `valide` pour que seul ce compte puisse y écrire) ;
- facultatif, pour lancer Semaphore après promotion : variables `SEMAPHORE_URL`,
`SEMAPHORE_PROJECT_ID`, `SEMAPHORE_TEMPLATE_ID` et secret `SEMAPHORE_TOKEN`.
La tâche est créée en **dry run** (`--check`).
## Côté Semaphore
- dépôt : ce projet, branche `valide` (clé « Login with password » du compte `svc-semaphore`) ;
- inventaire : `inventory/hosts.yml`, clé SSH root du compte `ansible` ;
- modèle de tâche : `site.yml`, premier passage en `--check --diff` ;
- collections : `ansible.posix` (voir `collections/requirements.yml`).
+88
View File
@@ -1,2 +1,90 @@
# ansible-docker
Installation de **Docker Engine** et du **plugin Docker Compose** depuis le dépôt officiel
Docker, puis durcissement de l'hôte, sur Debian 12/13 et Ubuntu 22.04/24.04.
Gitea valide le code, **Semaphore** l'exécute (branche `valide`) : voir [CICD.md](CICD.md).
## Arborescence
```
ansible-docker/
├── ansible.cfg
├── site.yml # playbook unique (groupe d'inventaire « docker »)
├── collections/requirements.yml # ansible.posix
├── inventory/hosts.yml
├── group_vars/all/main.yml # valeurs du lab (DNS, réseaux autorisés)
├── roles/
│ ├── docker_install/ # dépôt officiel, clé vérifiée, paquets, services
│ └── docker_hardening/ # daemon.json, groupe docker, DOCKER-USER, auditd, contrôles
└── .gitea/workflows/ansible-lint.yml
```
Aucun secret n'est nécessaire : pas de `vault.yml` dans ce projet.
## Ce que fait chaque rôle
**docker_install**
- refuse toute distribution non prise en charge ;
- retire les paquets en conflit (`docker.io`, `containerd`, `runc`, `podman-docker`…,
ainsi que les homonymes `docker`/`wmdocker`, un dock pour gestionnaire de fenêtres) ;
- télécharge la clé du dépôt Docker et **vérifie son empreinte** avant de déclarer le dépôt
(format deb822, `/etc/apt/sources.list.d/docker.sources`, l'ancien `docker.list` est retiré) ;
- installe `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`,
`docker-compose-plugin`, sans paquets recommandés (pas de `docker-ce-rootless-extras`) ;
- fige la version si `docker_install_version` est renseignée.
**docker_hardening**
| Mesure | Détail |
|---|---|
| `daemon.json` | `icc: false`, `no-new-privileges`, `live-restore`, `userland-proxy: false`, rotation des journaux, `experimental: false`, validé par `dockerd --validate` avant écriture |
| Liaison par défaut | `ip: 127.0.0.1` : un `-p 8080:80` n'expose plus rien au réseau |
| Groupe `docker` | réduit à `docker_hardening_group_members` (vide par défaut) : tout autre membre est retiré |
| Ports publiés | chaîne `DOCKER-USER` (IPv4 et IPv6) : seules les sources autorisées et les ports déclarés publics passent |
| Audit | règles auditd sur les binaires, la configuration et les unités Docker/containerd |
| Routage | `net.ipv4.ip_forward=1` posé en dernier (`99-zz-docker.conf`) |
| Contrôle final | seccomp, AppArmor, no-new-privileges, live-restore, socket `0660 root:docker`, aucune API sur 2375/2376 |
## Variables à connaître
| Variable | Défaut | Rôle |
|---|---|---|
| `docker_install_version` | `""` | version d'Engine à installer et figer (ex. `29.1.5`) |
| `docker_hardening_default_bind_ip` | `127.0.0.1` | IP de publication quand le compose n'en précise pas |
| `docker_hardening_firewall_allowed_sources` | `10.0.4.0/24`, `10.0.5.0/24` | sources autorisées sur tous les ports publiés |
| `docker_hardening_firewall_public_ports` | `[]` | ports ouverts à toute source, ex. `{ port: 443, proto: tcp }` |
| `docker_hardening_firewall_interfaces` | interface de la route par défaut | interfaces d'entrée filtrées |
| `docker_hardening_group_members` | `[]` | comptes autorisés dans le groupe `docker` |
| `docker_hardening_dns` | `10.0.4.2` | résolveurs transmis aux conteneurs |
| `docker_hardening_default_address_pools` | `[]` | plages des réseaux Docker |
| `docker_hardening_userns_remap` | `""` | remappage des UID, **à décider avant tout déploiement de conteneurs** |
| `docker_hardening_audit_watch_data_root` | `false` | audit de `/var/lib/docker` (très verbeux) |
La liste complète et commentée est dans `roles/*/defaults/main.yml`.
## Points d'attention
- **Publication des ports** : avec `ip: 127.0.0.1`, un service à ouvrir au réseau doit être
publié explicitement dans son compose, par exemple `"0.0.0.0:443:443"` pour Traefik, puis
déclaré dans `docker_hardening_firewall_public_ports` s'il doit être joignable hors des
réseaux autorisés.
- **`no-new-privileges`** : les images qui s'appuient sur `sudo`/`su` ou un binaire setuid
à l'intérieur du conteneur échouent. Le lever pour un seul service :
`security_opt: ["no-new-privileges:false"]`.
- **Redémarrage du démon** : un changement de `daemon.json` redémarre Docker. Grâce à
`live-restore`, les conteneurs continuent de tourner, mais les options réseau (`icc`,
plages d'adresses…) ne s'appliquent qu'aux réseaux et conteneurs recréés.
- **Pare-feu de l'hôte** : si un autre rôle (par exemple le durcissement Linux) gère nftables,
sa configuration ne doit pas contenir `flush ruleset` ni une politique `drop` sur la chaîne
`forward` : elle effacerait ou bloquerait les règles de Docker.
- **auditd immuable** : si une autre configuration pose `-e 2`, `augenrules --load` échoue
jusqu'au prochain redémarrage.
## Exécution (Semaphore)
Modèle de tâche sur `site.yml`, inventaire `inventory/hosts.yml`. Premier passage conseillé en
`--check --diff` : sur un hôte vierge, le rôle de durcissement s'arrête proprement après
l'installation simulée, puisque Docker n'existe pas encore.
Tags : `docker_install`, `docker_hardening`.
+19
View File
@@ -0,0 +1,19 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
collections_path = ~/.ansible/collections:./collections
remote_user = root
interpreter_python = /usr/bin/python3
host_key_checking = True
stdout_callback = ansible.builtin.default
callback_result_format = yaml
callbacks_enabled = ansible.posix.profile_tasks
forks = 5
retry_files_enabled = False
# Pas de section [privilege_escalation] : les cibles n'ont pas sudo,
# Ansible s'y connecte directement en root par clé.
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o StrictHostKeyChecking=accept-new
pipelining = True
+1
View File
@@ -0,0 +1 @@
ansible_collections/
+3
View File
@@ -0,0 +1,3 @@
---
collections:
- name: ansible.posix
+15
View File
@@ -0,0 +1,15 @@
---
ansible_managed: "Fichier géré par Ansible (ansible-docker) - toute modification manuelle sera écrasée"
# Résolveur du lab, transmis aux conteneurs
docker_hardening_dns:
- 10.0.4.2
# Réseaux autorisés à joindre les ports publiés par les conteneurs (chaîne DOCKER-USER)
docker_hardening_firewall_allowed_sources:
- 10.0.4.0/24
- 10.0.5.0/24
# Ports publiés ouverts à toute source (à définir par hôte, ex. Traefik) :
# docker_hardening_firewall_public_ports:
# - { port: 443, proto: tcp }
+9
View File
@@ -0,0 +1,9 @@
---
all:
vars:
ansible_user: root
children:
docker:
hosts:
srv-deb-01:
ansible_host: 10.0.5.21
+67
View File
@@ -0,0 +1,67 @@
---
# ---------------------------------------------------------------------------
# Démon Docker (/etc/docker/daemon.json)
# ---------------------------------------------------------------------------
# Adresse d'écoute par défaut des ports publiés sans IP explicite (-p 8080:80).
# "127.0.0.1" : rien n'est exposé au réseau par accident ; un port à ouvrir au LAN
# doit être publié explicitement (ex. "0.0.0.0:443:443" pour Traefik).
docker_hardening_default_bind_ip: "127.0.0.1"
# Pas de communication libre entre conteneurs du bridge par défaut
docker_hardening_icc: false
# Les conteneurs survivent au redémarrage du démon
docker_hardening_live_restore: true
# Publication des ports par iptables plutôt que par le processus docker-proxy
docker_hardening_userland_proxy: false
# Interdit l'acquisition de privilèges (setuid, setcap) dans les conteneurs
docker_hardening_no_new_privileges: true
# Rotation des journaux des conteneurs
docker_hardening_log_max_size: "10m"
docker_hardening_log_max_file: 5
# Résolveurs transmis aux conteneurs ([] = ceux de l'hôte)
docker_hardening_dns: []
# Plages d'adresses des réseaux Docker ([] = plages par défaut)
# Exemple : [{ base: "172.20.0.0/14", size: 24 }]
docker_hardening_default_address_pools: []
# Registres sans TLS vérifié (à éviter) et miroirs
docker_hardening_insecure_registries: []
docker_hardening_registry_mirrors: []
# Remappage des UID ("" = désactivé, "default" = compte dockremap).
# À décider AVANT d'héberger des conteneurs : l'activer masque les images,
# conteneurs et volumes existants et impose de revoir les droits des bind mounts.
docker_hardening_userns_remap: ""
# Emplacement des données ("" = /var/lib/docker)
docker_hardening_data_root: ""
# ---------------------------------------------------------------------------
# Accès au démon
# ---------------------------------------------------------------------------
# Membres autorisés du groupe docker (équivalent root) : tout autre membre est retiré
docker_hardening_group_members: []
# ---------------------------------------------------------------------------
# Filtrage des ports publiés (chaîne DOCKER-USER)
# ---------------------------------------------------------------------------
# Docker insère ses propres règles iptables : sans ce filtrage, un port publié
# sur 0.0.0.0 est joignable depuis tout le réseau, quel que soit le pare-feu de l'hôte.
docker_hardening_firewall_enabled: true
# Interfaces d'entrée filtrées (par défaut : celle de la route par défaut)
docker_hardening_firewall_interfaces: >-
{{ [ansible_facts['default_ipv4']['interface']]
if ansible_facts['default_ipv4']['interface'] is defined else [] }}
# Sources autorisées à joindre tous les ports publiés
docker_hardening_firewall_allowed_sources: []
docker_hardening_firewall_allowed_sources_v6: []
# Ports publiés ouverts à toute source : [{ port: 443, proto: tcp }]
docker_hardening_firewall_public_ports: []
# Journaliser les paquets rejetés (limité à 5/min)
docker_hardening_firewall_log_drops: false
# ---------------------------------------------------------------------------
# Audit (auditd)
# ---------------------------------------------------------------------------
docker_hardening_audit_enabled: true
# Surveiller aussi le répertoire de données : très verbeux sur un hôte chargé
docker_hardening_audit_watch_data_root: false
+22
View File
@@ -0,0 +1,22 @@
---
# Ordre d'exécution = ordre de déclaration : le démon d'abord, le filtrage ensuite.
- name: Recharger systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Redémarrer Docker
ansible.builtin.systemd:
name: docker
state: restarted
- name: Recharger les règles d'audit
ansible.builtin.command:
cmd: augenrules --load
changed_when: true
- name: Appliquer le filtrage DOCKER-USER
ansible.builtin.systemd:
name: docker-user-firewall
state: restarted
daemon_reload: true
ignore_errors: "{{ ansible_check_mode }}"
+20
View File
@@ -0,0 +1,20 @@
---
- name: Lire les membres du groupe docker
ansible.builtin.getent:
database: group
key: docker
- name: Retirer du groupe docker les comptes non autorisés
ansible.builtin.command:
cmd: gpasswd -d {{ item }} docker
loop: "{{ docker_hardening_current_members | difference(docker_hardening_group_members) }}"
changed_when: true
vars:
docker_hardening_current_members: "{{ ansible_facts['getent_group']['docker'][2].split(',') | select | list }}"
- name: Ajouter au groupe docker les comptes autorisés
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: true
loop: "{{ docker_hardening_group_members }}"
+21
View File
@@ -0,0 +1,21 @@
---
- name: Installer auditd
ansible.builtin.apt:
name: auditd
state: present
- name: Déposer les règles d'audit Docker
ansible.builtin.template:
src: docker.rules.j2
dest: /etc/audit/rules.d/docker.rules
owner: root
group: root
mode: "0640"
notify: Recharger les règles d'audit
- name: Activer auditd
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
ignore_errors: "{{ ansible_check_mode }}"
+26
View File
@@ -0,0 +1,26 @@
---
- name: Sécuriser le répertoire de configuration Docker
ansible.builtin.file:
path: /etc/docker
state: directory
owner: root
group: root
mode: "0755"
- name: Déposer la configuration du démon
ansible.builtin.template:
src: daemon.json.j2
dest: /etc/docker/daemon.json
owner: root
group: root
mode: "0644"
validate: dockerd --validate --config-file %s
notify: Redémarrer Docker
- name: Maintenir le routage IPv4 nécessaire aux réseaux Docker
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: "1"
sysctl_file: /etc/sysctl.d/99-zz-docker.conf
state: present
reload: true
+55
View File
@@ -0,0 +1,55 @@
---
- name: Déployer le filtrage DOCKER-USER
when: docker_hardening_firewall_enabled
block:
- name: Déposer le script de filtrage
ansible.builtin.template:
src: docker-user-firewall.sh.j2
dest: /usr/local/sbin/docker-user-firewall
owner: root
group: root
mode: "0750"
validate: sh -n %s
notify: Appliquer le filtrage DOCKER-USER
- name: Déposer l'unité systemd du filtrage
ansible.builtin.template:
src: docker-user-firewall.service.j2
dest: /etc/systemd/system/docker-user-firewall.service
owner: root
group: root
mode: "0644"
notify: Appliquer le filtrage DOCKER-USER
- name: Activer le filtrage au démarrage de Docker
ansible.builtin.systemd:
name: docker-user-firewall
enabled: true
state: started
daemon_reload: true
# En mode check, l'unité n'a pas encore été déposée au premier passage
ignore_errors: "{{ ansible_check_mode }}"
- name: Retirer le filtrage DOCKER-USER
when: not docker_hardening_firewall_enabled
block:
- name: Rechercher l'unité de filtrage
ansible.builtin.stat:
path: /etc/systemd/system/docker-user-firewall.service
register: docker_hardening_fw_unit
- name: Arrêter et désactiver le filtrage
ansible.builtin.systemd:
name: docker-user-firewall
enabled: false
state: stopped
when: docker_hardening_fw_unit.stat.exists
- name: Supprimer le script et l'unité de filtrage
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/systemd/system/docker-user-firewall.service
- /usr/local/sbin/docker-user-firewall
notify: Recharger systemd
+49
View File
@@ -0,0 +1,49 @@
---
- name: Rechercher le démon Docker
ansible.builtin.stat:
path: /usr/bin/dockerd
register: docker_hardening_dockerd
- name: Signaler l'arrêt anticipé en mode check
ansible.builtin.debug:
msg: "Docker n'est pas encore installé : le durcissement sera évalué au premier passage réel."
when:
- ansible_check_mode
- not docker_hardening_dockerd.stat.exists
- name: Arrêter ici en mode check si Docker n'est pas installé
ansible.builtin.meta: end_host
when:
- ansible_check_mode
- not docker_hardening_dockerd.stat.exists
- name: Vérifier les paramètres du durcissement
ansible.builtin.assert:
that:
- not docker_hardening_firewall_enabled or docker_hardening_firewall_interfaces | length > 0
- docker_hardening_firewall_public_ports | rejectattr('port', 'defined') | list | length == 0
- docker_hardening_firewall_public_ports | map(attribute='proto', default='tcp') | difference(['tcp', 'udp']) | length == 0
fail_msg: >-
Paramètres invalides : interface de filtrage introuvable, ou entrée de
docker_hardening_firewall_public_ports sans « port » ou avec un protocole autre que tcp/udp.
quiet: true
- name: Appliquer la configuration du démon
ansible.builtin.import_tasks: daemon.yml
- name: Restreindre l'accès au démon
ansible.builtin.import_tasks: access.yml
- name: Filtrer les ports publiés
ansible.builtin.import_tasks: firewall.yml
- name: Auditer les composants Docker
ansible.builtin.import_tasks: audit.yml
when: docker_hardening_audit_enabled
- name: Appliquer les changements en attente avant contrôle
ansible.builtin.meta: flush_handlers
- name: Contrôler l'état final
ansible.builtin.import_tasks: verify.yml
when: not ansible_check_mode
+49
View File
@@ -0,0 +1,49 @@
---
- name: Relever la configuration effective de Docker
ansible.builtin.command:
cmd: docker info --format json
register: docker_hardening_info_raw
changed_when: false
- name: Relever la version du plugin Compose
ansible.builtin.command:
cmd: docker compose version --short
register: docker_hardening_compose_version
changed_when: false
- name: Rechercher une API Docker exposée en TCP
ansible.builtin.command:
cmd: ss -ltnH '( sport = :2375 or sport = :2376 )'
register: docker_hardening_tcp_api
changed_when: false
- name: Relever les droits du socket Docker
ansible.builtin.stat:
path: /run/docker.sock
register: docker_hardening_socket
- name: Vérifier le durcissement effectif
ansible.builtin.assert:
that:
- docker_hardening_info['SecurityOptions'] | select('match', '^name=seccomp') | list | length > 0
- docker_hardening_info['SecurityOptions'] | select('match', '^name=apparmor') | list | length > 0
- >-
not docker_hardening_no_new_privileges
or docker_hardening_info['SecurityOptions'] | select('match', '^name=no-new-privileges') | list | length > 0
- docker_hardening_info['LiveRestoreEnabled'] == (docker_hardening_live_restore | bool)
- docker_hardening_tcp_api.stdout | length == 0
- docker_hardening_socket.stat.exists
- docker_hardening_socket.stat.mode == '0660'
- docker_hardening_socket.stat.pw_name == 'root'
- docker_hardening_socket.stat.gr_name == 'docker'
fail_msg: "Le durcissement effectif ne correspond pas à la configuration attendue."
success_msg: >-
Docker {{ docker_hardening_info['ServerVersion'] }}, Compose {{ docker_hardening_compose_version.stdout }} :
seccomp, AppArmor, no-new-privileges, live-restore, socket 0660 root:docker, aucune API TCP.
vars:
docker_hardening_info: "{{ docker_hardening_info_raw.stdout | from_json }}"
- name: Afficher les avertissements du démon
ansible.builtin.debug:
msg: "{{ (docker_hardening_info_raw.stdout | from_json)['Warnings'] | default([], true) }}"
when: (docker_hardening_info_raw.stdout | from_json)['Warnings'] | default([], true) | length > 0
@@ -0,0 +1,34 @@
{#- Pas d'en-tête ansible_managed : le format JSON n'admet aucun commentaire. -#}
{
"icc": {{ docker_hardening_icc | bool | to_json }},
"ip": {{ docker_hardening_default_bind_ip | to_json }},
"live-restore": {{ docker_hardening_live_restore | bool | to_json }},
"userland-proxy": {{ docker_hardening_userland_proxy | bool | to_json }},
"no-new-privileges": {{ docker_hardening_no_new_privileges | bool | to_json }},
"experimental": false,
{% if docker_hardening_dns | length > 0 %}
"dns": {{ docker_hardening_dns | to_json }},
{% endif %}
{% if docker_hardening_default_address_pools | length > 0 %}
"default-address-pools": {{ docker_hardening_default_address_pools | to_json }},
{% endif %}
{% if docker_hardening_insecure_registries | length > 0 %}
"insecure-registries": {{ docker_hardening_insecure_registries | to_json }},
{% endif %}
{% if docker_hardening_registry_mirrors | length > 0 %}
"registry-mirrors": {{ docker_hardening_registry_mirrors | to_json }},
{% endif %}
{% if docker_hardening_userns_remap | length > 0 %}
"userns-remap": {{ docker_hardening_userns_remap | to_json }},
{% endif %}
{% if docker_hardening_data_root | length > 0 %}
"data-root": {{ docker_hardening_data_root | to_json }},
{% endif %}
"log-level": "info",
"log-driver": "json-file",
"log-opts": {
"max-size": {{ docker_hardening_log_max_size | to_json }},
"max-file": {{ docker_hardening_log_max_file | string | to_json }},
"compress": "true"
}
}
@@ -0,0 +1,14 @@
# {{ ansible_managed }}
[Unit]
Description=Filtrage des ports publiés par Docker (chaîne DOCKER-USER)
After=docker.service
PartOf=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/docker-user-firewall start
ExecStop=/usr/local/sbin/docker-user-firewall stop
[Install]
WantedBy=docker.service
@@ -0,0 +1,61 @@
#!/bin/sh
# {{ ansible_managed }}
#
# Filtre les connexions entrantes vers les ports publiés par les conteneurs
# (chaîne DOCKER-USER, consultée par Docker avant ses propres règles).
# Lancé par docker-user-firewall.service après docker.service.
# start : applique les règles stop : vide la chaîne
set -eu
{% macro rules(cmd, sources) %}
{% for iface in docker_hardening_firewall_interfaces %}
{{ cmd }} -w -A DOCKER-USER -i {{ iface }} -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
{% for p in docker_hardening_firewall_public_ports %}
{{ cmd }} -w -A DOCKER-USER -i {{ iface }} -p {{ p.proto | default('tcp') }} -m conntrack --ctorigdstport {{ p.port }} --ctdir ORIGINAL -j RETURN
{% endfor %}
{% for src in sources %}
{{ cmd }} -w -A DOCKER-USER -i {{ iface }} -s {{ src }} -j RETURN
{% endfor %}
{% if docker_hardening_firewall_log_drops %}
{{ cmd }} -w -A DOCKER-USER -i {{ iface }} -m limit --limit 5/min -j LOG --log-prefix "DOCKER-USER DROP: "
{% endif %}
{{ cmd }} -w -A DOCKER-USER -i {{ iface }} -j DROP
{% endfor %}
{% endmacro %}
reset_chain() {
"$1" -w -N DOCKER-USER 2>/dev/null || true
"$1" -w -F DOCKER-USER
}
rules_v4() {
{{ rules('iptables', docker_hardening_firewall_allowed_sources) -}}
}
rules_v6() {
{{ rules('ip6tables', docker_hardening_firewall_allowed_sources_v6) -}}
}
case "${1:-start}" in
start)
reset_chain iptables
rules_v4
iptables -w -A DOCKER-USER -j RETURN
if command -v ip6tables >/dev/null 2>&1; then
reset_chain ip6tables
rules_v6
ip6tables -w -A DOCKER-USER -j RETURN
fi
;;
stop)
reset_chain iptables
iptables -w -A DOCKER-USER -j RETURN
if command -v ip6tables >/dev/null 2>&1; then
reset_chain ip6tables
ip6tables -w -A DOCKER-USER -j RETURN
fi
;;
*)
echo "Usage : $0 {start|stop}" >&2
exit 2
;;
esac
@@ -0,0 +1,15 @@
## {{ ansible_managed }}
## Surveillance des composants Docker (recommandations CIS Docker Benchmark, section 1)
-w /usr/bin/dockerd -p rwxa -k docker
-w /usr/bin/containerd -p rwxa -k docker
-w /usr/bin/containerd-shim-runc-v2 -p rwxa -k docker
-w /usr/bin/runc -p rwxa -k docker
-w /etc/docker -p rwxa -k docker
-w /etc/containerd -p rwxa -k docker
-w /run/containerd -p rwxa -k docker
-w /usr/lib/systemd/system/docker.service -p rwxa -k docker
-w /usr/lib/systemd/system/docker.socket -p rwxa -k docker
-w /usr/lib/systemd/system/containerd.service -p rwxa -k docker
{% if docker_hardening_audit_watch_data_root %}
-w {{ docker_hardening_data_root | default('/var/lib/docker', true) }} -p rwxa -k docker
{% endif %}
+29
View File
@@ -0,0 +1,29 @@
---
# Distributions prises en charge : Debian par version majeure, Ubuntu par version complète
docker_install_supported:
Debian: ["12", "13"]
Ubuntu: ["22.04", "24.04"]
# Version de Docker Engine à installer ("" = dernière version stable du dépôt).
# Exemple : "29.1.5" — docker-ce et docker-ce-cli sont alors figés (apt hold).
docker_install_version: ""
# Paquets en conflit à retirer (paquets de la distribution et homonymes sans rapport)
docker_install_conflicting_packages:
- docker.io
- docker-doc
- docker-compose
- docker-compose-v2
- podman-docker
- containerd
- runc
- docker
- wmdocker
# Empreinte de la clé de signature officielle du dépôt Docker
docker_install_gpg_fingerprint: "9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
docker_install_gpg_key: /etc/apt/keyrings/docker.asc
docker_install_arch_map:
x86_64: amd64
aarch64: arm64
+112
View File
@@ -0,0 +1,112 @@
---
- name: Vérifier que la distribution est prise en charge
ansible.builtin.assert:
that:
- ansible_facts['distribution'] in docker_install_supported
- docker_install_os_version in docker_install_supported[ansible_facts['distribution']]
- ansible_facts['architecture'] in docker_install_arch_map
fail_msg: >-
{{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
({{ ansible_facts['architecture'] }}) n'est pas pris en charge par ce rôle.
quiet: true
- name: Retirer les paquets en conflit avec Docker CE
ansible.builtin.apt:
name: "{{ docker_install_conflicting_packages }}"
state: absent
purge: true
- name: Retirer l'ancienne déclaration du dépôt (format .list)
ansible.builtin.file:
path: /etc/apt/sources.list.d/docker.list
state: absent
- name: Installer les prérequis
ansible.builtin.apt:
name:
- ca-certificates
- gnupg
- apparmor
- iptables
state: present
update_cache: true
cache_valid_time: 3600
- name: Créer le répertoire des clés APT
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
- name: Télécharger la clé de signature du dépôt Docker
ansible.builtin.get_url:
url: "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg"
dest: "{{ docker_install_gpg_key }}"
owner: root
group: root
mode: "0644"
- name: Contrôler la présence de la clé
ansible.builtin.stat:
path: "{{ docker_install_gpg_key }}"
register: docker_install_key_stat
- name: Lire l'empreinte de la clé téléchargée
ansible.builtin.command:
cmd: gpg --show-keys --with-colons --with-fingerprint {{ docker_install_gpg_key }}
register: docker_install_key_fpr
changed_when: false
check_mode: false
when: docker_install_key_stat.stat.exists
- name: Vérifier l'empreinte de la clé Docker
ansible.builtin.assert:
that:
- docker_install_gpg_fingerprint in docker_install_key_fpr.stdout
fail_msg: "Empreinte de la clé Docker inattendue : le dépôt n'est pas déclaré."
quiet: true
when: docker_install_key_stat.stat.exists
- name: Déclarer le dépôt APT officiel Docker
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/docker.sources
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
Types: deb
URIs: https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
Suites: {{ ansible_facts['distribution_release'] }}
Components: stable
Architectures: {{ docker_install_arch_map[ansible_facts['architecture']] }}
Signed-By: {{ docker_install_gpg_key }}
register: docker_install_repo
- name: Installer Docker Engine et le plugin Compose
ansible.builtin.apt:
name: "{{ docker_install_packages_versioned }}"
state: present
install_recommends: false
allow_change_held_packages: true
update_cache: "{{ docker_install_repo is changed }}"
ignore_errors: "{{ ansible_check_mode }}"
- name: Figer ou libérer la version d'Engine
ansible.builtin.dpkg_selections:
name: "{{ item }}"
selection: "{{ 'hold' if docker_install_version | length > 0 else 'install' }}"
loop: "{{ docker_install_engine_packages }}"
when: not ansible_check_mode
- name: Activer et démarrer containerd et Docker
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- containerd
- docker
ignore_errors: "{{ ansible_check_mode }}"
+21
View File
@@ -0,0 +1,21 @@
---
docker_install_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
# Paquets portant la version d'Engine (figés si docker_install_version est renseignée)
docker_install_engine_packages:
- docker-ce
- docker-ce-cli
docker_install_packages_versioned: >-
{{ docker_install_packages if docker_install_version | length == 0
else ['docker-ce=5:' ~ docker_install_version ~ '*', 'docker-ce-cli=5:' ~ docker_install_version ~ '*']
+ docker_install_packages[2:] }}
docker_install_os_version: >-
{{ ansible_facts['distribution_major_version'] if ansible_facts['distribution'] == 'Debian'
else ansible_facts['distribution_version'] }}
+11
View File
@@ -0,0 +1,11 @@
---
# Installation et durcissement de Docker Engine + plugin Docker Compose
# Cibles : Debian 12/13, Ubuntu 22.04/24.04 — exécuté depuis Semaphore (branche « valide »)
- name: Docker - installation et durcissement
hosts: docker
gather_facts: true
roles:
- role: docker_install
tags: [docker_install]
- role: docker_hardening
tags: [docker_hardening]