From f92c9f0a1afdf1d268d86e301181f6f8fe8833da Mon Sep 17 00:00:00 2001 From: hcornet Date: Wed, 30 Sep 2026 16:08:36 +0200 Subject: [PATCH] remove history --- .ansible-lint | 10 + .gitattributes | 5 + .gitea/workflows/deploy.yml | 50 +++ .gitea/workflows/lint.yml | 76 ++++ .gitignore | 8 + .yamllint | 21 + README.md | 397 ++++++++++++++++++ ansible.cfg | 20 + examples/auth-basic.yml | 20 + examples/auth-kerberos.yml | 21 + examples/auth-ldap.yml | 26 ++ examples/auth-none.yml | 11 + group_vars/all/main.yml | 7 + group_vars/squid/main.yml | 56 +++ group_vars/squid/vault.yml.example | 16 + groups/debian.yml | 23 + groups/ubuntu.yml | 11 + groups/windows.yml | 31 ++ inventory/hosts.yml | 9 + roles/squid/defaults/main.yml | 263 ++++++++++++ roles/squid/handlers/main.yml | 23 + roles/squid/meta/main.yml | 23 + roles/squid/meta/runtime.yml | 2 + roles/squid/tasks/assert.yml | 113 +++++ roles/squid/tasks/auth.yml | 70 +++ roles/squid/tasks/confd.yml | 66 +++ roles/squid/tasks/configure.yml | 54 +++ roles/squid/tasks/install.yml | 38 ++ roles/squid/tasks/main.yml | 43 ++ roles/squid/tasks/profiles.yml | 94 +++++ roles/squid/tasks/reports.yml | 139 ++++++ roles/squid/tasks/service.yml | 20 + roles/squid/tasks/ssl_bump.yml | 107 +++++ roles/squid/tasks/verify.yml | 55 +++ roles/squid/templates/auth/basic.conf.j2 | 7 + roles/squid/templates/auth/kerberos.conf.j2 | 6 + roles/squid/templates/auth/ldap.conf.j2 | 14 + roles/squid/templates/auth/none.conf.j2 | 2 + roles/squid/templates/auth/passwd.j2 | 3 + .../templates/auth/squid-kerberos.conf.j2 | 4 + roles/squid/templates/confd/group.conf.j2 | 32 ++ roles/squid/templates/confd/ports.conf.j2 | 8 + roles/squid/templates/confd/server.conf.j2 | 36 ++ roles/squid/templates/lists/domains.txt.j2 | 7 + roles/squid/templates/sarg-exclusions.j2 | 5 + roles/squid/templates/sarg-nginx.conf.j2 | 25 ++ roles/squid/templates/sarg-rapport.sh.j2 | 38 ++ roles/squid/templates/sarg.conf.j2 | 44 ++ roles/squid/templates/squid.conf.j2 | 215 ++++++++++ roles/squid/vars/main.yml | 26 ++ scripts/import-conf-d.py | 232 ++++++++++ scripts/openbao-env.sh | 50 +++ scripts/trigger-semaphore.sh | 76 ++++ servers/swappfond01.yml | 14 + servers/swappfond02.yml | 16 + servers/swappfond03.yml | 28 ++ servers/swappfono01.yml | 11 + servers/swappfono02.yml | 42 ++ servers/swappfono03.yml | 20 + servers/swappfonp01.yml | 76 ++++ servers/swappfonp02.yml | 16 + servers/swappfonp03.yml | 21 + servers/swappfonr01.yml | 16 + servers/swappfonr02.yml | 16 + servers/swappfonr03.yml | 15 + site.yml | 8 + tests/ci-paths.yml | 12 + tests/groups-ci/windows.yml | 9 + tests/inventory-ci.yml | 5 + tests/render.yml | 105 +++++ tests/scenarios/allowlist-cache.yml | 15 + tests/scenarios/basic.yml | 9 + tests/scenarios/kerberos.yml | 7 + tests/scenarios/ldap.yml | 11 + tests/scenarios/minimal.yml | 7 + tests/scenarios/none.yml | 6 + tests/servers-ci/SRV01.yml | 11 + tests/servers-ci/SRV02.yml | 8 + 78 files changed, 3262 insertions(+) create mode 100644 .ansible-lint create mode 100644 .gitattributes create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitea/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .yamllint create mode 100644 README.md create mode 100644 ansible.cfg create mode 100644 examples/auth-basic.yml create mode 100644 examples/auth-kerberos.yml create mode 100644 examples/auth-ldap.yml create mode 100644 examples/auth-none.yml create mode 100644 group_vars/all/main.yml create mode 100644 group_vars/squid/main.yml create mode 100644 group_vars/squid/vault.yml.example create mode 100644 groups/debian.yml create mode 100644 groups/ubuntu.yml create mode 100644 groups/windows.yml create mode 100644 inventory/hosts.yml create mode 100644 roles/squid/defaults/main.yml create mode 100644 roles/squid/handlers/main.yml create mode 100644 roles/squid/meta/main.yml create mode 100644 roles/squid/meta/runtime.yml create mode 100644 roles/squid/tasks/assert.yml create mode 100644 roles/squid/tasks/auth.yml create mode 100644 roles/squid/tasks/confd.yml create mode 100644 roles/squid/tasks/configure.yml create mode 100644 roles/squid/tasks/install.yml create mode 100644 roles/squid/tasks/main.yml create mode 100644 roles/squid/tasks/profiles.yml create mode 100644 roles/squid/tasks/reports.yml create mode 100644 roles/squid/tasks/service.yml create mode 100644 roles/squid/tasks/ssl_bump.yml create mode 100644 roles/squid/tasks/verify.yml create mode 100644 roles/squid/templates/auth/basic.conf.j2 create mode 100644 roles/squid/templates/auth/kerberos.conf.j2 create mode 100644 roles/squid/templates/auth/ldap.conf.j2 create mode 100644 roles/squid/templates/auth/none.conf.j2 create mode 100644 roles/squid/templates/auth/passwd.j2 create mode 100644 roles/squid/templates/auth/squid-kerberos.conf.j2 create mode 100644 roles/squid/templates/confd/group.conf.j2 create mode 100644 roles/squid/templates/confd/ports.conf.j2 create mode 100644 roles/squid/templates/confd/server.conf.j2 create mode 100644 roles/squid/templates/lists/domains.txt.j2 create mode 100644 roles/squid/templates/sarg-exclusions.j2 create mode 100644 roles/squid/templates/sarg-nginx.conf.j2 create mode 100644 roles/squid/templates/sarg-rapport.sh.j2 create mode 100644 roles/squid/templates/sarg.conf.j2 create mode 100644 roles/squid/templates/squid.conf.j2 create mode 100644 roles/squid/vars/main.yml create mode 100644 scripts/import-conf-d.py create mode 100644 scripts/openbao-env.sh create mode 100644 scripts/trigger-semaphore.sh create mode 100644 servers/swappfond01.yml create mode 100644 servers/swappfond02.yml create mode 100644 servers/swappfond03.yml create mode 100644 servers/swappfono01.yml create mode 100644 servers/swappfono02.yml create mode 100644 servers/swappfono03.yml create mode 100644 servers/swappfonp01.yml create mode 100644 servers/swappfonp02.yml create mode 100644 servers/swappfonp03.yml create mode 100644 servers/swappfonr01.yml create mode 100644 servers/swappfonr02.yml create mode 100644 servers/swappfonr03.yml create mode 100644 site.yml create mode 100644 tests/ci-paths.yml create mode 100644 tests/groups-ci/windows.yml create mode 100644 tests/inventory-ci.yml create mode 100644 tests/render.yml create mode 100644 tests/scenarios/allowlist-cache.yml create mode 100644 tests/scenarios/basic.yml create mode 100644 tests/scenarios/kerberos.yml create mode 100644 tests/scenarios/ldap.yml create mode 100644 tests/scenarios/minimal.yml create mode 100644 tests/scenarios/none.yml create mode 100644 tests/servers-ci/SRV01.yml create mode 100644 tests/servers-ci/SRV02.yml diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..8b592c5 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,10 @@ +--- +profile: production + +exclude_paths: + - .venv/ + - .gitea/ + +# Les collections externes ne sont pas installées dans le conteneur +# semaphore-app : le rôle n'utilise que ansible.builtin. +mock_roles: [] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a137fc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.yml text eol=lf +*.j2 text eol=lf +*.sh text eol=lf +*.cfg text eol=lf diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e59d320 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,50 @@ +--- +name: Déploiement Squid + +on: + push: + branches: [main] + paths: + - site.yml + - ansible.cfg + - group_vars/** + - inventory/** + - roles/** + workflow_dispatch: + inputs: + +concurrency: + group: deploiement-squid + cancel-in-progress: false + +jobs: + deployer: + name: Déclencher le modèle Semaphore + runs-on: ubuntu-latest + steps: + - name: Récupérer le dépôt + uses: actions/checkout@v4 + + - name: Installer curl et jq + run: | + set -euo pipefail + SUDO="" + if [ "$(id -u)" -ne 0 ]; then SUDO=sudo; fi + ${SUDO} apt-get update -qq + ${SUDO} apt-get install -y -qq curl jq + + - name: Lancer la tâche et suivre son issue + env: + OPENBAO_ADDR: ${{ secrets.OPENBAO_ADDR }} + OPENBAO_ROLE_ID: ${{ secrets.OPENBAO_ROLE_ID }} + OPENBAO_SECRET_ID: ${{ secrets.OPENBAO_SECRET_ID }} + OPENBAO_SECRET_PATH: secret/data/squid + SEMAPHORE_LIMIT: ${{ inputs.limite }} + SEMAPHORE_TAGS: ${{ inputs.etiquettes }} + SEMAPHORE_DRY_RUN: ${{ inputs.simulation || 'false' }} + run: | + set -euo pipefail + chmod +x scripts/openbao-env.sh scripts/trigger-semaphore.sh + # shellcheck source=/dev/null + source scripts/openbao-env.sh + ./scripts/trigger-semaphore.sh diff --git a/.gitea/workflows/lint.yml b/.gitea/workflows/lint.yml new file mode 100644 index 0000000..7ff9d23 --- /dev/null +++ b/.gitea/workflows/lint.yml @@ -0,0 +1,76 @@ +--- +name: Qualité du code Ansible + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + lint: + name: yamllint, ansible-lint et syntaxe du playbook + runs-on: ubuntu-latest + steps: + - name: Récupérer le dépôt + uses: actions/checkout@v4 + + - name: Installer l'outillage + run: | + set -euo pipefail + python3 -m pip install --quiet --upgrade --break-system-packages \ + "ansible-core>=2.15" "ansible-lint>=24" "yamllint>=1.35" + + - name: Contrôler la syntaxe YAML + run: yamllint . + + - name: Contrôler les bonnes pratiques Ansible + run: ansible-lint + + - name: Contrôler la syntaxe du playbook + run: ansible-playbook --syntax-check site.yml + + configuration: + name: Validation de squid.conf par Squid lui-même + runs-on: ubuntu-latest + steps: + - name: Récupérer le dépôt + uses: actions/checkout@v4 + + - name: Installer Ansible et Squid + run: | + set -euo pipefail + SUDO="" + if [ "$(id -u)" -ne 0 ]; then SUDO=sudo; fi + python3 -m pip install --quiet --upgrade --break-system-packages "ansible-core>=2.15" + ${SUDO} apt-get update -qq + ${SUDO} apt-get install -y -qq squid-openssl openssl nginx-light + + - name: Préparer une autorité de test et la base de certificats + run: | + set -euo pipefail + mkdir -p /tmp/squid-render + openssl req -new -newkey rsa:2048 -sha256 -days 30 -nodes -x509 \ + -extensions v3_ca -subj "/CN=CI Test CA" \ + -keyout /tmp/squid-render/ca.key -out /tmp/squid-render/ca.crt 2>/dev/null + /usr/lib/squid/security_file_certgen -c -s /tmp/squid-render/ssl_db -M 4MB + + - name: Rendre et valider chaque scénario + run: | + set -euo pipefail + echec=0 + for scenario in tests/scenarios/*.yml; do + nom=$(basename "${scenario}" .yml) + ansible-playbook tests/render.yml \ + --inventory tests/inventory-ci.yml \ + --extra-vars "@${scenario}" >/dev/null + sh -n /tmp/squid-render/sarg-rapport.sh + if /usr/sbin/squid -k parse -f /tmp/squid-render/squid.conf >/tmp/parse.log 2>&1; then + echo "${nom} : configuration valide" + else + echo "${nom} : ECHEC" + grep -iE "error|fatal" /tmp/parse.log || true + echec=1 + fi + done + exit "${echec}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98cd0ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.retry +*.log +.vault_pass +vault.yml +group_vars/**/vault.yml +inventory/*.local.yml +.venv/ +__pycache__/ diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..8139c73 --- /dev/null +++ b/.yamllint @@ -0,0 +1,21 @@ +--- +extends: default + +rules: + line-length: + max: 200 + level: warning + truthy: + allowed-values: ["true", "false"] + check-keys: 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 + +ignore: | + .venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfe1e54 --- /dev/null +++ b/README.md @@ -0,0 +1,397 @@ +# ansible-squid + +Déploiement d'un proxy web **Squid** sur Debian, piloté depuis **Semaphore** +et déclenché par **Gitea Actions**. + +Le rôle couvre les ACL réseau, le filtrage de domaines, les quatre méthodes +d'authentification (aucune, basic, LDAP/AD, Kerberos), le mode explicite et +l'interception transparente, et le déchiffrement TLS (SSL Bump). + +--- + +## 1. Contraintes assumées + +| Contrainte | Conséquence dans le dépôt | +|---|---| +| Le conteneur `semaphore-app` n'embarque aucune collection Ansible | Le rôle n'utilise que `ansible.builtin`. Pas de `requirements.yml`, rien à installer. | +| Le paquet Debian `squid` n'est pas compilé avec OpenSSL | Quand `squid_ssl_bump` vaut `true`, le rôle installe `squid-openssl` à la place. APT remplace l'un par l'autre. | +| Une configuration Squid invalide casse le service | `squid.conf` est déployé avec `validate: squid -k parse`, et la CI rejoue ce contrôle sur six scénarios avant tout déploiement. | +| Un port intercepté ne peut pas porter d'authentification | Les ports interceptés sont nommés et exemptés automatiquement (ACL `myportname`). Le contrôle y reste basé sur l'adresse source. | + +--- + +## 2. Arborescence + +``` +ansible.cfg configuration Ansible (callback default + result_format yaml) +site.yml playbook unique, groupe « squid » +inventory/hosts.yml inventaire d'exemple +group_vars/all/main.yml variables communes +group_vars/squid/main.yml paramétrage du proxy +group_vars/squid/vault.yml.example secrets à chiffrer (ou à mettre dans Semaphore) +examples/auth-*.yml un exemple complet par méthode d'authentification +roles/squid/ + defaults/main.yml toutes les variables surchargeables + vars/main.yml chemins des helpers Debian, raccourcis calculés + tasks/assert.yml validation des variables avant toute action + tasks/install.yml paquets, répertoires, sauvegarde de la conf d'origine + tasks/ssl_bump.yml autorité de certification et base de certificats générés + tasks/auth.yml fichiers de la méthode retenue, nettoyage des autres + tasks/configure.yml listes de filtrage puis squid.conf (avec validation) + tasks/service.yml cache éventuel, activation, flush des handlers + tasks/verify.yml ports, syntaxe, requête de bout en bout + templates/squid.conf.j2 gabarit principal + templates/auth/*.j2 un fragment par méthode + templates/lists/domains.txt.j2 +tests/ rendu hors ligne et scénarios utilisés par la CI +scripts/openbao-env.sh lecture des secrets Semaphore dans OpenBao +scripts/trigger-semaphore.sh déclenchement et suivi de la tâche Semaphore +.gitea/workflows/lint.yml qualité + validation de la configuration générée +.gitea/workflows/deploy.yml déploiement via Semaphore +``` + +--- + +## 3. Prérequis + +**Sur la cible** : Debian 12 ou 13, accès SSH avec un compte disposant de +`sudo`, sortie Internet. + +**Côté Semaphore** (projet `Production`, celui déjà utilisé pour Centreon) : + +1. Clé d'accès Gitea existante (`svc-semaphore`, type « Login with password »). +2. Dépôt : `https://gitea.tips-of-mine.com/Tips-Of-Mine/ansible-squid.git`, + branche `main`. +3. Inventaire de type `file` pointant sur `inventory/hosts.yml`, avec la clé + SSH du compte `ansible`. +4. Groupe de variables (menu **Variable Groups** depuis la 2.19) nommé + `Squid`, pour les secrets : `squid_ldap_bind_password`, + `squid_kerberos_keytab_b64`, `squid_ssl_ca_key_content`. +5. Modèle `Déploiement Squid` sur `site.yml`. + +**Côté Gitea** : trois secrets de dépôt seulement — `OPENBAO_ADDR`, +`OPENBAO_ROLE_ID`, `OPENBAO_SECRET_ID`. Tout le reste vit dans OpenBao sous +`secret/squid` : `semaphore_url`, `semaphore_token`, `semaphore_project_id`, +`semaphore_template_id`. + +--- + +## 4. Démarrage rapide + +```bash +# 1. Renseigner l'inventaire +$EDITOR inventory/hosts.yml + +# 2. Ajuster le paramétrage +$EDITOR group_vars/squid/main.yml + +# 3. Déployer +ansible-playbook site.yml +``` + +Depuis Semaphore, lancer le modèle `Déploiement Squid`. Depuis Gitea, +onglet **Actions** → *Déploiement Squid* → **Run workflow**, avec au besoin +un `--limit`, des étiquettes (`install`, `ssl`, `auth`, `config`, `service`, +`verify`) ou le mode simulation. + +--- + +## 5. Variables principales + +| Variable | Défaut | Rôle | +|---|---|---| +| `squid_mode` | `both` | `explicit`, `intercept` ou les deux | +| `squid_http_port` | `3128` | port du proxy explicite | +| `squid_intercept_http_port` | `3129` | port d'interception HTTP | +| `squid_intercept_https_port` | `3130` | port d'interception HTTPS (SSL Bump) | +| `squid_allowed_networks` | `10.0.4.0/24` | réseaux autorisés | +| `squid_denied_sources` | `[]` | adresses refusées avant toute autre règle | +| `squid_filter_mode` | `blocklist` | `blocklist` ou `allowlist` | +| `squid_denied_domains` | `[]` | domaines interdits | +| `squid_allowed_domains` | `[]` | domaines autorisés (obligatoire en `allowlist`) | +| `squid_auth_method` | `none` | `none`, `basic`, `ldap`, `kerberos` | +| `squid_ssl_bump` | `false` | déchiffrement TLS | +| `squid_no_bump_domains` | 3 entrées | domaines relayés sans déchiffrement | +| `squid_cache_enabled` | `false` | cache disque | +| `squid_log_to_syslog` | `false` | journalisation vers syslog plutôt qu'en fichier | +| `squid_verify` | `true` | contrôles de fin de déploiement | + +La liste exhaustive et commentée est dans `roles/squid/defaults/main.yml`. + +Un point de vigilance : en mode `allowlist`, tout ce qui n'est pas dans +`squid_allowed_domains` est refusé, y compris les domaines de mise à jour du +système. Le rôle refuse d'ailleurs de partir si la liste est vide. + +--- + +## 6. Les quatre authentifications + +Chaque méthode dispose d'un exemple prêt à copier dans `group_vars/squid/` : + +- `examples/auth-none.yml` — contrôle par adresse source uniquement. Seule + méthode compatible avec l'interception. +- `examples/auth-basic.yml` — comptes locaux. Les condensats se génèrent + avec `openssl passwd -apr1` (saisie interactive, rien dans l'historique). +- `examples/auth-ldap.yml` — annuaire AD, avec restriction optionnelle par + groupe. Le mot de passe du compte de service est lu dans un fichier (`-W`) + et n'apparaît jamais dans `ps`. +- `examples/auth-kerberos.yml` — SSO domaine. Contient les commandes + `setspn` et `ktpass` à passer côté contrôleur de domaine, et l'encodage du + keytab en base64. + +Changer de méthode suffit : `tasks/auth.yml` supprime les fichiers de celles +qui ne sont plus retenues. + +--- + +## 7. SSL Bump + +Par défaut le rôle génère une autorité auto-signée +(`squid_ssl_ca_self_signed: true`). En production, fournir plutôt une +autorité issue de ta PKI via `squid_ssl_ca_cert_content` et +`squid_ssl_ca_key_content`, alimentées par Semaphore ou OpenBao. + +Le certificat public est déposé sur la cible dans +`/usr/local/share/ca-certificates/squid-proxy-ca.crt`. **Il doit être +distribué à tous les postes clients**, sinon chaque site HTTPS produira une +erreur de certificat. Sur un parc AD, une GPO « Autorités de certification +racines de confiance » fait l'affaire. + +Les domaines de `squid_no_bump_domains` sont relayés sans déchiffrement +(`ssl_bump splice`) : à alimenter avec la banque, la santé, les impôts, et +tout ce qui pratique l'épinglage de certificat. Les domaines interdits sont +coupés dès le SNI (`ssl_bump terminate`), avant même le déchiffrement. + +--- + +## 8. Mode interception + +Le rôle configure Squid ; la redirection du trafic reste à faire sur le +firewall. Sur OPNsense, deux règles de NAT de type *port forward* depuis le +réseau client : + +| Protocole | Port source | Destination | Redirection | +|---|---|---|---| +| TCP | 80 | toute | `IP_du_proxy:3129` | +| TCP | 443 | toute | `IP_du_proxy:3130` | + +Le proxy lui-même doit être exclu de ces règles, faute de quoi le trafic +sortant de Squid revient sur Squid. + +Pour le mode explicite, le plus simple reste un fichier PAC distribué par +GPO, ou la variable d'environnement `http_proxy` sur les serveurs Linux. + +--- + +## 9. Vérifications + +Le rôle termine par une série de contrôles (`squid_verify`) : ouverture des +ports, `squid -k parse` sur la configuration en place, et une requête réelle +à travers le proxy quand l'authentification le permet. + +À la main, sur la cible : + +```bash +# Version et syntaxe +squid -k parse >/dev/null && echo "configuration valide" + +# Test explicite +curl -sS -o /dev/null -w '%{http_code}\n' -x http://127.0.0.1:3128 http://www.debian.org/ + +# Test d'un domaine interdit (403 attendu) +curl -sS -o /dev/null -w '%{http_code}\n' -x http://127.0.0.1:3128 http://doubleclick.net/ + +# Dix dernières lignes du journal, sans le détail des requêtes internes +tail -n 10 /var/log/squid/access.log +``` + +Deux comportements normaux à ne pas chercher à corriger : + +- `WARNING: HTTP requires the use of Via` au démarrage : conséquence directe + de `via off`, choisi pour ne pas annoncer le proxy aux serveurs distants. +- `logfile_rotate 0` : la rotation est confiée à logrotate, qui appelle + `squid -k rotate`. Le fichier `/etc/squid/conf.d/` n'est volontairement + pas inclus, la configuration est entièrement pilotée par le rôle. + +--- + +## 10. Intégration continue + +`lint.yml` (à chaque push et chaque PR) : + +1. `yamllint` sur tout le dépôt ; +2. `ansible-lint` en profil **production** ; +3. `ansible-playbook --syntax-check` ; +4. rendu des six scénarios de `tests/scenarios/` puis validation de chaque + `squid.conf` par `squid -k parse`, avec un vrai Squid installé dans le + job. C'est ce contrôle qui a détecté, avant livraison, une inclusion de + gabarit qui collait deux directives sur la même ligne. + +`deploy.yml` (push sur `main` touchant le code Ansible, ou lancement +manuel) : lecture des secrets dans OpenBao par AppRole, déclenchement du +modèle Semaphore, puis attente du résultat. Le job échoue si la tâche +Semaphore échoue. + +Les jetons ne sont jamais affichés : `openbao-env.sh` les masque +(`::add-mask::`) et n'écrit que l'URL Semaphore visée. + +--- + +## 11. Pièges connus + +- **Bit d'exécution des scripts** : si les fichiers de `scripts/` arrivent + sans droit d'exécution depuis Windows, corriger une fois pour toutes avec + `git update-index --chmod=+x scripts/openbao-env.sh scripts/trigger-semaphore.sh`. + Le workflow applique de toute façon un `chmod +x` avant appel. +- **Fins de ligne** : `.gitattributes` force le LF sur les `.yml`, `.j2`, + `.sh` et `.cfg`. +- **Première exécution avec SSL Bump** : l'installation de `squid-openssl` + remplace le paquet `squid` et redémarre le service. Prévoir une courte + coupure si le proxy est déjà en production. + +--- + +## 12. Modèle par serveur (fragments conf.d) + +Chaque machine autorisée possède une fiche dans `servers/`, chaque +regroupement une fiche dans `groups/`. Le rôle en dérive un fichier par +entité dans `/etc/squid/conf.d/`, chargé par le socle **avant** le +`http_access deny all` final. + +```yaml +# servers/srv-warda-01.yml +name: srv-warda-01 +env: prod +sources: + - srv-warda-01.tips-of-mine.local +groups: [windows] # documentaire : l'appartenance fait foi côté groupe +rules: + - domains: [javadl.oracle.com, www.java.com] + ports: [https] +``` + +```yaml +# groups/windows.yml +name: windows +members: # seules ces machines bénéficient du groupe + - srv-warda-01.tips-of-mine.local +rules: + - domains: [download.windowsupdate.com] + ports: [http, https] +``` + +Fichiers produits : `00-ports.conf` (catalogue de ports partagé), +`10-groupe-.conf`, `20-.conf`. + +### Garantie de non-accès + +Une machine n'obtient un flux que si son FQDN figure explicitement dans une +ACL `src`. Trois mécanismes la protègent : + +1. `squid_allow_localnet` bascule **automatiquement à false** dès qu'une + fiche existe : plus aucune autorisation globale par réseau. +2. Le rôle refuse de déployer si un membre de groupe n'a pas de fiche + serveur correspondante. +3. `http_access deny all` clôt le socle, après l'`include`. + +### Nommage des ACL de destination + +Le nom est dérivé du domaine lui-même : `go.microsoft.com` devient +`go_microsoft_com`, `.datadoghq.com` devient `_datadoghq_com`. Même domaine += toujours le même nom et la même valeur, donc deux fiches ne peuvent pas +définir la même ACL avec des contenus différents. C'est ce qui se produit +aujourd'hui entre plusieurs fragments écrits à la main. + +### Ports + +Le catalogue vit dans `squid_port_acls` et n'est rendu qu'une fois, dans +`00-ports.conf`. Les fiches ne référencent que des clés (`https`, `sftp`, +`8443`…) ; une clé absente du catalogue fait échouer le déploiement avec la +liste des ports fautifs. + +### Reprise de l'existant + +```bash +python3 scripts/import-conf-d.py /etc/squid/conf.d --sortie servers/ +``` + +Le script produit une fiche par fragment et un rapport d'anomalies. Passé +sur les cinq fichiers de production fournis, il a relevé : une ACL +référencée mais jamais définie (Squid refuse de démarrer dans ce cas), un +domaine terminé par `/`, une adresse IP en `dstdomain`, une ligne tronquée +et plusieurs ACL déclarées deux fois. `--rapport-seul` fait l'audit sans +rien écrire. + +### Points vérifiés sur Squid 6.14 + +- Les noms d'ACL sont **insensibles à la casse** : `srv-warda-02_prod` et + `srv-warda-02_prod` désignent la même ACL. +- Une ACL référencée sans être définie est **fatale**, pas un avertissement. +- `dns_v4_first` est **obsolète** depuis Squid 6 et génère une erreur au + démarrage : `squid_dns_v4_first` est donc à `false` par défaut. +- `acl src ` impose une résolution DNS au démarrage. Si un nom + ne résout plus, Squid **refuse de démarrer**. Des sources en adresses IP + suppriment cette dépendance. + +--- + +## 13. Rapports d'utilisation (SARG) + +Désactivé par défaut. Activation : + +```yaml +squid_reports_enabled: true +``` + +Le rôle installe `sarg` depuis les dépôts Debian, génère `/etc/sarg/sarg.conf`, +un script `/usr/local/sbin/sarg-rapport.sh` et les entrées `/etc/cron.d/` +correspondantes, puis publie les rapports par un nginx local. + +### Périodes et consultation + +Trois périodes, réglables par `squid_sarg_periods` : `daily` (la veille), +`weekly` (7 jours glissants), `monthly` (mois précédent). Les rapports +atterrissent dans `/var/www/sarg/{quotidien,hebdomadaire,mensuel}` et sont +consultables sur le port 8081, **restreint aux réseaux de +`squid_sarg_web_allowed`** — par défaut les mêmes que `squid_allowed_networks`. +Aucune exposition publique, pas d'indexation, pas de cache navigateur. + +Le script passe à SARG le journal courant **et** `access.log.1` s'il existe, +pour couvrir une rotation intervenue entre-temps. + +### Contenu des rapports + +`topusers`, `topsites`, `sites_users`, `users_sites`, `denied` et +`auth_failures`. Sans authentification, `records_without_userid: ip` rattache +chaque ligne à l'adresse source ; avec LDAP ou Kerberos, les rapports sont +nominatifs. + +Le rapport `denied` est le plus utile au quotidien : il liste les tentatives +refusées, donc les machines qui cherchent à sortir sans y être autorisées. + +### Points vérifiés sur SARG 2.4.0 + +- La directive `language` n'existe plus et provoque `Unknown option language`. + Elle a été retirée du gabarit ; `charset` suffit. +- Le type de rapport `date_time` échoue à créer le répertoire de son graphique + (`Cannot open .../graph.html`). Il est volontairement absent des valeurs par + défaut. L'ajouter à `squid_sarg_report_type` fait réapparaître l'erreur. +- SARG ne crée pas les sous-répertoires de période : le rôle s'en charge. + +### Vie privée + +Avec le SSL Bump actif, les rapports contiennent les URL complètes, pas +seulement les noms de domaine. Deux leviers : + +```yaml +squid_sarg_privacy: true # masque les URL, conserve les agrégats +squid_sarg_exclude_hosts: # domaines absents des rapports + - .ameli.fr +squid_sarg_exclude_users: [] +squid_sarg_keep: 90 # nombre de rapports conservés par période +``` + +En entreprise, un rapport nominatif d'historique de navigation relève du suivi +individuel : information des personnes, inscription au registre des traitements +et durée de conservation définie sont à cadrer avant la mise en service. + diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..37008cc --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,20 @@ +[defaults] +inventory = inventory/hosts.yml +roles_path = roles +interpreter_python = /usr/bin/python3 +host_key_checking = True +stdout_callback = ansible.builtin.default +result_format = yaml +callbacks_enabled = ansible.posix.profile_tasks +display_skipped_hosts = False +retry_files_enabled = False +forks = 5 +remote_user = root + +# Pas de section [privilege_escalation] : la machine n'a pas la commande sudo. +# Ansible se connecte directement en root par clé, c'est sa voie d'accès. +# Pour un accès par compte non privilégié, passer squid_become à true. + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o StrictHostKeyChecking=accept-new diff --git a/examples/auth-basic.yml b/examples/auth-basic.yml new file mode 100644 index 0000000..d226ec3 --- /dev/null +++ b/examples/auth-basic.yml @@ -0,0 +1,20 @@ +--- +# Exemple 2/4 — Authentification basic sur fichier local +# Adapté à quelques comptes, sans annuaire. Les identifiants circulent en +# base64 : à réserver au proxy explicite sur un réseau maîtrisé. +# +# Génération d'un condensat, sur une machine disposant d'OpenSSL : +# openssl passwd -apr1 +# (saisie interactive du mot de passe, rien n'apparaît dans l'historique) + +squid_auth_method: basic +squid_auth_realm: Proxy Tips-Of-Mine +squid_auth_children: 20 +squid_auth_ttl: 2 hours +squid_basic_passwd_file: /etc/squid/passwd + +squid_basic_users: + - username: hcornet + password_hash: "$apr1$k8Hn2xQr$0hQK7Z0v7mJ7pZ3xY2b1d." + - username: svc-maj + password_hash: "$apr1$Tg4rPz1s$9pQ2mN5vL8xR3kD0wS7t01" diff --git a/examples/auth-kerberos.yml b/examples/auth-kerberos.yml new file mode 100644 index 0000000..f7ef630 --- /dev/null +++ b/examples/auth-kerberos.yml @@ -0,0 +1,21 @@ +--- +# Exemple 4/4 — Authentification Kerberos (SSO domaine) +# Aucun mot de passe ne circule : le poste joint au domaine présente un +# ticket de service. C'est la méthode à privilégier avec un AD. +# +# Côté contrôleur de domaine, création du principal et du keytab : +# setspn -S HTTP/proxy01.lab.tips-of-mine.internal LAB\svc-squid +# ktpass -princ HTTP/proxy01.lab.tips-of-mine.internal@LAB.TIPS-OF-MINE.INTERNAL ` +# -mapuser LAB\svc-squid -crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL ` +# -pass * -out HTTP.keytab +# Puis encodage pour Ansible : +# base64 -w0 HTTP.keytab +# +# Les clients doivent viser le proxy par son FQDN (celui du principal), +# jamais par son adresse IP, sinon le navigateur retombe en NTLM ou échoue. + +squid_auth_method: kerberos +squid_kerberos_realm: LAB.TIPS-OF-MINE.INTERNAL +squid_kerberos_service_principal: HTTP/proxy01.lab.tips-of-mine.internal +squid_kerberos_keytab: /etc/squid/HTTP.keytab +# squid_kerberos_keytab_b64 : à fournir par vault.yml ou par Semaphore diff --git a/examples/auth-ldap.yml b/examples/auth-ldap.yml new file mode 100644 index 0000000..db13bd3 --- /dev/null +++ b/examples/auth-ldap.yml @@ -0,0 +1,26 @@ +--- +# Exemple 3/4 — Authentification LDAP / Active Directory +# Les comptes restent dans l'annuaire ; le proxy interroge l'AD à chaque +# nouvelle session (mise en cache pendant squid_auth_ttl). +# Le mot de passe du compte de service est déposé dans un fichier lu par le +# helper (-W) : il n'apparaît jamais dans la table des processus. + +squid_auth_method: ldap +squid_auth_realm: Proxy Tips-Of-Mine + +squid_ldap_server: dc01.lab.tips-of-mine.internal +squid_ldap_port: 389 +squid_ldap_use_starttls: true +squid_ldap_base_dn: "OU=Utilisateurs,DC=lab,DC=tips-of-mine,DC=internal" +squid_ldap_bind_dn: "CN=svc-squid,OU=Services,DC=lab,DC=tips-of-mine,DC=internal" +# squid_ldap_bind_password : à fournir par vault.yml ou par Semaphore +squid_ldap_search_filter: (sAMAccountName=%s) +squid_ldap_version: 3 + +# Restriction supplémentaire par appartenance à un groupe +squid_ldap_group_acl: true +squid_ldap_group_base_dn: "OU=Groupes,DC=lab,DC=tips-of-mine,DC=internal" +squid_ldap_group_filter: "(&(objectClass=group)(cn=%g)(member=%u))" +squid_ldap_allowed_groups: + - GG_Proxy_Internet + - GG_Administrateurs diff --git a/examples/auth-none.yml b/examples/auth-none.yml new file mode 100644 index 0000000..3872210 --- /dev/null +++ b/examples/auth-none.yml @@ -0,0 +1,11 @@ +--- +# Exemple 1/4 — Aucune authentification +# Le contrôle repose uniquement sur l'adresse source. C'est la seule méthode +# utilisable sur les ports interceptés, et la plus simple à déployer. +# Traçabilité : le journal contient l'IP, pas d'identité utilisateur. + +squid_auth_method: none + +squid_allowed_networks: + - 10.0.4.0/24 + - 10.0.5.0/24 diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml new file mode 100644 index 0000000..4dea629 --- /dev/null +++ b/group_vars/all/main.yml @@ -0,0 +1,7 @@ +--- +# Variables communes à tous les hôtes du dépôt. +ansible_user: ansible +ansible_python_interpreter: /usr/bin/python3 + +# Fuseau appliqué aux journaux et aux horodatages des rapports. +site_timezone: Europe/Paris diff --git a/group_vars/squid/main.yml b/group_vars/squid/main.yml new file mode 100644 index 0000000..d4bb8b0 --- /dev/null +++ b/group_vars/squid/main.yml @@ -0,0 +1,56 @@ +--- +# --------------------------------------------------------------------------- +# Paramétrage du groupe « squid » +# Ces valeurs surchargent roles/squid/defaults/main.yml. +# Les secrets (LDAP, keytab, clé de l'autorité) ne sont PAS ici : +# voir vault.yml.example et le groupe de variables Semaphore. +# --------------------------------------------------------------------------- + +# --- Mode : proxy explicite ET interception transparente ------------------- +squid_mode: both +squid_http_port: 3128 +squid_intercept_http_port: 3129 +squid_intercept_https_port: 3130 + +# --- ACL réseau ------------------------------------------------------------ +squid_allowed_networks: + - 10.0.4.0/24 + - 10.0.5.0/24 +squid_denied_sources: [] + +# --- Filtrage de domaines -------------------------------------------------- +# blocklist : tout est autorisé sauf la liste ci-dessous. +# Passer à « allowlist » pour l'approche inverse (squid_allowed_domains). +squid_filter_mode: blocklist +squid_denied_domains: + - .doubleclick.net + - .coinhive.com + - .torproject.org +squid_allowed_domains: [] + +# --- Authentification ------------------------------------------------------ +# none | basic | ldap | kerberos — un exemple complet par méthode dans +# examples/auth-*.yml +squid_auth_method: none + +# --- Déchiffrement TLS ----------------------------------------------------- +squid_ssl_bump: true +squid_ssl_ca_self_signed: true +squid_ssl_ca_subject: /C=FR/O=Tips-Of-Mine/CN=Squid Proxy CA +squid_no_bump_domains: + - .windowsupdate.com + - .apple.com + - .dropbox.com + - .impots.gouv.fr + - .ameli.fr + +# --- Journalisation -------------------------------------------------------- +squid_log_to_syslog: false +squid_syslog_facility: local2 + +# --- Cache : non demandé --------------------------------------------------- +squid_cache_enabled: false + +# Test : fiches en adresses IP, sans dépendance DNS +# squid_servers_dir: "{{ playbook_dir }}/tests/servers-ci" +# squid_groups_dir: "{{ playbook_dir }}/tests/groups-ci" diff --git a/group_vars/squid/vault.yml.example b/group_vars/squid/vault.yml.example new file mode 100644 index 0000000..109f5cb --- /dev/null +++ b/group_vars/squid/vault.yml.example @@ -0,0 +1,16 @@ +--- +# Copier en group_vars/squid/vault.yml puis chiffrer : +# ansible-vault encrypt group_vars/squid/vault.yml +# Sous Semaphore, préférer un groupe de variables (Variable Groups) plutôt +# qu'un fichier chiffré dans le dépôt. + +# Compte de service LDAP / Active Directory +squid_ldap_bind_password: "à-remplacer" + +# Keytab Kerberos du principal HTTP, encodé en base64 : +# base64 -w0 /chemin/HTTP.keytab +squid_kerberos_keytab_b64: "" + +# Autorité de certification du SSL Bump, si elle n'est pas auto-signée +squid_ssl_ca_cert_content: "" +squid_ssl_ca_key_content: "" diff --git a/groups/debian.yml b/groups/debian.yml new file mode 100644 index 0000000..3a60559 --- /dev/null +++ b/groups/debian.yml @@ -0,0 +1,23 @@ +--- +name: debian +description: Dépôts de paquets et écosystème Debian +members: [] +rules: + - domains: + - deb.debian.org + - security.debian.org + - security-cdn.debian.org + - ftp.debian.org + - packages.sury.org + - packages.adoptium.net + - packages.wazuh.com + ports: [http, https] + - domains: + - github.com + - api.github.com + - raw.githubusercontent.com + - codeload.github.com + - objects.githubusercontent.com + - pypi.org + - files.pythonhosted.org + ports: [https] diff --git a/groups/ubuntu.yml b/groups/ubuntu.yml new file mode 100644 index 0000000..70bc080 --- /dev/null +++ b/groups/ubuntu.yml @@ -0,0 +1,11 @@ +--- +name: ubuntu +description: Dépôts de paquets Ubuntu +members: [] +rules: + - domains: + - archive.ubuntu.com + - security.ubuntu.com + - ppa.launchpad.net + - changelogs.ubuntu.com + ports: [http, https] diff --git a/groups/windows.yml b/groups/windows.yml new file mode 100644 index 0000000..d5a5808 --- /dev/null +++ b/groups/windows.yml @@ -0,0 +1,31 @@ +--- +name: windows +description: Destinations communes à tous les serveurs Windows +# Seules ces machines beneficient du groupe. Une machine absente d'ici +# n'obtient rien : elle doit aussi posséder sa fiche dans servers/. +members: + - swadmappp00.tips-of-mine.local + - swadmappp01.tips-of-mine.local + - SWAZCEVEP01.tips-of-mine.local +rules: + - domains: + - download.windowsupdate.com + - sws.update.microsoft.com + - statsfe2.update.microsoft.com + - fe2.update.microsoft.com + - www.update.microsoft.com + - sls.update.microsoft.com + - ctldl.windowsupdate.com + - ds.download.windowsupdate.com + - download.microsoft.com + - go.microsoft.com + - activation.sls.microsoft.com + ports: [http, https] + - domains: + - crl.microsoft.com + - ocsp.digicert.com + - crl3.digicert.com + - crl.globalsign.com + - ocsp.globalsign.com + - apps.identrust.com + ports: [http, https] diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..cb8145c --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,9 @@ +--- +# Remplacé par generated/inventory.yml produit par le dépôt centreon-terraform. +all: + children: + squid: + hosts: + centreon: + ansible_host: 10.0.5.20 + ansible_user: root diff --git a/roles/squid/defaults/main.yml b/roles/squid/defaults/main.yml new file mode 100644 index 0000000..26292ad --- /dev/null +++ b/roles/squid/defaults/main.yml @@ -0,0 +1,263 @@ +--- +# --------------------------------------------------------------------------- +# Rôle squid — valeurs par défaut +# Toutes les variables sont surchargeables depuis group_vars/ ou depuis un +# groupe de variables Semaphore. +# --------------------------------------------------------------------------- + +# --- Paquet et service ----------------------------------------------------- +# Sur Debian, le SSL Bump exige le paquet squid-openssl (compilé avec +# --with-openssl). Le paquet squid standard ne sait pas faire de bump. +squid_package: "{{ 'squid-openssl' if squid_ssl_bump | bool else 'squid' }}" +squid_service: squid +squid_conf_dir: /etc/squid +squid_lists_dir: /etc/squid/lists +squid_log_dir: /var/log/squid +squid_visible_hostname: "{{ inventory_hostname }}" + +# --- Mode de mise en œuvre ------------------------------------------------- +# explicit : proxy déclaré côté client (port 3128) +# intercept : interception transparente (redirection depuis le firewall) +# both : les deux en parallèle +squid_mode: both +squid_http_port: 3128 +squid_intercept_http_port: 3129 +squid_intercept_https_port: 3130 + +# Autorise en bloc les réseaux de squid_allowed_networks. Bascule +# automatiquement à false dès qu'une fiche serveur ou groupe existe : dans ce +# modèle, seule une machine explicitement déclarée obtient un accès, et une +# machine inconnue tombe sur le « deny all » final. +squid_allow_localnet: "{{ (squid_servers | default([]) | length + squid_groups | default([]) | length) == 0 }}" + +# --- ACL réseau ------------------------------------------------------------ +# Réseaux autorisés à utiliser le proxy. +squid_allowed_networks: + - 10.0.4.0/24 +# Adresses/réseaux explicitement refusés (évalués avant les autorisations). +squid_denied_sources: [] +# Ports considérés comme sûrs (liste Safe_ports de Squid). +squid_safe_ports: [80, 21, 443, 70, 210, 1025-65535, 280, 488, 591, 777] +squid_ssl_ports: [443] + +# --- Filtrage de domaines -------------------------------------------------- +# blocklist : tout est autorisé sauf squid_denied_domains +# allowlist : tout est refusé sauf squid_allowed_domains +squid_filter_mode: blocklist +squid_denied_domains: [] +squid_allowed_domains: [] + +# --- Authentification ------------------------------------------------------ +# none | basic | ldap | kerberos +# Remarque : l'authentification est impossible sur un port en interception +# (le client croit parler au serveur d'origine, il n'enverra jamais +# d'en-tête Proxy-Authorization). Le rôle exempte donc automatiquement les +# ports interceptés. +squid_auth_method: none +squid_auth_realm: Proxy Tips-Of-Mine +squid_auth_children: 20 +squid_auth_ttl: 2 hours + +# Authentification basic (fichier NCSA local) +squid_basic_passwd_file: /etc/squid/passwd +# Liste de comptes : [{username: "bob", password_hash: "$apr1$..."}] +# Génération d'un condensat : openssl passwd -apr1 +squid_basic_users: [] + +# Authentification LDAP / Active Directory +squid_ldap_server: '' +squid_ldap_port: 389 +squid_ldap_base_dn: '' +squid_ldap_bind_dn: '' +squid_ldap_bind_password: '' +squid_ldap_bind_password_file: /etc/squid/ldap_bind.secret +squid_ldap_search_filter: (sAMAccountName=%s) +squid_ldap_version: 3 +squid_ldap_use_starttls: false +# Restriction par groupe (helper ext_ldap_group_acl) +squid_ldap_group_acl: false +squid_ldap_group_base_dn: '' +squid_ldap_group_filter: >- + (&(objectClass=group)(cn=%g)(member=%u)) +squid_ldap_allowed_groups: [] + +# Authentification Kerberos (SSO domaine, helper negotiate_kerberos_auth) +squid_kerberos_keytab: /etc/squid/HTTP.keytab +# Contenu du keytab encodé en base64 (à fournir par un secret, jamais en clair) +squid_kerberos_keytab_b64: '' +squid_kerberos_service_principal: "HTTP/{{ inventory_hostname }}" +squid_kerberos_realm: '' + +# --- SSL Bump -------------------------------------------------------------- +squid_ssl_bump: false +squid_ssl_dir: /etc/squid/ssl_cert +squid_ssl_ca_cert: /etc/squid/ssl_cert/squid-ca.crt +squid_ssl_ca_key: /etc/squid/ssl_cert/squid-ca.key +# Autorité fournie par un secret (recommandé). Si les deux sont vides et que +# squid_ssl_ca_self_signed vaut true, le rôle génère une AC locale. +squid_ssl_ca_cert_content: '' +squid_ssl_ca_key_content: '' +squid_ssl_ca_self_signed: true +squid_ssl_ca_subject: /C=FR/O=Tips-Of-Mine/CN=Squid Proxy CA +squid_ssl_ca_days: 3650 +squid_ssl_ca_key_size: 4096 +squid_ssl_db_dir: /var/spool/squid/ssl_db +squid_ssl_db_size: 4MB +squid_sslcrtd_children: 8 +# Domaines dont le trafic est relayé sans déchiffrement (splice) : +# banque, santé, applications à épinglage de certificat… +squid_no_bump_domains: + - .windowsupdate.com + - .apple.com + - .dropbox.com +squid_ssl_verify_upstream: true + +# --- Cache ----------------------------------------------------------------- +# Non demandé : le proxy sert au contrôle d'accès et à la journalisation. +squid_cache_enabled: false +squid_cache_mem: 256 MB +squid_cache_dir_size: 10240 +squid_maximum_object_size: 256 MB + +# --- Journalisation et divers --------------------------------------------- +squid_access_log: /var/log/squid/access.log +squid_cache_log: /var/log/squid/cache.log +squid_log_to_syslog: false +squid_syslog_facility: local2 +squid_forwarded_for: delete +squid_via: 'off' +squid_dns_nameservers: [] +squid_connect_timeout: 1 minute +squid_request_header_max_size: 64 KB +# Lignes de configuration libres ajoutées en fin de fichier +squid_extra_config: [] + +# --- Vérification post-déploiement ---------------------------------------- +squid_verify: true +squid_verify_url: http://www.debian.org/ + +# --- Fragments conf.d : catalogue de ports, groupes, serveurs -------------- +squid_confd_dir: /etc/squid/conf.d +# Charge les fiches YAML depuis le dépôt (répertoires servers/ et groups/) +squid_servers_dir: "{{ playbook_dir }}/servers" +squid_groups_dir: "{{ playbook_dir }}/groups" +# Supprime de conf.d tout fichier qui ne correspond plus à une fiche +squid_confd_purge: true + +# Préfixe numérique du fragment, par environnement. Il fixe l'ordre de +# chargement et regroupe visuellement les strates dans un « ls ». +# 00 = catalogue de ports, 10 = groupes. +squid_env_prefixes: + prod: "20" + preprod: "30" + rec: "40" + dev: "50" +# Préfixe appliqué à un environnement absent de la table ci-dessus +squid_env_prefix_default: "90" + +# Catalogue des ports, rendu dans 00-ports.conf. La clé est le nom employé +# dans les fiches ; l'ACL générée s'appelle _port. +squid_port_acls: + http: [80] + https: [443] + ftp: [21, 990, "12000-12100"] + sftp: [22] + imaps: [993] + "993": [993] + git: [9418] + "1022": [1022] + "2169": [2169] + "2202": [2202] + "2207": [2207] + "2222": [2222] + "4080": [4080] + "5000": [5000] + "5080": [5080] + "5443": [5443] + "8001": [8001] + "8080": [8080] + "8081": [8081] + "8441": [8441] + "8443": [8443] + "8530": [8530] + "8888": [8888] + "9085": [9085] + "18080": [18080] + "40000": [40000] + "50000": [50000] + "61616": [61616] + +# --- Protection des réseaux internes -------------------------------------- +# Interdit d'utiliser le proxy comme rebond vers les réseaux privés +# (ACL définie sur la DESTINATION, pas sur la source). +squid_deny_internal_destinations: true +squid_internal_destinations: + - 0.0.0.1-0.255.255.255 + - 10.0.0.0/8 + - 100.64.0.0/10 + - 169.254.0.0/16 + - 172.16.0.0/12 + - 192.168.0.0/16 + - "fc00::/7" + - "fe80::/10" + +# --- Divers repris du proxy de référence ----------------------------------- +# Obsolète depuis Squid 6 : laissé à false, ne l'activer que sur Squid 5 ou antérieur +squid_dns_v4_first: false +squid_always_direct_ftp: true +# !unsafe : la chaîne contient « {% raw %}{%{% endraw %} », que Jinja +# interpréterait comme une balise de gabarit. +squid_logformat_custom: !unsafe '%{%d/%m/%Y %H:%M:%S}tl %>a %Ss/%03>Hs:%Sh "%rm %ru HTTP/%rv" %mt %>Hs %h" "%{Referer}>h"' +squid_access_log_format: squid + +# --- Supervision SNMP (Centreon) ------------------------------------------- +squid_snmp_enabled: false +squid_snmp_port: 3401 +squid_snmp_community: '' +squid_snmp_allowed_from: localhost + +# --- Rapports d'utilisation (SARG) ----------------------------------------- +# Analyse de l'access.log et production de rapports HTML : utilisateurs, +# adresses IP, volumes, sites visités, horaires, refus. +squid_reports_enabled: false +squid_sarg_conf: /etc/sarg/sarg.conf +squid_sarg_output_dir: /var/www/sarg +squid_sarg_temp_dir: /var/tmp/sarg +squid_sarg_script: /usr/local/sbin/sarg-rapport.sh +squid_sarg_charset: UTF-8 +squid_sarg_title: Rapport d'utilisation du proxy +# Résolution inverse des adresses : coûteuse et souvent muette en interne +squid_sarg_resolve_ip: false +# Sans authentification, les enregistrements n'ont pas d'identifiant : +# « ip » les rattache à l'adresse source, « ignore » les écarte. +squid_sarg_records_without_userid: ip +squid_sarg_topsites_num: 100 +# « date_time » est volontairement absent : SARG 2.4.0 échoue à créer le +# répertoire du graphique par utilisateur (Cannot open .../graph.html). +squid_sarg_report_type: + - topusers + - topsites + - sites_users + - users_sites + - denied + - auth_failures +# Masque les URL détaillées tout en conservant les statistiques agrégées. +# À activer si les rapports ne doivent pas exposer la navigation individuelle. +squid_sarg_privacy: false +squid_sarg_privacy_string: URL masquée +squid_sarg_exclude_hosts: [] +squid_sarg_exclude_users: [] +# Périodes générées : daily, weekly, monthly +squid_sarg_periods: [daily, weekly, monthly] +squid_sarg_cron_hour: "1" +squid_sarg_cron_minute: "15" +# Conservation des rapports, en nombre de répertoires par période +squid_sarg_keep: 90 +# Génère un premier rapport à la fin du déploiement +squid_sarg_initial_report: true + +# Consultation des rapports par un nginx local, restreint aux réseaux +# d'administration. Aucune exposition publique. +squid_sarg_web_enabled: true +squid_sarg_web_port: 8081 +squid_sarg_web_allowed: "{{ squid_allowed_networks }}" diff --git a/roles/squid/handlers/main.yml b/roles/squid/handlers/main.yml new file mode 100644 index 0000000..fa3df44 --- /dev/null +++ b/roles/squid/handlers/main.yml @@ -0,0 +1,23 @@ +--- +- name: Recharger squid + ansible.builtin.systemd_service: + name: "{{ squid_service }}" + state: reloaded + +- name: Redémarrer squid + ansible.builtin.systemd_service: + name: "{{ squid_service }}" + state: restarted + +- name: Recharger systemd + ansible.builtin.systemd_service: + daemon_reload: true + +- name: Mettre à jour le magasin de certificats + ansible.builtin.command: update-ca-certificates + changed_when: true + +- name: Recharger nginx + ansible.builtin.systemd_service: + name: nginx + state: reloaded diff --git a/roles/squid/meta/main.yml b/roles/squid/meta/main.yml new file mode 100644 index 0000000..9e215b2 --- /dev/null +++ b/roles/squid/meta/main.yml @@ -0,0 +1,23 @@ +--- +galaxy_info: + role_name: squid + namespace: tips_of_mine + author: Hubert Cornet + description: >- + Installation et configuration d'un proxy web Squid sur Debian : ACL + réseau, filtrage de domaines, authentification (basic, LDAP/AD, + Kerberos), mode explicite et/ou interception, déchiffrement SSL Bump. + company: Tips-Of-Mine + license: MIT + min_ansible_version: "2.15" + platforms: + - name: Debian + versions: + - bookworm + - trixie + galaxy_tags: + - squid + - proxy + - debian + - security +dependencies: [] diff --git a/roles/squid/meta/runtime.yml b/roles/squid/meta/runtime.yml new file mode 100644 index 0000000..1e85b01 --- /dev/null +++ b/roles/squid/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.15.0" diff --git a/roles/squid/tasks/assert.yml b/roles/squid/tasks/assert.yml new file mode 100644 index 0000000..40a8e37 --- /dev/null +++ b/roles/squid/tasks/assert.yml @@ -0,0 +1,113 @@ +--- +- name: Vérifier que la cible est une Debian + ansible.builtin.assert: + that: + - ansible_facts['os_family'] == 'Debian' + fail_msg: >- + Ce rôle ne cible que Debian (famille détectée : + {{ ansible_facts['os_family'] | default('inconnue') }}). + quiet: true + +- name: Vérifier la cohérence des modes et des méthodes + ansible.builtin.assert: + that: + - squid_mode in squid_valid_modes + - squid_filter_mode in squid_valid_filter_modes + - squid_auth_method in squid_valid_auth_methods + fail_msg: >- + Valeur invalide : squid_mode={{ squid_mode }} (attendu + {{ squid_valid_modes | join(', ') }}), + squid_filter_mode={{ squid_filter_mode }} (attendu + {{ squid_valid_filter_modes | join(', ') }}), + squid_auth_method={{ squid_auth_method }} (attendu + {{ squid_valid_auth_methods | join(', ') }}). + quiet: true + +- name: Vérifier qu'au moins un réseau est autorisé + ansible.builtin.assert: + that: + - squid_allowed_networks | length > 0 + fail_msg: >- + squid_allowed_networks est vide : aucun client ne pourrait utiliser le + proxy. + quiet: true + +- name: Vérifier que la liste blanche n'est pas vide en mode allowlist + ansible.builtin.assert: + that: + - squid_allowed_domains | length > 0 + fail_msg: >- + squid_filter_mode vaut « allowlist » mais squid_allowed_domains est + vide : tout le trafic serait refusé. + quiet: true + when: squid_filter_mode == 'allowlist' + +- name: Vérifier les paramètres de l'authentification basic + ansible.builtin.assert: + that: + - squid_basic_users | length > 0 + - squid_basic_users | selectattr('password_hash', 'defined') | list | length == squid_basic_users | length + fail_msg: >- + L'authentification basic exige squid_basic_users, chaque entrée portant + les clés « username » et « password_hash » (openssl passwd -apr1). + quiet: true + when: squid_auth_method == 'basic' + +- name: Vérifier les paramètres de l'authentification LDAP + ansible.builtin.assert: + that: + - squid_ldap_server | length > 0 + - squid_ldap_base_dn | length > 0 + - squid_ldap_bind_dn | length > 0 + - squid_ldap_bind_password | length > 0 + fail_msg: >- + L'authentification LDAP exige squid_ldap_server, squid_ldap_base_dn, + squid_ldap_bind_dn et squid_ldap_bind_password. + quiet: true + when: squid_auth_method == 'ldap' + +- name: Vérifier les paramètres du contrôle de groupe LDAP + ansible.builtin.assert: + that: + - squid_ldap_group_base_dn | length > 0 + - squid_ldap_allowed_groups | length > 0 + fail_msg: >- + squid_ldap_group_acl est activé : squid_ldap_group_base_dn et + squid_ldap_allowed_groups sont obligatoires. + quiet: true + when: + - squid_auth_method == 'ldap' + - squid_ldap_group_acl | bool + +- name: Vérifier les paramètres de l'authentification Kerberos + ansible.builtin.assert: + that: + - squid_kerberos_keytab_b64 | length > 0 + - squid_kerberos_realm | length > 0 + fail_msg: >- + L'authentification Kerberos exige squid_kerberos_keytab_b64 (keytab + encodé en base64, fourni par un secret) et squid_kerberos_realm. + quiet: true + when: squid_auth_method == 'kerberos' + +- name: Vérifier la disponibilité d'une autorité pour le SSL Bump + ansible.builtin.assert: + that: + - (squid_ssl_ca_cert_content | length > 0 and squid_ssl_ca_key_content | length > 0) + or (squid_ssl_ca_self_signed | bool) + fail_msg: >- + Le SSL Bump exige soit squid_ssl_ca_cert_content et + squid_ssl_ca_key_content, soit squid_ssl_ca_self_signed à true. + quiet: true + when: squid_ssl_bump | bool + +- name: Avertir que l'interception seule ne permet aucune authentification + ansible.builtin.debug: + msg: >- + Mode « intercept » et authentification « {{ squid_auth_method }} » : + les ports interceptés sont exemptés d'authentification (le client + n'envoie pas d'en-tête Proxy-Authorization sur une connexion + interceptée). Le contrôle y reste basé sur l'adresse source. + when: + - squid_auth_enabled | bool + - squid_intercept_enabled | bool diff --git a/roles/squid/tasks/auth.yml b/roles/squid/tasks/auth.yml new file mode 100644 index 0000000..59d51d1 --- /dev/null +++ b/roles/squid/tasks/auth.yml @@ -0,0 +1,70 @@ +--- +# --- basic : fichier NCSA local -------------------------------------------- +- name: Déployer le fichier de comptes basic + ansible.builtin.template: + src: auth/passwd.j2 + dest: "{{ squid_basic_passwd_file }}" + owner: root + group: proxy + mode: "0640" + no_log: true + when: squid_auth_method == 'basic' + notify: Recharger squid + +# --- ldap : compte de service --------------------------------------------- +- name: Déposer le mot de passe du compte de service LDAP + ansible.builtin.copy: + content: "{{ squid_ldap_bind_password }}\n" + dest: "{{ squid_ldap_bind_password_file }}" + owner: root + group: proxy + mode: "0640" + no_log: true + when: squid_auth_method == 'ldap' + notify: Recharger squid + +# --- kerberos : keytab et environnement du service ------------------------- +- name: Déposer le keytab Kerberos du service HTTP + ansible.builtin.copy: + content: "{{ squid_kerberos_keytab_b64 | b64decode }}" + dest: "{{ squid_kerberos_keytab }}" + owner: proxy + group: proxy + mode: "0600" + no_log: true + when: squid_auth_method == 'kerberos' + notify: Redémarrer squid + +- name: Créer le répertoire de surcharge systemd du service Squid + ansible.builtin.file: + path: /etc/systemd/system/squid.service.d + state: directory + owner: root + group: root + mode: "0755" + when: squid_auth_method == 'kerberos' + +- name: Déclarer le keytab dans l'environnement du service Squid + ansible.builtin.template: + src: auth/squid-kerberos.conf.j2 + dest: /etc/systemd/system/squid.service.d/kerberos.conf + owner: root + group: root + mode: "0644" + when: squid_auth_method == 'kerberos' + notify: + - Recharger systemd + - Redémarrer squid + +# --- nettoyage des méthodes non retenues ----------------------------------- +- name: Retirer les fichiers d'authentification devenus inutiles + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: + - {path: "{{ squid_basic_passwd_file }}", keep: "{{ squid_auth_method == 'basic' }}"} + - {path: "{{ squid_ldap_bind_password_file }}", keep: "{{ squid_auth_method == 'ldap' }}"} + - {path: "{{ squid_kerberos_keytab }}", keep: "{{ squid_auth_method == 'kerberos' }}"} + loop_control: + label: "{{ item.path }}" + when: not (item.keep | bool) diff --git a/roles/squid/tasks/confd.yml b/roles/squid/tasks/confd.yml new file mode 100644 index 0000000..3a100f1 --- /dev/null +++ b/roles/squid/tasks/confd.yml @@ -0,0 +1,66 @@ +--- +- name: Créer le répertoire des fragments + ansible.builtin.file: + path: "{{ squid_confd_dir }}" + state: directory + owner: root + group: proxy + mode: "0750" + +- name: Déployer le catalogue de ports + ansible.builtin.template: + src: confd/ports.conf.j2 + dest: "{{ squid_confd_dir }}/00-ports.conf" + owner: root + group: proxy + mode: "0640" + notify: Recharger squid + +- name: Déployer les fragments de groupes + ansible.builtin.template: + src: confd/group.conf.j2 + dest: "{{ squid_confd_dir }}/10-groupe-{{ item.name }}.conf" + owner: root + group: proxy + mode: "0640" + loop: "{{ squid_groups }}" + loop_control: + label: "{{ item.name }}" + vars: + profile: "{{ item }}" + notify: Recharger squid + +- name: Déployer les fragments de serveurs + ansible.builtin.template: + src: confd/server.conf.j2 + dest: >- + {{ squid_confd_dir }}/{{ squid_env_prefixes.get(item.env | default('prod'), + squid_env_prefix_default) }}-{{ item.name }}.conf + owner: root + group: proxy + mode: "0640" + loop: "{{ squid_servers }}" + loop_control: + label: "{{ item.name }}" + vars: + profile: "{{ item }}" + notify: Recharger squid + +- name: Inventorier les fragments présents sur la cible + ansible.builtin.find: + paths: "{{ squid_confd_dir }}" + patterns: "*.conf" + register: squid_confd_presents + when: squid_confd_purge | bool + +- name: Retirer les fragments sans fiche correspondante + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: >- + {{ squid_confd_presents.files | default([]) | map(attribute='path') | list + | difference(squid_confd_attendus) }} + loop_control: + label: "{{ item | basename }}" + when: squid_confd_purge | bool + notify: Recharger squid diff --git a/roles/squid/tasks/configure.yml b/roles/squid/tasks/configure.yml new file mode 100644 index 0000000..33551ae --- /dev/null +++ b/roles/squid/tasks/configure.yml @@ -0,0 +1,54 @@ +--- +# Les listes sont déployées avant squid.conf : la validation +# « squid -k parse » du fichier de configuration référence ces fichiers. +- name: Déployer la liste des domaines interdits + ansible.builtin.template: + src: lists/domains.txt.j2 + dest: "{{ squid_lists_dir }}/denied_domains.txt" + owner: root + group: proxy + mode: "0640" + vars: + domains_list: "{{ squid_denied_domains }}" + domains_title: Domaines interdits + when: squid_denied_domains | length > 0 + notify: Recharger squid + +- name: Déployer la liste des domaines autorisés + ansible.builtin.template: + src: lists/domains.txt.j2 + dest: "{{ squid_lists_dir }}/allowed_domains.txt" + owner: root + group: proxy + mode: "0640" + vars: + domains_list: "{{ squid_allowed_domains }}" + domains_title: Domaines autorisés + when: squid_allowed_domains | length > 0 + notify: Recharger squid + +- name: Déployer la liste des domaines non déchiffrés + ansible.builtin.template: + src: lists/domains.txt.j2 + dest: "{{ squid_lists_dir }}/no_bump_domains.txt" + owner: root + group: proxy + mode: "0640" + vars: + domains_list: "{{ squid_no_bump_domains }}" + domains_title: Domaines relayés sans déchiffrement + when: + - squid_ssl_bump | bool + - squid_no_bump_domains | length > 0 + notify: Recharger squid + +- name: Déployer la configuration principale de Squid + ansible.builtin.template: + src: squid.conf.j2 + dest: "{{ squid_conf_dir }}/squid.conf" + owner: root + group: proxy + mode: "0640" + backup: true + validate: /usr/sbin/squid -k parse -f %s + notify: Recharger squid diff --git a/roles/squid/tasks/install.yml b/roles/squid/tasks/install.yml new file mode 100644 index 0000000..da502d9 --- /dev/null +++ b/roles/squid/tasks/install.yml @@ -0,0 +1,38 @@ +--- +- name: Installer le paquet Squid + ansible.builtin.apt: + name: "{{ squid_package }}" + state: present + update_cache: true + cache_valid_time: 3600 + notify: Redémarrer squid + +- name: Installer les paquets nécessaires au SSL Bump + ansible.builtin.apt: + name: "{{ squid_extra_packages_ssl }}" + state: present + when: squid_ssl_bump | bool + +- name: Installer les paquets nécessaires à Kerberos + ansible.builtin.apt: + name: "{{ squid_extra_packages_kerberos }}" + state: present + when: squid_auth_method == 'kerberos' + +- name: Créer le répertoire des listes de filtrage + ansible.builtin.file: + path: "{{ squid_lists_dir }}" + state: directory + owner: root + group: proxy + mode: "0750" + +- name: Conserver une copie de la configuration d'origine + ansible.builtin.copy: + src: "{{ squid_conf_dir }}/squid.conf" + dest: "{{ squid_conf_dir }}/squid.conf.dist" + remote_src: true + owner: root + group: root + mode: "0644" + force: false diff --git a/roles/squid/tasks/main.yml b/roles/squid/tasks/main.yml new file mode 100644 index 0000000..aa68511 --- /dev/null +++ b/roles/squid/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Valider les paramètres du rôle + ansible.builtin.import_tasks: assert.yml + tags: [squid, always] + +- name: Installer Squid + ansible.builtin.import_tasks: install.yml + tags: [squid, install] + +- name: Préparer l'autorité de certification du SSL Bump + ansible.builtin.import_tasks: ssl_bump.yml + when: squid_ssl_bump | bool + tags: [squid, ssl] + +- name: Configurer l'authentification + ansible.builtin.import_tasks: auth.yml + tags: [squid, auth] + +- name: Charger et valider les fiches serveurs et groupes + ansible.builtin.import_tasks: profiles.yml + tags: [squid, config, profils] + +- name: Déployer la configuration + ansible.builtin.import_tasks: configure.yml + tags: [squid, config] + +- name: Déployer les fragments conf.d + ansible.builtin.import_tasks: confd.yml + tags: [squid, config, profils] + +- name: Piloter le service + ansible.builtin.import_tasks: service.yml + tags: [squid, service] + +- name: Vérifier le fonctionnement du proxy + ansible.builtin.import_tasks: verify.yml + when: squid_verify | bool + tags: [squid, verify] + +- name: Mettre en place les rapports d'utilisation + ansible.builtin.import_tasks: reports.yml + when: squid_reports_enabled | bool + tags: [squid, reports] diff --git a/roles/squid/tasks/profiles.yml b/roles/squid/tasks/profiles.yml new file mode 100644 index 0000000..09a2ca1 --- /dev/null +++ b/roles/squid/tasks/profiles.yml @@ -0,0 +1,94 @@ +--- +- name: Rechercher les fiches de groupes + ansible.builtin.set_fact: + squid_group_files: "{{ query('ansible.builtin.fileglob', squid_groups_dir ~ '/*.yml') | sort }}" + +- name: Rechercher les fiches de serveurs + ansible.builtin.set_fact: + squid_server_files: "{{ query('ansible.builtin.fileglob', squid_servers_dir ~ '/*.yml') | sort }}" + +- name: Lire les fiches de groupes + ansible.builtin.set_fact: + squid_groups: "{{ squid_groups | default([]) + [lookup('ansible.builtin.file', item) | from_yaml] }}" + loop: "{{ squid_group_files }}" + loop_control: + label: "{{ item | basename }}" + +- name: Lire les fiches de serveurs + ansible.builtin.set_fact: + squid_servers: "{{ squid_servers | default([]) + [lookup('ansible.builtin.file', item) | from_yaml] }}" + loop: "{{ squid_server_files }}" + loop_control: + label: "{{ item | basename }}" + +- name: Initialiser les listes vides + ansible.builtin.set_fact: + squid_groups: "{{ squid_groups | default([]) }}" + squid_servers: "{{ squid_servers | default([]) }}" + +- name: Vérifier que chaque fiche porte les clés attendues + ansible.builtin.assert: + that: + - item.name is defined + - item.rules is defined or item.full_access | default(false) | bool + fail_msg: >- + Fiche incomplète : « name » et « rules » sont obligatoires + ({{ item | to_json | truncate(120) }}). + quiet: true + loop: "{{ squid_groups + squid_servers }}" + loop_control: + label: "{{ item.name | default('sans nom') }}" + +- name: Vérifier que tous les ports référencés existent au catalogue + ansible.builtin.assert: + that: + - ports_inconnus | length == 0 + fail_msg: >- + Ports absents de squid_port_acls : {{ ports_inconnus | join(', ') }}. + Ajouter l'entrée au catalogue ou corriger la fiche. + quiet: true + vars: + ports_inconnus: >- + {{ (squid_groups + squid_servers) + | selectattr('rules') | map(attribute='rules') | flatten + | map(attribute='ports') | flatten + | map('string') | unique + | difference(squid_port_acls.keys() | map('string') | list) }} + +- name: Vérifier que tout membre de groupe possède une fiche serveur + ansible.builtin.assert: + that: + - membres_orphelins | length == 0 + fail_msg: >- + Ces machines sont listées dans un groupe sans fiche serveur + correspondante : {{ membres_orphelins | join(', ') }}. Un serveur non + déclaré ne doit obtenir aucun accès. + quiet: true + vars: + membres_orphelins: >- + {{ squid_groups | selectattr('members', 'defined') | map(attribute='members') + | flatten | map('lower') | unique + | difference(squid_servers | map(attribute='sources') | flatten | map('lower') | list) }} + +- name: Établir la liste des fragments de groupes attendus + ansible.builtin.set_fact: + squid_confd_groupes: >- + {{ squid_groups | map(attribute='name') + | map('regex_replace', '^(.*)$', squid_confd_dir ~ '/10-groupe-\1.conf') | list }} + +- name: Établir la liste des fragments de serveurs attendus + ansible.builtin.set_fact: + squid_confd_serveurs: "{{ squid_confd_serveurs | default([]) + [chemin] }}" + loop: "{{ squid_servers }}" + loop_control: + label: "{{ item.name }}" + vars: + chemin: >- + {{ squid_confd_dir }}/{{ squid_env_prefixes.get(item.env | default('prod'), + squid_env_prefix_default) }}-{{ item.name }}.conf + +- name: Réunir la liste des fragments attendus + ansible.builtin.set_fact: + squid_confd_attendus: >- + {{ [squid_confd_dir ~ '/00-ports.conf'] + squid_confd_groupes + + (squid_confd_serveurs | default([])) }} diff --git a/roles/squid/tasks/reports.yml b/roles/squid/tasks/reports.yml new file mode 100644 index 0000000..9959aef --- /dev/null +++ b/roles/squid/tasks/reports.yml @@ -0,0 +1,139 @@ +--- +- name: Installer SARG + ansible.builtin.apt: + name: sarg + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Installer nginx pour la consultation des rapports + ansible.builtin.apt: + name: nginx + state: present + when: squid_sarg_web_enabled | bool + +- name: Créer les répertoires de travail de SARG + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0750" + loop: + - "{{ squid_sarg_output_dir }}" + - "{{ squid_sarg_temp_dir }}" + - "{{ squid_sarg_output_dir }}/quotidien" + - "{{ squid_sarg_output_dir }}/hebdomadaire" + - "{{ squid_sarg_output_dir }}/mensuel" + +- name: Déployer la liste des hôtes exclus des rapports + ansible.builtin.template: + src: sarg-exclusions.j2 + dest: /etc/sarg/exclude_hosts + owner: root + group: root + mode: "0644" + vars: + exclusions_liste: "{{ squid_sarg_exclude_hosts }}" + exclusions_titre: Hôtes exclus des rapports + when: squid_sarg_exclude_hosts | length > 0 + +- name: Déployer la liste des utilisateurs exclus des rapports + ansible.builtin.template: + src: sarg-exclusions.j2 + dest: /etc/sarg/exclude_users + owner: root + group: root + mode: "0644" + vars: + exclusions_liste: "{{ squid_sarg_exclude_users }}" + exclusions_titre: Utilisateurs exclus des rapports + when: squid_sarg_exclude_users | length > 0 + +- name: Déployer la configuration de SARG + ansible.builtin.template: + src: sarg.conf.j2 + dest: "{{ squid_sarg_conf }}" + owner: root + group: root + mode: "0644" + backup: true + +- name: Déployer le script de génération des rapports + ansible.builtin.template: + src: sarg-rapport.sh.j2 + dest: "{{ squid_sarg_script }}" + owner: root + group: root + mode: "0750" + +- name: Planifier la génération des rapports + ansible.builtin.cron: + name: "Rapport SARG {{ item.nom }}" + cron_file: sarg-rapports + user: root + minute: "{{ squid_sarg_cron_minute }}" + hour: "{{ squid_sarg_cron_hour }}" + day: "{{ item.day }}" + weekday: "{{ item.weekday }}" + job: "{{ squid_sarg_script }} {{ item.nom }}" + state: "{{ 'present' if item.nom in squid_sarg_periods else 'absent' }}" + loop: + - {nom: daily, day: "*", weekday: "*"} + - {nom: weekly, day: "*", weekday: "1"} + - {nom: monthly, day: "1", weekday: "*"} + loop_control: + label: "{{ item.nom }}" + +- name: Déployer le site nginx de consultation + ansible.builtin.template: + src: sarg-nginx.conf.j2 + dest: /etc/nginx/sites-available/sarg.conf + owner: root + group: root + mode: "0644" + when: squid_sarg_web_enabled | bool + notify: Recharger nginx + +- name: Activer le site nginx de consultation + ansible.builtin.file: + src: /etc/nginx/sites-available/sarg.conf + dest: /etc/nginx/sites-enabled/sarg.conf + state: link + owner: root + group: root + when: squid_sarg_web_enabled | bool + notify: Recharger nginx + +- name: Autoriser nginx à lire les rapports + ansible.builtin.file: + path: "{{ squid_sarg_output_dir }}" + state: directory + owner: root + group: "{{ 'www-data' if squid_sarg_web_enabled | bool else 'root' }}" + mode: "0750" + +- name: Contrôler la configuration nginx + ansible.builtin.command: nginx -t + register: squid_nginx_test + changed_when: false + failed_when: squid_nginx_test.rc != 0 + when: squid_sarg_web_enabled | bool + +- name: Appliquer les changements en attente + ansible.builtin.meta: flush_handlers + +- name: Générer un premier rapport + ansible.builtin.command: "{{ squid_sarg_script }} daily" + register: squid_sarg_first + changed_when: squid_sarg_first.rc == 0 + failed_when: false + when: squid_sarg_initial_report | bool + +- name: Restituer l'accès aux rapports + ansible.builtin.debug: + msg: >- + Rapports SARG disponibles sur + http://{{ ansible_default_ipv4.address | default(inventory_hostname) }}:{{ squid_sarg_web_port }}/ + — accès restreint à {{ squid_sarg_web_allowed | join(', ') }} + when: squid_sarg_web_enabled | bool diff --git a/roles/squid/tasks/service.yml b/roles/squid/tasks/service.yml new file mode 100644 index 0000000..4a5fe30 --- /dev/null +++ b/roles/squid/tasks/service.yml @@ -0,0 +1,20 @@ +--- +- name: Initialiser les répertoires de cache + ansible.builtin.command: + argv: + - /usr/sbin/squid + - -z + - --foreground + creates: /var/spool/squid/00 + when: squid_cache_enabled | bool + notify: Redémarrer squid + +- name: Activer et démarrer Squid + ansible.builtin.systemd_service: + name: "{{ squid_service }}" + enabled: true + state: started + daemon_reload: true + +- name: Appliquer les changements en attente avant la vérification + ansible.builtin.meta: flush_handlers diff --git a/roles/squid/tasks/ssl_bump.yml b/roles/squid/tasks/ssl_bump.yml new file mode 100644 index 0000000..a4926ba --- /dev/null +++ b/roles/squid/tasks/ssl_bump.yml @@ -0,0 +1,107 @@ +--- +- name: Créer le répertoire de l'autorité de certification + ansible.builtin.file: + path: "{{ squid_ssl_dir }}" + state: directory + owner: root + group: proxy + mode: "0750" + +- name: Déposer le certificat de l'autorité fournie + ansible.builtin.copy: + content: "{{ squid_ssl_ca_cert_content }}" + dest: "{{ squid_ssl_ca_cert }}" + owner: root + group: proxy + mode: "0644" + when: squid_ssl_ca_cert_content | length > 0 + notify: Redémarrer squid + +- name: Déposer la clé privée de l'autorité fournie + ansible.builtin.copy: + content: "{{ squid_ssl_ca_key_content }}" + dest: "{{ squid_ssl_ca_key }}" + owner: root + group: proxy + mode: "0640" + no_log: true + when: squid_ssl_ca_key_content | length > 0 + notify: Redémarrer squid + +- name: Générer une autorité auto-signée locale + ansible.builtin.command: + argv: + - openssl + - req + - -new + - -newkey + - "rsa:{{ squid_ssl_ca_key_size }}" + - -sha256 + - -days + - "{{ squid_ssl_ca_days }}" + - -nodes + - -x509 + - -extensions + - v3_ca + - -subj + - "{{ squid_ssl_ca_subject }}" + - -keyout + - "{{ squid_ssl_ca_key }}" + - -out + - "{{ squid_ssl_ca_cert }}" + creates: "{{ squid_ssl_ca_cert }}" + when: + - squid_ssl_ca_self_signed | bool + - squid_ssl_ca_cert_content | length == 0 + notify: Redémarrer squid + +- name: Appliquer les droits sur les fichiers de l'autorité + ansible.builtin.file: + path: "{{ item.path }}" + owner: root + group: proxy + mode: "{{ item.mode }}" + loop: + - {path: "{{ squid_ssl_ca_cert }}", mode: "0644"} + - {path: "{{ squid_ssl_ca_key }}", mode: "0640"} + loop_control: + label: "{{ item.path }}" + +- name: Créer le répertoire parent de la base de certificats générés + ansible.builtin.file: + path: "{{ squid_ssl_db_dir | dirname }}" + state: directory + owner: proxy + group: proxy + mode: "0750" + +- name: Initialiser la base de certificats générés + ansible.builtin.command: + argv: + - "{{ squid_helper_certgen }}" + - -c + - -s + - "{{ squid_ssl_db_dir }}" + - -M + - "{{ squid_ssl_db_size }}" + creates: "{{ squid_ssl_db_dir }}/index.txt" + notify: Redémarrer squid + +- name: Attribuer la base de certificats générés à l'utilisateur proxy + ansible.builtin.file: + path: "{{ squid_ssl_db_dir }}" + state: directory + owner: proxy + group: proxy + mode: "0750" + recurse: true + +- name: Déposer le certificat de l'autorité pour distribution aux postes clients + ansible.builtin.copy: + src: "{{ squid_ssl_ca_cert }}" + dest: /usr/local/share/ca-certificates/squid-proxy-ca.crt + remote_src: true + owner: root + group: root + mode: "0644" + notify: Mettre à jour le magasin de certificats diff --git a/roles/squid/tasks/verify.yml b/roles/squid/tasks/verify.yml new file mode 100644 index 0000000..86c9ed4 --- /dev/null +++ b/roles/squid/tasks/verify.yml @@ -0,0 +1,55 @@ +--- +- name: Attendre l'ouverture du port explicite + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ squid_http_port }}" + timeout: 30 + when: squid_explicit_enabled | bool + +- name: Attendre l'ouverture des ports interceptés + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ item }}" + timeout: 30 + loop: >- + {{ [squid_intercept_http_port] + + ([squid_intercept_https_port] if squid_ssl_bump | bool else []) }} + when: squid_intercept_enabled | bool + +- name: Relever la version de Squid en service + ansible.builtin.command: /usr/sbin/squid -v + register: squid_version_out + changed_when: false + +- name: Vérifier la syntaxe de la configuration en place + ansible.builtin.command: /usr/sbin/squid -k parse + register: squid_parse_out + changed_when: false + failed_when: squid_parse_out.rc != 0 + +- name: Effectuer une requête de bout en bout à travers le proxy + ansible.builtin.uri: + url: "{{ squid_verify_url }}" + method: HEAD + status_code: [200, 301, 302] + use_proxy: true + validate_certs: false + environment: + http_proxy: "http://127.0.0.1:{{ squid_http_port }}" + https_proxy: "http://127.0.0.1:{{ squid_http_port }}" + register: squid_probe + failed_when: false + changed_when: false + when: + - squid_explicit_enabled | bool + - not squid_auth_enabled | bool + +- name: Restituer le résultat des vérifications + ansible.builtin.debug: + msg: + - "Version : {{ squid_version_out.stdout_lines[0] | default('inconnue') }}" + - "Configuration : syntaxe valide" + - >- + Requête de test : + {{ ('HTTP ' ~ squid_probe.status) if squid_probe.status is defined + else 'non effectuée (authentification active ou mode interception seul)' }} diff --git a/roles/squid/templates/auth/basic.conf.j2 b/roles/squid/templates/auth/basic.conf.j2 new file mode 100644 index 0000000..1a96405 --- /dev/null +++ b/roles/squid/templates/auth/basic.conf.j2 @@ -0,0 +1,7 @@ +# Authentification basic sur fichier NCSA local. +# Génération d'un condensat : openssl passwd -apr1 +auth_param basic program {{ squid_helper_basic_ncsa }} {{ squid_basic_passwd_file }} +auth_param basic children {{ squid_auth_children }} startup=2 idle=1 +auth_param basic realm {{ squid_auth_realm }} +auth_param basic credentialsttl {{ squid_auth_ttl }} +auth_param basic casesensitive off diff --git a/roles/squid/templates/auth/kerberos.conf.j2 b/roles/squid/templates/auth/kerberos.conf.j2 new file mode 100644 index 0000000..a3c7e25 --- /dev/null +++ b/roles/squid/templates/auth/kerberos.conf.j2 @@ -0,0 +1,6 @@ +# Authentification Negotiate/Kerberos (SSO domaine). +# Le poste client doit joindre le proxy par son nom pleinement qualifié, +# celui du principal de service, et non par son adresse IP. +auth_param negotiate program {{ squid_helper_negotiate_kerberos }} -k {{ squid_kerberos_keytab }} -s {{ squid_kerberos_service_principal }}@{{ squid_kerberos_realm }} +auth_param negotiate children {{ squid_auth_children }} startup=2 idle=1 +auth_param negotiate keep_alive on diff --git a/roles/squid/templates/auth/ldap.conf.j2 b/roles/squid/templates/auth/ldap.conf.j2 new file mode 100644 index 0000000..37ec7f8 --- /dev/null +++ b/roles/squid/templates/auth/ldap.conf.j2 @@ -0,0 +1,14 @@ +# Authentification basic adossée à un annuaire LDAP / Active Directory. +# Le mot de passe du compte de service est lu dans un fichier (-W), jamais +# passé en argument : il serait visible dans la table des processus. +auth_param basic program {{ squid_helper_basic_ldap }} -b "{{ squid_ldap_base_dn }}" -D "{{ squid_ldap_bind_dn }}" -W {{ squid_ldap_bind_password_file }} -f "{{ squid_ldap_search_filter }}" -v {{ squid_ldap_version }}{{ ' -Z' if squid_ldap_use_starttls | bool else '' }} -h {{ squid_ldap_server }} -p {{ squid_ldap_port }} +auth_param basic children {{ squid_auth_children }} startup=2 idle=1 +auth_param basic realm {{ squid_auth_realm }} +auth_param basic credentialsttl {{ squid_auth_ttl }} +auth_param basic casesensitive off +{% if squid_ldap_group_acl | bool %} + +# Restriction par appartenance à un groupe de l'annuaire +external_acl_type ldap_group ttl=300 negative_ttl=60 children-max=10 %LOGIN {{ squid_helper_group_ldap }} -b "{{ squid_ldap_group_base_dn }}" -D "{{ squid_ldap_bind_dn }}" -W {{ squid_ldap_bind_password_file }} -f "{{ squid_ldap_group_filter | trim }}" -v {{ squid_ldap_version }}{{ ' -Z' if squid_ldap_use_starttls | bool else '' }} -S -h {{ squid_ldap_server }} -p {{ squid_ldap_port }} +acl allowed_groups external ldap_group {{ squid_ldap_allowed_groups | map('regex_replace', '^(.*)$', '"\\1"') | join(' ') }} +{% endif %} diff --git a/roles/squid/templates/auth/none.conf.j2 b/roles/squid/templates/auth/none.conf.j2 new file mode 100644 index 0000000..8e6d6d5 --- /dev/null +++ b/roles/squid/templates/auth/none.conf.j2 @@ -0,0 +1,2 @@ +# Aucune authentification : le contrôle repose uniquement sur l'adresse +# source (squid_allowed_networks). Ce fichier n'est jamais inclus. diff --git a/roles/squid/templates/auth/passwd.j2 b/roles/squid/templates/auth/passwd.j2 new file mode 100644 index 0000000..8396c2f --- /dev/null +++ b/roles/squid/templates/auth/passwd.j2 @@ -0,0 +1,3 @@ +{% for user in squid_basic_users %} +{{ user.username }}:{{ user.password_hash }} +{% endfor %} diff --git a/roles/squid/templates/auth/squid-kerberos.conf.j2 b/roles/squid/templates/auth/squid-kerberos.conf.j2 new file mode 100644 index 0000000..4317768 --- /dev/null +++ b/roles/squid/templates/auth/squid-kerberos.conf.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +[Service] +Environment=KRB5_KTNAME={{ squid_kerberos_keytab }} +Environment=KRB5RCACHETYPE=none diff --git a/roles/squid/templates/confd/group.conf.j2 b/roles/squid/templates/confd/group.conf.j2 new file mode 100644 index 0000000..3fb6dc1 --- /dev/null +++ b/roles/squid/templates/confd/group.conf.j2 @@ -0,0 +1,32 @@ +# {{ ansible_managed }} +# Groupe « {{ profile.name }} »{% if profile.description is defined %} — {{ profile.description }}{% endif %} + +# Destinations communes à {{ profile.members | default([]) | length }} machine(s) +# nommément déclarées. Aucune machine absente de cette liste n'obtient +# d'accès par ce fichier. + +{% if profile.members | default([]) | length > 0 %} +acl groupe_{{ profile.name }} src {{ profile.members | join(' ') }} +{% endif %} + +{% set vus = [] %} +{% for regle in profile.rules | default([], true) %} +{% for domaine in regle.domains %} +{% set brut = domaine | regex_replace('[^A-Za-z0-9]', '_') %}{% set nom = brut if brut | length <= 62 else brut[:53] ~ '_' ~ (domaine | hash('md5'))[:8] %} +{% if nom not in vus %} +{% if vus.append(nom) %}{% endif %} +acl {{ nom }} dstdomain {{ domaine }} +{% endif %} +{% endfor %} +{% endfor %} + +{% if profile.members | default([]) | length > 0 %} +{% for regle in profile.rules | default([], true) %} +{% for domaine in regle.domains %} +{% set brut = domaine | regex_replace('[^A-Za-z0-9]', '_') %}{% set nom = brut if brut | length <= 62 else brut[:53] ~ '_' ~ (domaine | hash('md5'))[:8] %} +{% for port in regle.ports %} +http_access allow groupe_{{ profile.name }} {{ nom }} {{ port }}_port +{% endfor %} +{% endfor %} +{% endfor %} +{% endif %} diff --git a/roles/squid/templates/confd/ports.conf.j2 b/roles/squid/templates/confd/ports.conf.j2 new file mode 100644 index 0000000..ae5d43d --- /dev/null +++ b/roles/squid/templates/confd/ports.conf.j2 @@ -0,0 +1,8 @@ +# {{ ansible_managed }} +# Catalogue des ports autorisables, partagé par tous les fragments. +# Une entrée de squid_port_acls = une ACL « _port ». +# Ce fichier ne donne aucun accès par lui-même : il ne fait que nommer. + +{% for cle, ports in squid_port_acls | dictsort %} +acl {{ cle }}_port port {{ ports | join(' ') }} +{% endfor %} diff --git a/roles/squid/templates/confd/server.conf.j2 b/roles/squid/templates/confd/server.conf.j2 new file mode 100644 index 0000000..7cf3214 --- /dev/null +++ b/roles/squid/templates/confd/server.conf.j2 @@ -0,0 +1,36 @@ +# {{ ansible_managed }} +# Serveur « {{ profile.name }} »{% if profile.description is defined %} — {{ profile.description }}{% endif %} + +{% if profile.groups | default([]) | length > 0 %} +# Hérite en plus des groupes : {{ profile.groups | join(', ') }} +{% endif %} + +{% set suffixe = profile.env | default('prod') %} +{% for source in profile.sources %} +acl {{ profile.name }}_{{ suffixe }} src {{ source }} +{% endfor %} + +{% if profile.full_access | default(false) %} +# ATTENTION : accès sans restriction de destination ni de port. +http_access allow {{ profile.name }}_{{ suffixe }} +{% endif %} + +{% set vus = [] %} +{% for regle in profile.rules | default([], true) %} +{% for domaine in regle.domains %} +{% set brut = domaine | regex_replace('[^A-Za-z0-9]', '_') %}{% set nom = brut if brut | length <= 62 else brut[:53] ~ '_' ~ (domaine | hash('md5'))[:8] %} +{% if nom not in vus %} +{% if vus.append(nom) %}{% endif %} +acl {{ nom }} dstdomain {{ domaine }} +{% endif %} +{% endfor %} +{% endfor %} + +{% for regle in profile.rules | default([], true) %} +{% for domaine in regle.domains %} +{% set brut = domaine | regex_replace('[^A-Za-z0-9]', '_') %}{% set nom = brut if brut | length <= 62 else brut[:53] ~ '_' ~ (domaine | hash('md5'))[:8] %} +{% for port in regle.ports %} +http_access allow {{ profile.name }}_{{ suffixe }} {{ nom }} {{ port }}_port +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/roles/squid/templates/lists/domains.txt.j2 b/roles/squid/templates/lists/domains.txt.j2 new file mode 100644 index 0000000..c5fec3d --- /dev/null +++ b/roles/squid/templates/lists/domains.txt.j2 @@ -0,0 +1,7 @@ +# {{ ansible_managed }} +# {{ domains_title }} +# Un motif par ligne. Un point en tête couvre le domaine et ses +# sous-domaines (.exemple.fr), sans point le nom exact seulement. +{% for domain in domains_list %} +{{ domain }} +{% endfor %} diff --git a/roles/squid/templates/sarg-exclusions.j2 b/roles/squid/templates/sarg-exclusions.j2 new file mode 100644 index 0000000..d0b69e3 --- /dev/null +++ b/roles/squid/templates/sarg-exclusions.j2 @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +# {{ exclusions_titre }} +{% for entree in exclusions_liste %} +{{ entree }} +{% endfor %} diff --git a/roles/squid/templates/sarg-nginx.conf.j2 b/roles/squid/templates/sarg-nginx.conf.j2 new file mode 100644 index 0000000..dbf738e --- /dev/null +++ b/roles/squid/templates/sarg-nginx.conf.j2 @@ -0,0 +1,25 @@ +# {{ ansible_managed }} +# Consultation des rapports SARG, restreinte aux réseaux d'administration. +server { + listen {{ squid_sarg_web_port }}; + listen [::]:{{ squid_sarg_web_port }}; + server_name _; + + root {{ squid_sarg_output_dir }}; + index index.html; + autoindex on; + + # Les rapports contiennent l'historique de navigation : aucune indexation, + # aucune mise en cache côté navigateur. + add_header X-Robots-Tag "noindex, nofollow" always; + add_header Cache-Control "no-store" always; + +{% for reseau in squid_sarg_web_allowed %} + allow {{ reseau }}; +{% endfor %} + allow 127.0.0.1; + deny all; + + access_log /var/log/nginx/sarg-access.log; + error_log /var/log/nginx/sarg-error.log; +} diff --git a/roles/squid/templates/sarg-rapport.sh.j2 b/roles/squid/templates/sarg-rapport.sh.j2 new file mode 100644 index 0000000..5b6c8f0 --- /dev/null +++ b/roles/squid/templates/sarg-rapport.sh.j2 @@ -0,0 +1,38 @@ +#!/bin/sh +# {{ ansible_managed }} +# Génère un rapport SARG pour la période demandée : daily, weekly ou monthly. +# Appelé par /etc/cron.d/sarg-rapports. +set -eu + +periode="${1:-daily}" +hier="$(date -d 'yesterday' '+%d/%m/%Y')" + +case "${periode}" in + daily) + plage="${hier}-${hier}" + dossier=quotidien + ;; + weekly) + plage="$(date -d '7 days ago' '+%d/%m/%Y')-${hier}" + dossier=hebdomadaire + ;; + monthly) + plage="$(date -d "$(date '+%Y-%m-01') -1 month" '+%d/%m/%Y')-${hier}" + dossier=mensuel + ;; + *) + echo "Période inconnue : ${periode} (daily, weekly ou monthly)" >&2 + exit 1 + ;; +esac + +# Le journal courant et, s'il existe, celui que logrotate vient d'archiver. +journaux="{{ squid_access_log }}" +if [ -f "{{ squid_access_log }}.1" ]; then + journaux="${journaux} {{ squid_access_log }}.1" +fi + +exec sarg -f {{ squid_sarg_conf }} \ + -d "${plage}" \ + -o "{{ squid_sarg_output_dir }}/${dossier}" \ + ${journaux} diff --git a/roles/squid/templates/sarg.conf.j2 b/roles/squid/templates/sarg.conf.j2 new file mode 100644 index 0000000..f31a49c --- /dev/null +++ b/roles/squid/templates/sarg.conf.j2 @@ -0,0 +1,44 @@ +# {{ ansible_managed }} +# Configuration SARG générée par le rôle Ansible « squid ». +# Documentation des directives : man sarg.conf + +access_log {{ squid_access_log }} +temporary_dir {{ squid_sarg_temp_dir }} +output_dir {{ squid_sarg_output_dir }} + +charset {{ squid_sarg_charset }} +title "{{ squid_sarg_title }}" + +date_format e +resolve_ip {{ 'yes' if squid_sarg_resolve_ip | bool else 'no' }} +user_ip no +records_without_userid {{ squid_sarg_records_without_userid }} + +report_type {{ squid_sarg_report_type | join(' ') }} +topsites_num {{ squid_sarg_topsites_num }} +topuser_sort_field BYTES reverse +user_sort_field BYTES reverse +topsites_sort_order CONNECT D + +index yes +index_tree date +index_sort_order D +overwrite_report yes +remove_temp_files yes +long_url no +show_successful_message no +show_read_statistics no +lastlog {{ squid_sarg_keep }} + +{% if squid_sarg_privacy | bool %} +# Les statistiques restent agrégées, les URL détaillées sont masquées. +privacy yes +privacy_string "{{ squid_sarg_privacy_string }}" +privacy_string_color orange +{% endif %} +{% if squid_sarg_exclude_hosts | length > 0 %} +exclude_hosts /etc/sarg/exclude_hosts +{% endif %} +{% if squid_sarg_exclude_users | length > 0 %} +exclude_users /etc/sarg/exclude_users +{% endif %} diff --git a/roles/squid/templates/squid.conf.j2 b/roles/squid/templates/squid.conf.j2 new file mode 100644 index 0000000..d8e9b2d --- /dev/null +++ b/roles/squid/templates/squid.conf.j2 @@ -0,0 +1,215 @@ +# {{ ansible_managed }} +# Fichier généré par le rôle Ansible « squid ». +# Toute modification locale sera écrasée au prochain déploiement. +# Mode : {{ squid_mode }} | Filtrage : {{ squid_filter_mode }} | Authentification : {{ squid_auth_method }} | SSL Bump : {{ 'oui' if squid_ssl_bump | bool else 'non' }} + +# --------------------------------------------------------------------------- +# Identité du service +# --------------------------------------------------------------------------- +visible_hostname {{ squid_visible_hostname }} +httpd_suppress_version_string on +shutdown_lifetime 10 seconds +{% if squid_dns_v4_first | bool %} +dns_v4_first on +{% endif %} +{% if squid_always_direct_ftp | bool %} +acl FTP proto FTP +always_direct allow FTP +{% endif %} +{% if squid_dns_nameservers | length > 0 %} +dns_nameservers {{ squid_dns_nameservers | join(' ') }} +{% endif %} + +# --------------------------------------------------------------------------- +# Ports d'écoute +# --------------------------------------------------------------------------- +{% if squid_explicit_enabled | bool %} +{% if squid_ssl_bump | bool %} +http_port {{ squid_http_port }} name=explicit ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size={{ squid_ssl_db_size }} tls-cert={{ squid_ssl_ca_cert }} tls-key={{ squid_ssl_ca_key }} +{% else %} +http_port {{ squid_http_port }} name=explicit +{% endif %} +{% endif %} +{% if squid_intercept_enabled | bool %} +http_port {{ squid_intercept_http_port }} intercept name=intercept_http +{% if squid_ssl_bump | bool %} +https_port {{ squid_intercept_https_port }} intercept name=intercept_https ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size={{ squid_ssl_db_size }} tls-cert={{ squid_ssl_ca_cert }} tls-key={{ squid_ssl_ca_key }} +{% endif %} +{% endif %} + +# --------------------------------------------------------------------------- +# Définition des ACL +# --------------------------------------------------------------------------- +acl SSL_ports port {{ squid_ssl_ports | join(' ') }} +{% for port in squid_safe_ports %} +acl Safe_ports port {{ port }} +{% endfor %} +acl CONNECT method CONNECT +{% if squid_deny_internal_destinations | bool %} + +# Réseaux internes définis sur la DESTINATION : le proxy ne doit jamais +# servir de rebond vers le système d'information. +{% for reseau in squid_internal_destinations %} +acl internal_destinations dst {{ reseau }} +{% endfor %} +{% endif %} + +{% for network in squid_allowed_networks %} +acl localnet src {{ network }} +{% endfor %} +{% if squid_denied_sources | length > 0 %} +{% for source in squid_denied_sources %} +acl denied_sources src {{ source }} +{% endfor %} +{% endif %} +{% if squid_intercept_enabled | bool %} +acl intercepted myportname intercept_http{{ ' intercept_https' if squid_ssl_bump | bool else '' }} +{% endif %} + +{% if squid_denied_domains | length > 0 %} +{% if squid_filter_mode == 'blocklist' %} +acl denied_domains dstdomain "{{ squid_lists_dir }}/denied_domains.txt" +{% endif %} +{% if squid_ssl_bump | bool %} +acl denied_sni ssl::server_name "{{ squid_lists_dir }}/denied_domains.txt" +{% endif %} +{% endif %} +{% if squid_allowed_domains | length > 0 %} +acl allowed_domains dstdomain "{{ squid_lists_dir }}/allowed_domains.txt" +{% endif %} + +{% if squid_ssl_bump | bool %} +# --------------------------------------------------------------------------- +# Déchiffrement TLS (SSL Bump) +# --------------------------------------------------------------------------- +sslcrtd_program {{ squid_helper_certgen }} -s {{ squid_ssl_db_dir }} -M {{ squid_ssl_db_size }} +sslcrtd_children {{ squid_sslcrtd_children }} +acl step1 at_step SslBump1 +{% if squid_no_bump_domains | length > 0 %} +acl no_bump_domains ssl::server_name "{{ squid_lists_dir }}/no_bump_domains.txt" +{% endif %} +ssl_bump peek step1 +{% if squid_denied_domains | length > 0 %} +ssl_bump terminate denied_sni +{% endif %} +{% if squid_no_bump_domains | length > 0 %} +ssl_bump splice no_bump_domains +{% endif %} +ssl_bump bump all +{% if not squid_ssl_verify_upstream | bool %} +tls_outgoing_options flags=DONT_VERIFY_PEER +sslproxy_cert_error allow all +{% endif %} +{% endif %} + +{% if squid_auth_enabled | bool %} +# --------------------------------------------------------------------------- +# Authentification ({{ squid_auth_method }}) +# --------------------------------------------------------------------------- +{% include 'auth/' + squid_auth_method + '.conf.j2' %} + +acl authenticated proxy_auth REQUIRED +{% endif %} + +# --------------------------------------------------------------------------- +# Règles d'accès — l'ordre est significatif, Squid s'arrête à la 1re règle +# qui correspond +# --------------------------------------------------------------------------- +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +{% if squid_deny_internal_destinations | bool %} +http_access deny internal_destinations +{% endif %} +{% if squid_denied_sources | length > 0 %} +http_access deny denied_sources +{% endif %} + +{% if squid_filter_mode == 'blocklist' and squid_denied_domains | length > 0 %} +http_access deny denied_domains +{% endif %} +{% if squid_filter_mode == 'allowlist' %} +http_access deny !allowed_domains +{% endif %} + +{% if squid_auth_enabled | bool %} +http_access deny {{ '!intercepted ' if squid_intercept_enabled | bool else '' }}!authenticated +{% if squid_auth_method == 'ldap' and squid_ldap_group_acl | bool %} +http_access deny {{ '!intercepted ' if squid_intercept_enabled | bool else '' }}!allowed_groups +{% endif %} +{% endif %} + +{% if squid_allow_localnet | bool %} +http_access allow localnet +{% else %} +# Aucune autorisation globale : seuls les fragments ci-dessous ouvrent des flux. +{% endif %} +http_access allow localhost + +# --------------------------------------------------------------------------- +# Autorisations par serveur et par groupe +# Chaque fragment porte ses ACL source, destination et port. Une machine +# absente de tout fragment n'a aucun accès : elle tombe sur le deny final. +# --------------------------------------------------------------------------- +include {{ squid_confd_dir }}/*.conf + +# Et rien d'autre. +http_access deny all + +# --------------------------------------------------------------------------- +# Journalisation +# --------------------------------------------------------------------------- +logformat custom_log {{ squid_logformat_custom | trim }} +access_log {{ squid_access_log }} {{ squid_access_log_format }} +{% if squid_log_to_syslog | bool %} +access_log syslog:{{ squid_syslog_facility }}.info {{ squid_access_log_format }} +{% endif %} +cache_log {{ squid_cache_log }} +# La rotation est confiée à logrotate (/etc/logrotate.d/squid) +logfile_rotate 0 +strip_query_terms off + +# --------------------------------------------------------------------------- +# Cache +# --------------------------------------------------------------------------- +cache_mem {{ squid_cache_mem }} +{% if squid_cache_enabled | bool %} +maximum_object_size {{ squid_maximum_object_size }} +cache_dir ufs /var/spool/squid {{ squid_cache_dir_size }} 16 256 +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern . 0 20% 4320 +{% else %} +# Aucun cache demandé : le proxy sert au contrôle d'accès et à la traçabilité +cache deny all +{% endif %} +coredump_dir /var/spool/squid + +# --------------------------------------------------------------------------- +# Confidentialité et limites +# --------------------------------------------------------------------------- +{% if squid_snmp_enabled | bool %} +# --------------------------------------------------------------------------- +# Supervision SNMP +# --------------------------------------------------------------------------- +acl snmp_supervision snmp_community {{ squid_snmp_community }} +snmp_port {{ squid_snmp_port }} +snmp_access allow snmp_supervision {{ squid_snmp_allowed_from }} +snmp_access deny all + +{% endif %} +forwarded_for {{ squid_forwarded_for }} +via {{ squid_via }} +connect_timeout {{ squid_connect_timeout }} +request_header_max_size {{ squid_request_header_max_size }} +{% if squid_extra_config | length > 0 %} + +# --------------------------------------------------------------------------- +# Directives complémentaires (squid_extra_config) +# --------------------------------------------------------------------------- +{% for line in squid_extra_config %} +{{ line }} +{% endfor %} +{% endif %} diff --git a/roles/squid/vars/main.yml b/roles/squid/vars/main.yml new file mode 100644 index 0000000..048b23f --- /dev/null +++ b/roles/squid/vars/main.yml @@ -0,0 +1,26 @@ +--- +# Chemins des helpers livrés par les paquets Debian squid / squid-openssl. +# Ne pas surcharger sauf empaquetage particulier. +squid_helper_dir: /usr/lib/squid +squid_helper_basic_ncsa: /usr/lib/squid/basic_ncsa_auth +squid_helper_basic_ldap: /usr/lib/squid/basic_ldap_auth +squid_helper_group_ldap: /usr/lib/squid/ext_ldap_group_acl +squid_helper_negotiate_kerberos: /usr/lib/squid/negotiate_kerberos_auth +squid_helper_certgen: /usr/lib/squid/security_file_certgen + +# Paquets complémentaires selon les options activées. +squid_extra_packages_ssl: + - openssl + - ca-certificates +squid_extra_packages_kerberos: + - krb5-user + +# Valeurs acceptées, utilisées par les assertions de tasks/assert.yml +squid_valid_modes: [explicit, intercept, both] +squid_valid_filter_modes: [blocklist, allowlist] +squid_valid_auth_methods: [none, basic, ldap, kerberos] + +# Raccourcis calculés +squid_explicit_enabled: "{{ squid_mode in ['explicit', 'both'] }}" +squid_intercept_enabled: "{{ squid_mode in ['intercept', 'both'] }}" +squid_auth_enabled: "{{ squid_auth_method != 'none' }}" diff --git a/scripts/import-conf-d.py b/scripts/import-conf-d.py new file mode 100644 index 0000000..f4daa8e --- /dev/null +++ b/scripts/import-conf-d.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +"""Convertit des fragments Squid conf.d existants en fiches YAML. + +Lit un répertoire de fichiers ``*.conf`` écrits sur le modèle +``acl src ...`` / ``acl dstdomain ...`` / +``http_access allow ``, et produit une fiche +par fichier dans ``servers/``, plus un rapport d'anomalies portant sur +l'ensemble du parc. + +Rien n'est écrasé sans ``--force``. Le script ne modifie jamais la source. + + python3 scripts/import-conf-d.py /etc/squid/conf.d --sortie servers/ + python3 scripts/import-conf-d.py /etc/squid/conf.d --rapport-seul +""" + +from __future__ import annotations + +import argparse +import ipaddress +import pathlib +import re +import sys +from collections import defaultdict + +ACL_RE = re.compile(r"^acl\s+(\S+)\s+(\S+)\s+(.*)$") +ACCESS_RE = re.compile(r"^http_access\s+allow\s+(.*)$") + + +class Fragment: + """Un fichier conf.d analysé.""" + + def __init__(self, chemin: pathlib.Path) -> None: + self.chemin = chemin + self.nom = chemin.stem + self.sources: dict[str, list[str]] = {} + self.domaines: dict[str, list[str]] = {} + self.ports: dict[str, list[str]] = {} + self.acces: list[list[str]] = [] + self.anomalies: list[str] = [] + + +def analyser(chemin: pathlib.Path) -> Fragment: + fragment = Fragment(chemin) + vus: dict[str, int] = defaultdict(int) + + for numero, brute in enumerate(chemin.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + ligne = brute.split("#", 1)[0].strip() + if not ligne: + continue + if brute.rstrip("\n") != brute.rstrip(): + fragment.anomalies.append(f"ligne {numero} : espace en fin de ligne") + + correspondance = ACL_RE.match(ligne) + if correspondance: + nom, type_acl, valeurs_brutes = correspondance.groups() + valeurs = valeurs_brutes.split() + vus[nom] += 1 + if vus[nom] > 1: + fragment.anomalies.append(f"ligne {numero} : ACL « {nom} » déclarée plusieurs fois") + cible = {"src": fragment.sources, "dstdomain": fragment.domaines, "port": fragment.ports}.get(type_acl) + if cible is None: + fragment.anomalies.append(f"ligne {numero} : type d'ACL « {type_acl} » non pris en charge") + continue + cible.setdefault(nom, []).extend(valeurs) + if type_acl == "dstdomain": + for valeur in valeurs: + controler_domaine(fragment, numero, valeur) + continue + + correspondance = ACCESS_RE.match(ligne) + if correspondance: + fragment.acces.append(correspondance.group(1).split()) + continue + + if not ligne.startswith(("http_access deny", "include")): + fragment.anomalies.append(f"ligne {numero} : directive non reconnue par l'import — {ligne[:60]}") + + return fragment + + +def controler_domaine(fragment: Fragment, numero: int, valeur: str) -> None: + if valeur.endswith("/"): + fragment.anomalies.append(f"ligne {numero} : « {valeur} » se termine par / — ne correspondra jamais") + if valeur.endswith(">") or valeur.endswith("."): + fragment.anomalies.append(f"ligne {numero} : « {valeur} » semble tronqué") + candidat = valeur.lstrip("*.") + try: + ipaddress.ip_address(candidat) + except ValueError: + return + fragment.anomalies.append(f"ligne {numero} : « {valeur} » est une adresse IP — utiliser une ACL « dst »") + + +def construire_fiche(fragment: Fragment, ports_connus: dict[str, str]) -> dict: + """Regroupe les autorisations par jeu de ports identique.""" + par_domaine: dict[str, set[str]] = defaultdict(set) + environnement = "prod" + + noms_sources = {nom.lower() for nom in fragment.sources} + noms_domaines = {nom.lower() for nom in fragment.domaines} + + for regle in fragment.acces: + if not regle: + continue + jetons = list(regle) + + # Forme « allow [ports] » ou, dans les + # fragments applicatifs sans ACL source, « allow [ports] ». + source = None + if jetons[0].lower() in noms_sources: + source = jetons.pop(0) + if "_" in source: + environnement = source.rsplit("_", 1)[-1] or "prod" + elif noms_sources: + fragment.anomalies.append( + f"ACL source « {jetons[0]} » utilisée mais non définie dans ce fichier" + ) + + if not jetons: + fragment.anomalies.append(f"autorisation sans destination : {' '.join(regle)}") + continue + + destination = jetons.pop(0) + ports = jetons + + if destination.lower() not in noms_domaines: + fragment.anomalies.append( + f"ACL destination « {destination} » utilisée mais non définie dans ce fichier " + "— Squid refusera de démarrer si elle n'existe nulle part ailleurs" + ) + continue + + cle_reelle = next(nom for nom in fragment.domaines if nom.lower() == destination.lower()) + for domaine in fragment.domaines[cle_reelle]: + if not ports: + par_domaine[domaine].add("TOUS_PORTS") + for port in ports: + par_domaine[domaine].add(ports_connus.get(port, port.removesuffix("_port"))) + + regroupe: dict[tuple[str, ...], list[str]] = defaultdict(list) + for domaine, ports in sorted(par_domaine.items()): + regroupe[tuple(sorted(ports))].append(domaine) + + fiche = { + "name": fragment.nom, + "env": environnement, + "description": f"Importé depuis {fragment.chemin.name}", + "sources": sorted({valeur for valeurs in fragment.sources.values() for valeur in valeurs}), + "rules": [ + {"domains": domaines, "ports": list(ports)} + for ports, domaines in sorted(regroupe.items(), key=lambda element: element[1]) + ], + } + return fiche + + +def rendre_yaml(fiche: dict) -> str: + lignes = ["---", f"name: {fiche['name']}", f"env: {fiche['env']}", f"description: {fiche['description']}", "sources:"] + lignes += [f" - {source}" for source in fiche["sources"]] + lignes.append("rules:") + for regle in fiche["rules"]: + lignes.append(" - domains:") + lignes += [f' - "{domaine}"' for domaine in regle["domains"]] + ports = ", ".join(f'"{port}"' for port in regle["ports"]) + lignes.append(f" ports: [{ports}]") + return "\n".join(lignes) + "\n" + + +def main() -> int: + analyseur = argparse.ArgumentParser(description="Import de fragments Squid conf.d vers des fiches YAML") + analyseur.add_argument("source", type=pathlib.Path, help="répertoire contenant les fichiers *.conf") + analyseur.add_argument("--sortie", type=pathlib.Path, default=pathlib.Path("servers")) + analyseur.add_argument("--rapport-seul", action="store_true", help="n'écrit aucune fiche") + analyseur.add_argument("--force", action="store_true", help="écrase les fiches existantes") + arguments = analyseur.parse_args() + + fichiers = sorted(arguments.source.glob("*.conf")) + if not fichiers: + print(f"Aucun fichier *.conf dans {arguments.source}", file=sys.stderr) + return 1 + + fragments = [analyser(chemin) for chemin in fichiers] + + # Catalogue des ports : nom d'ACL « 8443_port » -> clé « 8443 » + ports_connus: dict[str, str] = {} + for fragment in fragments: + for nom in fragment.ports: + ports_connus[nom] = nom[:-5] if nom.endswith("_port") else nom + for nom in list(ports_connus): + ports_connus.setdefault(nom, nom) + + # Collision : même nom d'ACL, valeurs différentes selon les fichiers + global_domaines: dict[str, set[str]] = defaultdict(set) + origine: dict[str, set[str]] = defaultdict(set) + for fragment in fragments: + for nom, valeurs in fragment.domaines.items(): + global_domaines[nom.lower()].update(valeurs) + origine[nom.lower()].add(fragment.chemin.name) + + collisions = {nom: valeurs for nom, valeurs in global_domaines.items() if len(valeurs) > 1} + + total_anomalies = 0 + if not arguments.rapport_seul: + arguments.sortie.mkdir(parents=True, exist_ok=True) + + for fragment in fragments: + fiche = construire_fiche(fragment, ports_connus) + if not arguments.rapport_seul: + cible = arguments.sortie / f"{fragment.nom}.yml" + if cible.exists() and not arguments.force: + print(f" ignoré (existe déjà) : {cible}") + else: + cible.write_text(rendre_yaml(fiche), encoding="utf-8") + + if fragment.anomalies: + total_anomalies += len(fragment.anomalies) + print(f"\n{fragment.chemin.name}") + for anomalie in dict.fromkeys(fragment.anomalies): + print(f" - {anomalie}") + + if collisions: + print("\nACL portant le même nom avec des valeurs différentes selon les fichiers :") + for nom, valeurs in sorted(collisions.items()): + print(f" - {nom} : {', '.join(sorted(valeurs))} (vu dans {', '.join(sorted(origine[nom]))})") + total_anomalies += len(collisions) + + print(f"\n{len(fragments)} fichier(s) analysé(s), {total_anomalies} anomalie(s).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/openbao-env.sh b/scripts/openbao-env.sh new file mode 100644 index 0000000..2e3b94c --- /dev/null +++ b/scripts/openbao-env.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Lit les paramètres d'appel de Semaphore dans OpenBao et les exporte dans +# l'environnement du job Gitea Actions. +# +# Gitea ne détient que OPENBAO_ADDR, OPENBAO_ROLE_ID et OPENBAO_SECRET_ID ; +# tout le reste vit dans OpenBao, sous secret/squid : +# semaphore_url, semaphore_token, semaphore_project_id, semaphore_template_id +# +# Usage (dans un job) : source scripts/openbao-env.sh +# --------------------------------------------------------------------------- +set -euo pipefail + +: "${OPENBAO_ADDR:?OPENBAO_ADDR manquant}" +: "${OPENBAO_ROLE_ID:?OPENBAO_ROLE_ID manquant}" +: "${OPENBAO_SECRET_ID:?OPENBAO_SECRET_ID manquant}" +OPENBAO_SECRET_PATH="${OPENBAO_SECRET_PATH:-secret/data/squid}" + +login_payload=$(printf '{"role_id":"%s","secret_id":"%s"}' \ + "${OPENBAO_ROLE_ID}" "${OPENBAO_SECRET_ID}") + +bao_token=$(curl -sS --fail-with-body \ + --request POST \ + --data "${login_payload}" \ + "${OPENBAO_ADDR}/v1/auth/approle/login" | jq -r '.auth.client_token') + +if [ -z "${bao_token}" ] || [ "${bao_token}" = "null" ]; then + echo "Échec de l'authentification AppRole sur OpenBao" >&2 + exit 1 +fi +echo "::add-mask::${bao_token}" + +secret_json=$(curl -sS --fail-with-body \ + --header "X-Vault-Token: ${bao_token}" \ + "${OPENBAO_ADDR}/v1/${OPENBAO_SECRET_PATH}" | jq -c '.data.data') + +for key in semaphore_url semaphore_token semaphore_project_id semaphore_template_id; do + value=$(printf '%s' "${secret_json}" | jq -r --arg k "${key}" '.[$k] // empty') + if [ -z "${value}" ]; then + echo "Clé « ${key} » absente de ${OPENBAO_SECRET_PATH}" >&2 + exit 1 + fi + if [ "${key}" = "semaphore_token" ]; then + echo "::add-mask::${value}" + fi + export "$(echo "${key}" | tr '[:lower:]' '[:upper:]')=${value}" +done + +# Trace volontairement minimale : rien d'autre que l'URL n'est affiché. +echo "Secrets chargés depuis OpenBao — cible Semaphore : ${SEMAPHORE_URL}" diff --git a/scripts/trigger-semaphore.sh b/scripts/trigger-semaphore.sh new file mode 100644 index 0000000..0134688 --- /dev/null +++ b/scripts/trigger-semaphore.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Déclenche le modèle Semaphore « Déploiement Squid » puis suit la tâche +# jusqu'à son issue, et propage le code de retour au job Gitea Actions. +# +# Variables attendues (fournies par scripts/openbao-env.sh) : +# SEMAPHORE_URL, SEMAPHORE_TOKEN, SEMAPHORE_PROJECT_ID, SEMAPHORE_TEMPLATE_ID +# Variables optionnelles : +# SEMAPHORE_LIMIT restriction --limit (ex. proxy01) +# SEMAPHORE_TAGS étiquettes Ansible (ex. config,service) +# SEMAPHORE_DRY_RUN true pour un --check +# SEMAPHORE_TIMEOUT délai d'attente en secondes (défaut 900) +# --------------------------------------------------------------------------- +set -euo pipefail + +: "${SEMAPHORE_URL:?SEMAPHORE_URL manquant}" +: "${SEMAPHORE_TOKEN:?SEMAPHORE_TOKEN manquant}" +: "${SEMAPHORE_PROJECT_ID:?SEMAPHORE_PROJECT_ID manquant}" +: "${SEMAPHORE_TEMPLATE_ID:?SEMAPHORE_TEMPLATE_ID manquant}" + +limit="${SEMAPHORE_LIMIT:-}" +tags="${SEMAPHORE_TAGS:-}" +dry_run="${SEMAPHORE_DRY_RUN:-false}" +timeout="${SEMAPHORE_TIMEOUT:-900}" + +payload=$(jq -nc \ + --argjson template_id "${SEMAPHORE_TEMPLATE_ID}" \ + --argjson dry_run "${dry_run}" \ + --arg limit "${limit}" \ + --arg tags "${tags}" \ + '{template_id: $template_id, debug: false, dry_run: $dry_run} + + (if $limit == "" then {} else {limit: $limit} end) + + (if $tags == "" then {} else {tags: $tags} end)') + +task_id=$(curl -sS --fail-with-body \ + --request POST \ + --header "Authorization: Bearer ${SEMAPHORE_TOKEN}" \ + --header "Content-Type: application/json" \ + --data "${payload}" \ + "${SEMAPHORE_URL}/api/project/${SEMAPHORE_PROJECT_ID}/tasks" | jq -r '.id') + +if [ -z "${task_id}" ] || [ "${task_id}" = "null" ]; then + echo "Semaphore n'a pas renvoyé d'identifiant de tâche" >&2 + exit 1 +fi + +echo "Tâche Semaphore n°${task_id} démarrée : ${SEMAPHORE_URL}/project/${SEMAPHORE_PROJECT_ID}/history?t=${task_id}" + +elapsed=0 +interval=10 +status="" +while [ "${elapsed}" -lt "${timeout}" ]; do + status=$(curl -sS --fail-with-body \ + --header "Authorization: Bearer ${SEMAPHORE_TOKEN}" \ + "${SEMAPHORE_URL}/api/project/${SEMAPHORE_PROJECT_ID}/tasks/${task_id}" \ + | jq -r '.status') + + case "${status}" in + success) + echo "Tâche n°${task_id} : succès" + exit 0 + ;; + error | stopped | failed) + echo "Tâche n°${task_id} : échec (statut « ${status} »)" >&2 + echo "Journal complet dans l'interface Semaphore." >&2 + exit 1 + ;; + *) + sleep "${interval}" + elapsed=$((elapsed + interval)) + ;; + esac +done + +echo "Délai dépassé (${timeout}s) — dernier statut connu : « ${status} »" >&2 +exit 1 diff --git a/servers/swappfond01.yml b/servers/swappfond01.yml new file mode 100644 index 0000000..df3180b --- /dev/null +++ b/servers/swappfond01.yml @@ -0,0 +1,14 @@ +--- +name: swappfond01 +env: dev +description: Importé depuis swappfond01.conf +sources: + - swappfond01.tips-of-mine.local +rules: + - domains: + - "tsm-hprod-data-documents.storage.googleapis.com" + ports: ["https"] + - domains: + - "api.fts-crm360.com" + - "www.ecb.europa.eu" + ports: ["http", "https"] diff --git a/servers/swappfond02.yml b/servers/swappfond02.yml new file mode 100644 index 0000000..7c9d5f7 --- /dev/null +++ b/servers/swappfond02.yml @@ -0,0 +1,16 @@ +--- +name: swappfond02 +env: dev +description: Importé depuis swappfond02.conf +sources: + - swappfond02.tips-of-mine.local +rules: + - domains: + - "fonts.googleapis.com" + - "fonts.gstatic.com" + - "tsm-hprod-data-documents.storage.googleapis.com" + ports: ["https"] + - domains: + - "api.fts-crm360.com" + - "www.ecb.europa.eu" + ports: ["http", "https"] diff --git a/servers/swappfond03.yml b/servers/swappfond03.yml new file mode 100644 index 0000000..4f0c65b --- /dev/null +++ b/servers/swappfond03.yml @@ -0,0 +1,28 @@ +--- +name: slappfond03 +env: dev +description: Importé depuis slappfond03.conf +sources: + - slappfond03.tips-of-mine.local +rules: + - domains: + - "7-51-1-app.agent.datadoghq.eu" + - "agent-http-intake.logs.datadoghq.eu" + - "api.datadoghq.eu" + - "api.github.com" + - "apt.datadoghq.com" + - "codeload.github.com" + - "config.datadoghq.eu" + - "dbm-metrics-intake.datadoghq.eu" + - "github.com" + - "instrumentation-telemetry-intake.datadoghq.eu" + - "keys.datadoghq.com" + - "packagist.org" + - "process.datadoghq.eu" + - "raw.githubusercontent.com" + - "repo.packagist.org" + - "s3.amazonaws.com" + ports: ["https"] + - domains: + - "gitlab.com" + ports: ["http", "https", "sftp"] diff --git a/servers/swappfono01.yml b/servers/swappfono01.yml new file mode 100644 index 0000000..9640094 --- /dev/null +++ b/servers/swappfono01.yml @@ -0,0 +1,11 @@ +--- +name: swappfono01 +env: preprod +description: Importé depuis swappfono01.conf +sources: + - swappfono01.tips-of-mine.local +rules: + - domains: + - "tsm-hprod-data-documents.storage.googleapis.com" + - "tsm-preprod-data-documents.storage.googleapis.com" + ports: ["https"] diff --git a/servers/swappfono02.yml b/servers/swappfono02.yml new file mode 100644 index 0000000..c3faf9a --- /dev/null +++ b/servers/swappfono02.yml @@ -0,0 +1,42 @@ +--- +name: swappfono02 +env: preprod +description: Importé depuis swappfono02.conf +sources: + - swappfono02.tips-of-mine.local +rules: + - domains: + - "MPDGYZR-TSM.registry.snowflakecomputing.com" + - "api-cdb67645.duosecurity.com" + - "app.snowflake.com" + - "apps-api.c1.eu-central-1.aws.app.snowflake.com" + - "client-telemetry.snowflakecomputing.com" + - "metadata.google.internal" + - "mpdgyzr-tsm.snowflakecomputing.com" + - "o.ss2.us" + - "oauth2.googleapis.com" + - "ocsp.r2m01.amazontrust.com" + - "ocsp.r2m03.amazontrust.com" + - "ocsp.rootca1.amazontrust.com" + - "ocsp.rootg2.amazontrust.com" + - "ocsp.snowflakecomputing.com" + - "sfc-eu-ds1-26-customer-stage.s3.amazonaws.com" + - "sfc-eu-ds1-26-customer-stage.s3.eu-central-1.amazonaws.com" + - "sfc-repo.snowflakecomputing.com" + ports: ["http", "https"] + - domains: + - "aa77142.europe-west4.gcp.snowflakecomputing.com" + - "accounts.google.com" + - "bigquery.googleapis.com" + - "bigquerystorage.googleapis.com" + - "cq70349.eu-central-1.snowflakecomputing.com" + - "gcpeuropewest4-ki8z4g5-stage.storage.googleapis.com" + - "license.qlikcloud.com" + - "ocsp.digicert.com" + - "ocsp.pki.goog" + - "sfsxparxz6auctt.eu.qlikcloud.com" + - "sheets.googleapis.com" + - "storage.googleapis.com" + - "tsm.eu.qlikcloud.com" + - "www.googleapis.com" + ports: ["https"] diff --git a/servers/swappfono03.yml b/servers/swappfono03.yml new file mode 100644 index 0000000..5db8c51 --- /dev/null +++ b/servers/swappfono03.yml @@ -0,0 +1,20 @@ +--- +name: swappfono03 +env: preprod +description: Importé depuis swappfono03.conf +sources: + - swappfono03.tips-of-mine.local +rules: + - domains: + - "deb.debian.org" + - "packages.sury.org" + - "security.debian.org" + ports: ["ftp", "http", "https"] + - domains: + - "gccsg.tips-of-mine.com" + ports: ["ftp", "git", "http", "https", "sftp"] + - domains: + - "github.com" + - "packages.adoptium.net" + - "pear.php.net" + ports: ["https"] diff --git a/servers/swappfonp01.yml b/servers/swappfonp01.yml new file mode 100644 index 0000000..a1b52cc --- /dev/null +++ b/servers/swappfonp01.yml @@ -0,0 +1,76 @@ +--- +name: swappfonp01 +env: prod +description: Importé depuis swappfonp01.conf +sources: + - swappfonp01.tips-of-mine.local +rules: + - domains: + - "195.154.227.5" + - "home758503790.1and1-data.host" + ports: ["ftp"] + - domains: + - "213.246.57.28" + - "accounts.google.com" + - "geo2.adobe.com" + - "github.com" + - "objects.githubusercontent.com" + - "ocsp.digicert.com" + - "ocsps.ssl.com" + - "p13n.adobe.io" + - "pebics.cm-cic.com" + - "www.ebics.socgen.com" + ports: ["https"] + - domains: + - "62.13.23.250" + - "as2.edi-services.fr" + - "as2.foc-solution.de" + - "as2.mytfx.com" + ports: ["8080"] + - domains: + - "93.90.139.134" + ports: ["50000"] + - domains: + - "acroipm2.adobe.com" + - "ardownload3.adobe.com" + - "armmf.adobe.com" + - "edi.classen.de" + - "www.norddefrance-ediweb.credit-agricole.fr" + ports: ["http", "https"] + - domains: + - "as2-out.qweeby.com" + ports: ["18080"] + - domains: + - "as2-prod.ediservices.com" + ports: ["8001"] + - domains: + - "as2-prod.tradexpressondemand.com" + - "cxms-s3.conxpert.de" + - "datawizard.kaindl.com" + ports: ["http"] + - domains: + - "as2gw.atgpedi.net" + ports: ["4080"] + - domains: + - "as2gw.pranke.com" + ports: ["9085"] + - domains: + - "edi-eu.rocketedx.com" + ports: ["5080"] + - domains: + - "edi.zimmer-rohde.com" + ports: ["8080", "8443"] + - domains: + - "ediserver0570.softzoll.de" + ports: ["8081", "8441"] + - domains: + - "filtersun.weblockup.com:2169" + - "tests.weblockup.com:2169" + ports: ["2169"] + - domains: + - "ftp.partoo.fr" + - "ftptransfer-prod.adeo.pub.adeoservices.com" + ports: ["2222"] + - domains: + - "imap.gmail.com" + ports: ["993"] diff --git a/servers/swappfonp02.yml b/servers/swappfonp02.yml new file mode 100644 index 0000000..d9c50a5 --- /dev/null +++ b/servers/swappfonp02.yml @@ -0,0 +1,16 @@ +--- +name: swappfonp02 +env: prod +description: Importé depuis swappfonp02.conf +sources: + - swappfonp02.tips-of-mine.local +rules: + - domains: + - "185.51.218.178" + ports: ["ftp"] + - domains: + - "customerapi.fts-crm360.com" + ports: ["https"] + - domains: + - "sftp.yootalent.net" + ports: ["sftp"] diff --git a/servers/swappfonp03.yml b/servers/swappfonp03.yml new file mode 100644 index 0000000..904e599 --- /dev/null +++ b/servers/swappfonp03.yml @@ -0,0 +1,21 @@ +--- +name: swappfonp03 +env: prod +description: Importé depuis swappfonp03.conf +sources: + - swappfonp03.tips-of-mine.local +rules: + - domains: + - "api.github.com" + - "github.com" + - "glpi-network.com" + - "services.glpi-network.com" + ports: ["ftp", "http", "https"] + - domains: + - "apigtwb2c.us.dell.com" + - "telemetry.glpi-project.org" + ports: ["http", "https"] + - domains: + - "oauth2.googleapis.com" + - "openidconnect.googleapis.com" + ports: ["https"] diff --git a/servers/swappfonr01.yml b/servers/swappfonr01.yml new file mode 100644 index 0000000..d903264 --- /dev/null +++ b/servers/swappfonr01.yml @@ -0,0 +1,16 @@ +--- +name: swappfonr01 +env: rec +description: Importé depuis swappfonr01.conf +sources: + - swappfonr01.tips-of-mine.local +rules: + - domains: + - "api.fts-crm360.com" + - "tips-of-mine.com" + - "tsm-hprod-data-documents.storage.googleapis.com" + - "tsm-preprod-data-documents.storage.googleapis.com" + ports: ["https"] + - domains: + - "www.ecb.europa.eu" + ports: ["http", "https"] diff --git a/servers/swappfonr02.yml b/servers/swappfonr02.yml new file mode 100644 index 0000000..6ae6aad --- /dev/null +++ b/servers/swappfonr02.yml @@ -0,0 +1,16 @@ +--- +name: swappfonr02 +env: prod +description: Importé depuis swappfonr02.conf +sources: + - swappfonr02.tips-of-mine.local +rules: + - domains: + - "185.51.218.178" + ports: ["ftp"] + - domains: + - "customerapi.fts-crm360.com" + ports: ["https"] + - domains: + - "sftp.yootalent.net" + ports: ["sftp"] diff --git a/servers/swappfonr03.yml b/servers/swappfonr03.yml new file mode 100644 index 0000000..ee1f359 --- /dev/null +++ b/servers/swappfonr03.yml @@ -0,0 +1,15 @@ +--- +name: swappfonr03 +env: rec +description: Importé depuis swappfonr03.conf +sources: + - swappfonr03.tips-of-mine.local +rules: + - domains: + - "centos-mirror.usessionbuddy.com" + - "epel.besthosting.ua" + - "mirrorlist.centos.org" + - "mirrors.fedoraproject.org" + - "oauth2.googleapis.com" + - "usessionbuddy.com" + ports: ["http", "https"] diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..e0f4b52 --- /dev/null +++ b/site.yml @@ -0,0 +1,8 @@ +--- +- name: Déploiement du proxy web Squid + hosts: squid + become: "{{ squid_become | default(false) }}" + gather_facts: true + roles: + - role: squid + tags: [squid] diff --git a/tests/ci-paths.yml b/tests/ci-paths.yml new file mode 100644 index 0000000..e780ec9 --- /dev/null +++ b/tests/ci-paths.yml @@ -0,0 +1,12 @@ +--- +# Chemins de rendu propres à la CI — ce fichier est chargé après les +# defaults du rôle et les surcharge. +render_dir: /tmp/squid-render +squid_lists_dir: /tmp/squid-render/lists +squid_ssl_dir: /tmp/squid-render +squid_ssl_ca_cert: /tmp/squid-render/ca.crt +squid_ssl_ca_key: /tmp/squid-render/ca.key +squid_ssl_db_dir: /tmp/squid-render/ssl_db +squid_confd_dir: /tmp/squid-render/conf.d +squid_servers_dir: "{{ playbook_dir }}/servers-ci" +squid_groups_dir: "{{ playbook_dir }}/groups-ci" diff --git a/tests/groups-ci/windows.yml b/tests/groups-ci/windows.yml new file mode 100644 index 0000000..8888ba7 --- /dev/null +++ b/tests/groups-ci/windows.yml @@ -0,0 +1,9 @@ +--- +name: windows +description: Groupe de test CI (adresses IP, aucune résolution DNS requise) +members: + - 10.0.4.10 + - 10.0.4.11 +rules: + - domains: [download.windowsupdate.com, go.microsoft.com] + ports: [http, https] diff --git a/tests/inventory-ci.yml b/tests/inventory-ci.yml new file mode 100644 index 0000000..3840406 --- /dev/null +++ b/tests/inventory-ci.yml @@ -0,0 +1,5 @@ +--- +all: + hosts: + localhost: + ansible_connection: local diff --git a/tests/render.yml b/tests/render.yml new file mode 100644 index 0000000..c4a888b --- /dev/null +++ b/tests/render.yml @@ -0,0 +1,105 @@ +--- +# Rend la configuration Squid hors de toute machine cible, afin de la +# soumettre à « squid -k parse » dans la CI. Aucun paquet n'est installé, +# aucun service n'est touché. +- name: Rendu hors ligne de la configuration Squid + hosts: localhost + connection: local + gather_facts: false + vars_files: + - ../roles/squid/defaults/main.yml + - ../roles/squid/vars/main.yml + - ci-paths.yml + tasks: + - name: Créer les répertoires de rendu + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ render_dir }}" + - "{{ squid_lists_dir }}" + + - name: Rendre les listes de domaines + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/lists/domains.txt.j2" + dest: "{{ squid_lists_dir }}/{{ item.name }}" + mode: "0644" + vars: + domains_list: "{{ item.list }}" + domains_title: "{{ item.name }}" + loop: + - {name: denied_domains.txt, list: "{{ squid_denied_domains }}"} + - {name: allowed_domains.txt, list: "{{ squid_allowed_domains }}"} + - {name: no_bump_domains.txt, list: "{{ squid_no_bump_domains }}"} + loop_control: + label: "{{ item.name }}" + when: item.list | length > 0 + +# - name: Charger et valider les fiches +# ansible.builtin.import_tasks: ../roles/squid/tasks/profiles.yml + + - name: Charger et valider les fiches + ansible.builtin.include_role: + name: squid + tasks_from: profiles + + - name: Créer le répertoire des fragments + ansible.builtin.file: + path: "{{ squid_confd_dir }}" + state: directory + mode: "0755" + + - name: Rendre le catalogue de ports + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/confd/ports.conf.j2" + dest: "{{ squid_confd_dir }}/00-ports.conf" + mode: "0644" + + - name: Rendre les fragments de groupes + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/confd/group.conf.j2" + dest: "{{ squid_confd_dir }}/10-groupe-{{ item.name }}.conf" + mode: "0644" + loop: "{{ squid_groups }}" + loop_control: + label: "{{ item.name }}" + vars: + profile: "{{ item }}" + + - name: Rendre les fragments de serveurs + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/confd/server.conf.j2" + dest: >- + {{ squid_confd_dir }}/{{ squid_env_prefixes.get(item.env | default('prod'), + squid_env_prefix_default) }}-{{ item.name }}.conf + mode: "0644" + loop: "{{ squid_servers }}" + loop_control: + label: "{{ item.name }}" + vars: + profile: "{{ item }}" + + - name: Rendre squid.conf + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/squid.conf.j2" + dest: "{{ render_dir }}/squid.conf" + mode: "0644" + + - name: Rendre la configuration SARG + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/sarg.conf.j2" + dest: "{{ render_dir }}/sarg.conf" + mode: "0644" + + - name: Rendre le script de rapport SARG + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/sarg-rapport.sh.j2" + dest: "{{ render_dir }}/sarg-rapport.sh" + mode: "0750" + + - name: Rendre le site nginx de consultation + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/squid/templates/sarg-nginx.conf.j2" + dest: "{{ render_dir }}/sarg-nginx.conf" + mode: "0644" diff --git a/tests/scenarios/allowlist-cache.yml b/tests/scenarios/allowlist-cache.yml new file mode 100644 index 0000000..22d4c7a --- /dev/null +++ b/tests/scenarios/allowlist-cache.yml @@ -0,0 +1,15 @@ +--- +squid_auth_method: basic +squid_mode: intercept +squid_ssl_bump: true +squid_filter_mode: allowlist +squid_allowed_domains: [.debian.org] +squid_denied_domains: [.exemple-interdit.fr] +squid_denied_sources: [10.0.4.200] +squid_cache_enabled: true +squid_log_to_syslog: true +squid_basic_users: + - username: ci + password_hash: "$apr1$ci000000$0000000000000000000000" +squid_extra_config: + - "client_lifetime 12 hours" diff --git a/tests/scenarios/basic.yml b/tests/scenarios/basic.yml new file mode 100644 index 0000000..5f1c00e --- /dev/null +++ b/tests/scenarios/basic.yml @@ -0,0 +1,9 @@ +--- +squid_auth_method: basic +squid_mode: explicit +squid_ssl_bump: false +squid_filter_mode: allowlist +squid_allowed_domains: [.debian.org, .tips-of-mine.com] +squid_basic_users: + - username: ci + password_hash: "$apr1$ci000000$0000000000000000000000" diff --git a/tests/scenarios/kerberos.yml b/tests/scenarios/kerberos.yml new file mode 100644 index 0000000..d551467 --- /dev/null +++ b/tests/scenarios/kerberos.yml @@ -0,0 +1,7 @@ +--- +squid_auth_method: kerberos +squid_mode: explicit +squid_ssl_bump: true +squid_kerberos_realm: LAB.TIPS-OF-MINE.INTERNAL +squid_kerberos_service_principal: HTTP/proxy01.lab.tips-of-mine.internal +squid_kerberos_keytab_b64: "Y2k=" diff --git a/tests/scenarios/ldap.yml b/tests/scenarios/ldap.yml new file mode 100644 index 0000000..b2797a1 --- /dev/null +++ b/tests/scenarios/ldap.yml @@ -0,0 +1,11 @@ +--- +squid_auth_method: ldap +squid_mode: both +squid_ssl_bump: true +squid_ldap_server: dc01.lab.tips-of-mine.internal +squid_ldap_base_dn: "OU=Utilisateurs,DC=lab,DC=tips-of-mine,DC=internal" +squid_ldap_bind_dn: "CN=svc-squid,OU=Services,DC=lab,DC=tips-of-mine,DC=internal" +squid_ldap_bind_password: ci-only +squid_ldap_group_acl: true +squid_ldap_group_base_dn: "OU=Groupes,DC=lab,DC=tips-of-mine,DC=internal" +squid_ldap_allowed_groups: [GG_Proxy_Internet] diff --git a/tests/scenarios/minimal.yml b/tests/scenarios/minimal.yml new file mode 100644 index 0000000..9bb7bff --- /dev/null +++ b/tests/scenarios/minimal.yml @@ -0,0 +1,7 @@ +--- +squid_auth_method: none +squid_mode: explicit +squid_ssl_bump: false +squid_filter_mode: blocklist +squid_denied_domains: [] +squid_no_bump_domains: [] diff --git a/tests/scenarios/none.yml b/tests/scenarios/none.yml new file mode 100644 index 0000000..de91369 --- /dev/null +++ b/tests/scenarios/none.yml @@ -0,0 +1,6 @@ +--- +squid_auth_method: none +squid_mode: both +squid_ssl_bump: true +squid_filter_mode: blocklist +squid_denied_domains: [.exemple-interdit.fr] diff --git a/tests/servers-ci/SRV01.yml b/tests/servers-ci/SRV01.yml new file mode 100644 index 0000000..5a01a7f --- /dev/null +++ b/tests/servers-ci/SRV01.yml @@ -0,0 +1,11 @@ +--- +name: SRV01 +env: prod +description: Serveur de test CI +sources: [10.0.4.10] +groups: [windows] +rules: + - domains: [".datadoghq.com", "*.chinacloudapi.cn", "www.java.com"] + ports: [https] + - domains: [sftp.exemple.fr] + ports: [sftp, "1022", "2222"] diff --git a/tests/servers-ci/SRV02.yml b/tests/servers-ci/SRV02.yml new file mode 100644 index 0000000..8c8f3c0 --- /dev/null +++ b/tests/servers-ci/SRV02.yml @@ -0,0 +1,8 @@ +--- +name: SRV02 +env: preprod +description: Second serveur de test, partage une destination avec SRV01 +sources: [10.0.4.11] +rules: + - domains: [www.java.com, il-apigateway.exemple.net] + ports: [https, "8443"]