Private
Public Access
2
0

First version in Galaxy

This commit is contained in:
2026-01-21 15:29:56 +03:00
parent ebe4f15c0f
commit b923810fe3
79 changed files with 235 additions and 299 deletions

199
seb4itik/byow/README.md Normal file
View File

@@ -0,0 +1,199 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
Ce rôle Ansible déploie un WAF basé sur Caddy, Coraza et les OWASP Core Security Rules.
## Principes
### Go
Caddy, Coraza, le module Coraza pour Caddy et l'outil `xcaddy` sont
développés avec le langage Go.
L'outil `xcaddy` a besoin d'une version récente des outils de
développement Go. C'est pourquoi le rôle installe Go depuis
le site de téléchargement officiel et non via le gestionnaire
de paquets de la distribution Linux.
Go est installé dans le répertoire `/usr/local/go-1.25.5/go` et
un lien symbolique est créé de `/usr/local/bin/go` vers
`/usr/local/go-1.25.5/go/bin/go`.
### Caddy
Caddy est un serveur Web et reverse proxy HTTP puissant et versatile.
Étant développé en Go, son déploiement prend la forme d'un simble
binaire exécutable qui contient toutes les librairies requises. Ce
programme est construit avec l'outil `xcaddy` puis copié dans le
fichier `/usr/local/bin/caddy`. La ligne de commande utilisée pour
construire ce binaire est enregsitrée dans le fichier
`/usr/local/bin/.caddy.buil`.
Le fichier de configuration de caddy utilise le format `Caddyfile`.
Il est généré automatiquement à partir des paramètres du rôle puis
enregistré sous `/etc/caddy/Caddyfile`.
### Coraza
Coraza est directement inclus dans le binaire `caddy`
### Module Coraza pour Caddy
### xcaddy
qui permet de lier tous les modules Go requis
pour le WAF
### Plugins OWASP CRS
(À ne pas confondre avec les plugins Coraza.)
Voir: https://github.com/coreruleset/plugin-registry
- name: "drupal-rule-exclusions"
- name: "wordpress-rule-exclusions"
- name: "nextcloud-rule-exclusions"
- name: "dokuwiki-rule-exclusions"
- name: "phpmyadmin-rule-exclusions"
- name: "roundcube-rule-exclusions"
- name: "sogo-rule-exclusions"
## Paramètres du rôle Ansible
## TODO
Fonctionnalités déjà implémentées:
- Déploiement et configuration avec Ansible
- Serveurs transient
- VIP (avec Keepalived)
- DNS dynamique (via Nomad)
- Cluster via Nomad
- OWASP CRS
- Multisite (avec configuration complète des CRS par site)
- Alias avec redirection optionnelle vers nom canonique
- Redirection HTTP vers HTTPS
- Certificats SSL (personnalisés automatiques via Let's Encrypt)
- Filtrage (blacklists) par:
- adresses IP
- réseaux IP
- Antibot par:
- défi JavaScript
- Passive backend checks
- Compression Gzip et zstd
- Mode détection sans blocage par les CRS
- Filtrage par:
- méthode HTTP
- En-têtes de sécurité:
- HSTS
- CSP
- X-Frame-Options
- X-Content-Type-Options
- Referrer Policy
- Permissions Policy
- Keep Headers
- Cookie Flags
- Custom headers
- suppression d'en-têtes
- Collecte des logs via OVH LDP
Urgemment:
- Connexion à consul, ACL consul
- Collection Ansible
- Revérifier gestion CORS
- Active backend checks
- Correction de la collecte des logs via OVH LDP
Fomctionnalités à ajouter:
- Rate limiting distribué
- Antibot par:
- Cookie
- Captcha
- reCAPTCHA
- hCaptcha
- Turnstile
- mCaptcha
- Filtrage par:
- rDNS
- DNSBL
- ASN
- User-Agent (robots, crawlers, IA bots)
- taille
- URI patterns
- pays
- (ip:danmeuk-tor-exit, ua:mitchellkrogza-bad-user-agents, /etc/hapee-1.9/blacklist.acl)
- Client cache
- Proxy cache
- Compression Brotli
- Reverse scan
- Robots.txt (DarkVisitors API/Community Lists/Custom URLs/Manual Rules)
- Gestion des sessions
- Whitelist
- Bad behavior (via Caddy events ?)
- Authentification
- Anti DDoS
- Greylist
- HTML injection
- Metrics (Elasticsearch + Metabase)
- Static files et FastCGI pour PHP (est-ce une bonne idée ?!)
- Crowdsec
- https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/_generator_lists/bad-user-agents.list
## Ressources
- [Documentation officielle Caddy](https://caddyserver.com/docs/)
- [Site Officiel de Coraza](https://coraza.io)
- [Coraza WAF Caddy module](https://github.com/corazawaf/coraza-caddy)
- [Documentation AWOSP CRS](https://coreruleset.org/docs/)
- https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-(v3.x)
Modules
- []()
- []()
### Installation d'Ansible avec pipx
1. Désinstaller *Ansible* et *ansible-lint* s'ils sont déjà installés. Par
exemple avec *Homebrew*:
```
brew uninstall ansible ansible-lint
```
1. Installer *pipx*. Par exemple avec *Homebrew*:
```
brew install pipx
```
1. Inscrire le chemin de recherche des exécutables de *pipx* dans les
scripts d'ouverture de session (fermer le shell et en ouvrir un nouveau
pour activer ce chemin de recherche):
```
pipx ensurepath
```
1. Installer *Ansible* (version 12, et au moins version core 2.16.9, pour
être compatible avec *Mitogen*) dans un environnement isolé (qui sera
nommé `ansible`) puis injecter *ansible-lint* et les librairies requises
dans cet environnement:
```
pipx install ansible==12.3.0 --include-deps
pipx inject ansible ansible-lint --include-apps
pipx inject ansible requests
pipx inject ansible pip-system-certs
pipx inject ansible passlib # pour |password_hash sur macOS (et autres)
pipx inject ansible jmespath # pour |query_result
pipx inject ansible pytz # pour plugin inventory auto
pipx inject ansible mitogen # optionnel, pour utiliser Mitogen
```
1. Vérifier l'environnement:
```
pipx list --include-injected
ansible --version -> ansible [core 2.19.5]
ansible-lint --version -> ansible-lint 26.1.1
```

15
seb4itik/byow/galaxy.yml Normal file
View File

@@ -0,0 +1,15 @@
namespace: "seb4itik"
name: "byow"
version: "0.1.0"
readme: "README.md"
authors:
- "S. Namèche <sebastien@itik.fr>"
description: "Build Your Own WAF"
license:
- "BSD-4-Clause"
tags: ['waf']
dependencies: {}
repository: "https://lab.itik.fr/itik/bgwaf"
documentation: "https://lab.itik.fr/itik/bgwaf/src/branch/main/README.md"
homepage: "https://lab.itik.fr/itik/bgwaf"
issues: "https://lab.itik.fr/itik/bgwaf/issues"

View File

@@ -0,0 +1 @@
requires_ansible: '>=2.19.5'

View File

@@ -0,0 +1,31 @@
# Collections Plugins Directory
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
would contain module utils and modules respectively.
Here is an example directory of the majority of plugins currently supported by Ansible:
```
└── plugins
├── action
├── become
├── cache
├── callback
├── cliconf
├── connection
├── filter
├── httpapi
├── inventory
├── lookup
├── module_utils
├── modules
├── netconf
├── shell
├── strategy
├── terminal
├── test
└── vars
```
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.19/plugins/plugins.html).

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Caddy

View File

@@ -0,0 +1,408 @@
# Defaults
caddy_config_only: false
caddy_go_version: "1.25.5"
caddy_xcaddy_version: "0.4.5"
caddy_caddy_version: "2.10.2"
caddy_coraza_caddy_version: "2.1.0" # Coraza v3.3.3
caddy_owasp_crs_version: "4.22.0"
caddy_system_user: "byow"
caddy_system_group: "byow"
caddy_home_dir: "/opt/{{ caddy_system_user }}"
caddy_config_dir: "/etc/{{ caddy_system_user }}"
caddy_log_dir: "/var/log/{{ caddy_system_user }}"
# Cf.: https://github.com/coreruleset/plugin-registry
caddy_crs_plugins:
- name: "drupal-rule-exclusions"
version: "1.0.0"
- name: "wordpress-rule-exclusions"
version: "1.2.0"
- name: "nextcloud-rule-exclusions"
version: "1.5.0"
- name: "dokuwiki-rule-exclusions"
version: "1.0.0"
- name: "phpmyadmin-rule-exclusions"
version: "1.0.0"
has_after_config: true
- name: "roundcube-rule-exclusions"
version: "1.0.4"
provider: "EsadCetiner"
- name: "sogo-rule-exclusions"
version: "1.0.4"
provider: "EsadCetiner"
# Required unless not site.custom_cert
caddy_email: null
# Required if site.bot_barrier
caddy_bot_barrier_secret: null
# Optionnal
caddy_local_ca_name: null
caddy_ca_root: null
caddy_sites: []
# - name: ""
# site: ""
# paths:
# - path: "*"
# addrs:
# - ""
# blacklist:
# - ""
# aliases:
# - ""
# crs_exceptions:
# before_request:
# - ""
# after_response:
# - ""
caddy_waf_defaults:
redirect_from_aliases: true
custom_cert: false
custom_cert_file: ""
custom_key_file: ""
# Security headers
header_content_security_policy:
# https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#detailed-csp-directives
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy#directives
## Fetch directives
child-src: "'self'"
connect-src: "'self'"
font-src: "'self'"
img-src: "'self'"
manifest-src: "'self'"
media-src: "'self'"
# Deprecated: prefetch-src: "'self'"
object-src: "'none'"
script-src: "'none'"
script-src-attr: "'none'"
script-src-elem: "'none'"
style-src: "'self'"
style-src-attr: "'self'"
style-src-elem: "'self'"
default-src: "'self'"
## Document directives
# base-uri
# plugin-types
# sandbox
## Navigation directives
form-action: "'self'"
frame-ancestors: "'self'"
# https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-src
frame-src: "'self'"
# https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src
worker-src: "'self'"
# https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/block-all-mixed-content
block-all-mixed-content:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/fenced-frame-src
fenced-frame-src: "'self'"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/upgrade-insecure-requests
upgrade-insecure-requests:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy
header_cross_origin_embedder_policy: "require-corp"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy
header_cross_origin_opener_policy: "same-origin"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy
header_cross_origin_resource_policy: "same-site"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy
header_permissions_policy:
accelerometer: "()"
ambient-light-sensor: "()"
aria-notify: "()"
attribution-reporting: "()"
autoplay: "()"
bluetooth: "()"
browsing-topics: "()"
camera: "()"
captured-surface-control: "()"
compute-pressure: "()"
cross-origin-isolated: "()"
deferred-fetch: "()"
deferred-fetch-minimal: "()"
display-capture: "()"
encrypted-media: "()"
fullscreen: "()"
gamepad: "()"
geolocation: "()"
gyroscope: "()"
hid: "()"
identity-credentials-get: "()"
idle-detection: "()"
language-detector: "()"
local-fonts: "()"
magnetometer: "()"
microphone: "()"
midi: "()"
on-device-speech-recognition: "()"
otp-credentials: "()"
payment: "()"
picture-in-picture: "()"
private-state-token-issuance: "()"
private-state-token-redemption: "()"
publickey-credentials-create: "()"
publickey-credentials-get: "()"
screen-wake-lock: "()"
serial: "()"
speaker-selection: "()"
storage-access: "()"
translator: "()"
summarizer: "()"
usb: "()"
web-share: "()"
window-management: "()"
xr-spatial-tracking: "()"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#floc-federated-learning-of-cohorts
interest-cohort: "()"
# ???
battery: "()"
document-domain: "()"
execution-while-not-rendered: "()"
execution-while-out-of-viewport: "()"
navigation-override: "()"
# https://www.geeksforgeeks.org/computer-networks/http-headers-feature-policy/
header_feature_policy:
accelerometer: "'none'"
ambient-light-sensor: "'none'"
autoplay: "'none'"
battery: "'none'"
camera: "'none'"
display-capture: "'none'"
document-domain: "'none'"
encrypted-media: "'none'"
fullscreen: "'none'"
geolocation: "'none'"
gyroscope: "'none'"
layout-animation: "'none'"
legacy-image-formats: "'none'"
magnetometer: "'none'"
microphone: "'none'"
midi: "'none'"
oversized-images: "'none'"
payment: "'none'"
picture-in-picture: "'none'"
publickey-credentials-get: "'none'"
sync-xhr: "'none'"
usb: "'none'"
screen-wake-lock: "'none'"
xr-spatial-tracking: "'none'"
# ???
execution-while-not-rendered: "'none'"
execution-while-out-of-viewport: "'none'"
navigation-override: "'none'"
speaker-selection: "'none'"
unoptimized-images: "'none'"
unsized-media: "'none'"
web-share: "'none'"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy
header_referrer_policy: "strict-origin-when-cross-origin"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html
header_strict_transport_security: "max-age=2635200; includeSubDomains"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
header_x_content_type_options: "nosniff"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options
header_x_frame_options: "DENY"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-robots-tag
header_x_robots_tag: "noindex, nofollow"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#content-type
default_content_type: "text/html; charset=UTF-8"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#set-cookie
cookies_attributes: "SameSite=None; Secure;"
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-dns-prefetch-control
# FIXME
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#server
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-aspnet-version
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-aspnetmvc-version
# https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-powered-by
delete_headers:
- "Server"
- "Via"
- "X-AspNet-Version"
- "X-AspNetMvc-Version"
- "X-Powered-By"
custom_headers: {}
# - name: <string>
# value: <string>
# caddy-ratelimit
rate_events: 1000
rate_window: "1m"
# steffenbusch/caddy-bot-barrier
bot_barrier: false
# corazawaf/coraza-caddy
enable_crs: true
crs_plugins: []
log_only: false
paranoia_level: 1
detection_paranoia_level: null
enforce_bodyproc_urlencoded: 1
critical_anomaly_score: 5
error_anomaly_score: 4
warning_anomaly_score: 3
notice_anomaly_score: 2
inbound_anomaly_score_threshold: 5
outbound_anomaly_score_threshold: 4
reporting_level: 4
max_num_args: 255
arg_name_length: 100
arg_length: 1000
total_arg_length: 64000
max_file_size: 10485760 # 10 Mo
combined_file_sizes: 20970520 # 20 Mo
sampling_percentage: 100
allowed_methods:
- "GET"
- "HEAD"
- "POST"
- "OPTIONS"
allowed_request_content_type:
- "|application/csp-report|"
- "|application/json|"
- "|application/reports+json|"
- "|application/soap+xml|"
- "|application/x-www-form-urlencoded|"
- "|application/xml|"
- "|multipart/form-data|"
- "|text/xml|"
restricted_extensions:
- ".ani/"
- ".asa/"
- ".asax/"
- ".ascx/"
- ".back/"
- ".backup/"
- ".bak/"
- ".bat/"
- ".bck/"
- ".bk/"
- ".bkp/"
- ".cdx/"
- ".cer/"
- ".cfg/"
- ".cmd/"
- ".cnf/"
- ".com/"
- ".compositefont/"
- ".conf/"
- ".config/"
- ".copy/"
- ".crt/"
- ".cs/"
- ".csproj/"
- ".csr/"
- ".dat/"
- ".db/"
- ".dbf/"
- ".dist/"
- ".dll/"
- ".dos/"
- ".dpkg-dist/"
- ".drv/"
- ".gadget/"
- ".hta/"
- ".htr/"
- ".htw/"
- ".ida/"
- ".idc/"
- ".idq/"
- ".inc/"
- ".inf/"
- ".ini/"
- ".jse/"
- ".key/"
- ".licx/"
- ".lnk/"
- ".log/"
- ".mdb/"
- ".msc/"
- ".ocx/"
- ".old/"
- ".pass/"
- ".pdb/"
- ".pem/"
- ".pfx/"
- ".pif/"
- ".pol/"
- ".prf/"
- ".printer/"
- ".pwd/"
- ".rdb/"
- ".rdp/"
- ".reg/"
- ".resources/"
- ".resx/"
- ".sav/"
- ".save/"
- ".scr/"
- ".sct/"
- ".sh/"
- ".shs/"
- ".sql/"
- ".sqlite/"
- ".sqlite3/"
- ".swp/"
- ".sys/"
- ".temp/"
- ".tlb/"
- ".tmp/"
- ".vb/"
- ".vbe/"
- ".vbproj/"
- ".vbs/"
- ".vsdisco/"
- ".vxd/"
- ".webinfo/"
- ".ws/"
- ".wsc/"
- ".wsf/"
- ".wsh/"
- ".xsd/"
- ".xsx/"
restricted_headers_basic:
- "/content-encoding/"
- "/content-range/"
- "/expect/"
- "/if/"
- "/lock-token/"
- "/proxy/"
- "/x-http-method-override/"
- "/x-http-method/"
- "/x-method-override/"
- "/x-middleware-subrequest/"
restricted_headers_extended:
- "/accept-charset/"
allowed_http_versions:
- "HTTP/1.0"
- "HTTP/1.1"
- "HTTP/2"
- "HTTP/2.0"
- "HTTP/3"
- "HTTP/3.0"
allowed_request_content_type_charset:
- "|utf-8|"
- "|iso-8859-1|"
- "|iso-8859-15|"
- "|windows-1252|"

View File

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Check</title>
<style nonce="{{ .CSPNonce }}">
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-size: 2em;
margin: 0.4em 0 0.3em;
}
p {
font-size: 1.2em;
margin: 0.6em 0;
}
.spinner {
margin: 2em 0;
width: 50px;
height: 50px;
border: 5px solid #ccc;
border-top: 5px solid #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="wrapper">
<!-- English Section -->
<h1>Just a moment...</h1>
<p>A small mathematical challenge is running<br>
in the background - fully automatic to confirm<br>
you're not a bot 🔒.</p>
<!-- Spinner -->
<div class="spinner"></div>
<p>⏳ You'll be redirected shortly.</p>
</div>
<script nonce="{{ .CSPNonce }}">{{ .Script }}</script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
#!/usr/bin/python
class FilterModule(object):
def filters(self):
return {
'dict2str': self.dict2str
}
def dict2str(self, d, sep1='=', sep2=';', sep3=''):
return sep2.join(
[ str(k) + (sep1+str(v) if v!=None and v!='' else '') for k, v in d.items() ]
) + sep3
if __name__ == "__main__":
# Tests
filters = FilterModule()
print(filters.filters())
print(filters.dict2str(
{"un": 1, "deux": 2, "str": "a string", "rien": None, "bool": True}
))

View File

@@ -0,0 +1,4 @@
- name: Restart Caddy
ansible.builtin.service:
name: "{{ caddy_system_user }}"
state: "restarted"

View File

@@ -0,0 +1,73 @@
- name: Create sites config directories
loop: "{{ caddy_sites }}"
ansible.builtin.file:
path: "{{ caddy_config_dir }}/sites/{{ item.name }}"
state: "directory"
group: "{{ caddy_system_group }}"
mode: "0750"
- name: Generate sites coraza.conf config file
loop: "{{ caddy_sites }}"
loop_control:
loop_var: "site"
ansible.builtin.template:
src: "templates/coraza.conf"
dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/coraza.conf"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"
- name: Generate sites crs-setup config file
loop: "{{ caddy_sites }}"
loop_control:
loop_var: "site"
ansible.builtin.template:
src: "templates/crs-setup.conf"
dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/crs-setup.conf"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"
- name: Generate sites exclusions before request file
loop: "{{ caddy_sites }}"
loop_control:
loop_var: "site"
ansible.builtin.template:
src: "templates/exclusions-request-before.conf"
dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-request-before.conf"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"
- name: Generate sites exclusions after response file
loop: "{{ caddy_sites }}"
loop_control:
loop_var: "site"
ansible.builtin.template:
src: "templates/exclusions-response-after.conf"
dest: "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-response-after.conf"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"
- name: Generate Caddyfile
ansible.builtin.template:
src: "templates/Caddyfile"
dest: "{{ caddy_config_dir }}/Caddyfile"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"
- name: Copy bot barrier template
ansible.builtin.copy:
src: "files/bot_barrier_template.html"
dest: "{{ caddy_config_dir }}/bot_barrier_template.html"
group: "{{ caddy_system_group }}"
mode: "0640"
notify:
- "Restart Caddy"

View File

@@ -0,0 +1,135 @@
- name: Remove packaged Go
ansible.builtin.apt:
name:
- "golang"
state: "absent"
autoremove: true
purge: true
- name: Install requirements
ansible.builtin.apt:
name:
- "git"
- "libnss3-tools"
- "ssl-cert"
- name: Create Go directory
ansible.builtin.file:
path: "/usr/local/go-{{ caddy_go_version }}"
state: "directory"
mode: "0755"
- name: Install Go
ansible.builtin.unarchive:
src: "https://go.dev/dl/go{{ caddy_go_version }}.linux-amd64.tar.gz"
dest: "/usr/local/go-{{ caddy_go_version }}"
remote_src: true
- name: Link Go
ansible.builtin.file:
src: "/usr/local/go-{{ caddy_go_version }}/go/bin/go"
dest: "/usr/local/bin/go"
state: "link"
- name: Create xcaddy directory
ansible.builtin.file:
path: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}"
state: "directory"
mode: "0755"
- name: Install xcaddy
ansible.builtin.unarchive:
src: "{{ caddy_xcaddy_download }}/v{{ caddy_xcaddy_version }}/xcaddy_{{ caddy_xcaddy_version }}_linux_amd64.tar.gz"
dest: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}"
remote_src: true
- name: Link xcaddy
ansible.builtin.file:
src: "/usr/local/xcaddy-{{ caddy_xcaddy_version }}/xcaddy"
dest: "/usr/local/bin/xcaddy"
state: "link"
- name: Create Caddy build command
ansible.builtin.set_fact:
caddy_build_command: >
xcaddy build v{{ caddy_caddy_version }}
--with github.com/pteich/caddy-tlsconsul
--with github.com/corazawaf/coraza-caddy/v2@v{{ caddy_coraza_caddy_version }}
--with github.com/mholt/caddy-ratelimit
--with github.com/steffenbusch/caddy-bot-barrier
--output /usr/local/bin/caddy
- name: Test Caddy version
ansible.builtin.copy:
dest: "/usr/local/bin/.caddy.build"
content: "{{ caddy_build_command }}"
mode: "0644"
register: caddy_test_caddy_version
- name: Build Caddy
# noqa: no-handler
when: "caddy_test_caddy_version.changed"
ansible.builtin.command:
cmd: "{{ caddy_build_command }}"
creates: "/usr/local/bin/caddy"
notify:
- "Restart Caddy"
- name: Create Caddy system group
ansible.builtin.group:
name: "{{ caddy_system_group }}"
system: true
- name: Create Caddy system user
ansible.builtin.user:
name: "{{ caddy_system_user }}"
group: "{{ caddy_system_group }}"
groups: ["ssl-cert"]
password: "!"
system: true
home: "{{ caddy_home_dir }}"
- name: Create Caddy config directories
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
group: "{{ caddy_system_group }}"
mode: "0750"
loop:
- "{{ caddy_config_dir }}"
- "{{ caddy_config_dir }}/crs-plugins"
- "{{ caddy_config_dir }}/sites"
- name: Create Caddy log dir
ansible.builtin.file:
path: "{{ caddy_log_dir }}"
state: "directory"
group: "{{ caddy_system_group }}"
mode: "0770"
- name: Install OWASP CRS
ansible.builtin.unarchive:
src: "https://github.com/coreruleset/coreruleset/archive/refs/tags/v{{ caddy_owasp_crs_version }}.tar.gz"
dest: "{{ caddy_config_dir }}"
group: "{{ caddy_system_group }}"
remote_src: true
- name: Install plugins
loop: "{{ caddy_crs_plugins }}"
loop_control:
loop_var: "plugin"
ansible.builtin.include_tasks: "plugin_install.yml"
- name: Deploy caddy service file
ansible.builtin.template:
src: "templates/caddy.service"
dest: "/etc/systemd/system/{{ caddy_system_user }}.service"
mode: "0644"
notify:
- "Restart Caddy"
- name: Enable Caddy
ansible.builtin.service:
name: "{{ caddy_system_user }}"
enabled: true
daemon_reload: true

View File

@@ -0,0 +1,6 @@
- name: Install Caddy
when: "not(caddy_config_only)"
ansible.builtin.include_tasks: "caddy_install.yml"
- name: Config Caddy
ansible.builtin.include_tasks: "caddy_config.yml"

View File

@@ -0,0 +1,55 @@
- name: Download plugin {{ plugin.name }}
ansible.builtin.get_url:
url: "https://github.com/{{ plugin.provider | default('coreruleset') }}/{{ plugin.name }}-plugin/archive/refs/tags/v{{ plugin.version }}.tar.gz"
dest: "/usr/local/src/"
mode: "0644"
register: "caddy_plugin_downloaded"
- name: Deploy plugin {{ plugin.name }}
# noqa: no-handler
when: "caddy_plugin_downloaded.changed"
block:
- name: Get temp dir {{ plugin.name }}
ansible.builtin.tempfile:
state: "directory"
register: "caddy_temp_plugin_dir"
- name: Unarchive plugin {{ plugin.name }}
ansible.builtin.unarchive:
src: "/usr/local/src/{{ plugin.name }}-plugin-{{ plugin.version }}.tar.gz"
dest: "{{ caddy_temp_plugin_dir.path }}"
remote_src: true
- name: Copy plugin files {{ plugin.name }}
ansible.builtin.copy:
src: "{{ caddy_temp_plugin_dir.path }}/{{ plugin.name }}-plugin-{{ plugin.version }}/plugins/"
dest: "{{ caddy_config_dir }}/crs-plugins/"
mode: "0644"
# directory_mode: "0750"
group: "{{ caddy_system_group }}"
remote_src: true
notify:
- "Restart Caddy"
- name: Create after config {{ plugin.name }}
when: "not(plugin.has_after_config | default(false))"
ansible.builtin.copy:
content: ""
dest: "{{ caddy_config_dir }}/crs-plugins/{{ plugin.name }}-after.conf"
mode: "0644"
group: "{{ caddy_system_group }}"
# FIXME: bad hack, why is it required?
- name: Adjust plugins diretory permissions
ansible.builtin.file:
path: "{{ caddy_config_dir }}/crs-plugins"
state: "directory"
group: "{{ caddy_system_group }}"
mode: "0750"
- name: Delete temp dir {{ plugin.name }}
when: "caddy_temp_plugin_dir.path is defined"
ansible.builtin.file:
path: "{{ caddy_temp_plugin_dir.path }}"
state: "absent"

View File

@@ -0,0 +1,154 @@
{
grace_period 10s
order coraza_waf first
{% if caddy_email | default(None) != None %}
email "{{ caddy_email }}"
{% endif %}
{% if caddy_ca_root | default(None) != None %}
tls {
ca_root "{{ caddy_ca_root }}"
}
{% endif %}
log waf {
format json
include "http.handlers.waf"
output file {{ caddy_log_dir }}/waf.log
}
{% if caddy_local_ca_name | default(None) != None %}
pki {
ca local {
name "{{ caddy_local_ca_name }}"
}
}
{% endif %}
}
{% for site in caddy_sites %}
{% if (site.redirect_from_aliases | default(true)) and (site.aliases | default([]) | length) > 0 %}
{{ site.aliases | join(', ') }} {
redir https://{{ site.site }}{uri}
}
{% else %}
{% for alias in site.aliases | default([]) %}
{{ alias }},
{% endfor %}
{% endif %}
{{ site.site }} {
encode gzip zstd
{% for path in site.paths %}
handle {{ path.path | default('*') }} {
reverse_proxy {
to {{ path.addrs | join(' ') }}
header_up X-Real-IP {remote}
lb_policy ip_hash
# Active checks
# health_uri /
# health_interval 10s
# health_timeout 5s
# health_status 200
# Passive checks
fail_duration 30s
max_fails 3
unhealthy_latency 2000ms
transport http {
# tls_server_name {host}
# tls_insecure_skip_verify
}
}
}
{% endfor %}
{% if site.custom_cert | default (false) %}
tls {{ site.custom_cert_file }} {{ site.custom_key_file }}
{% endif %}
{% if site.blacklist | default([]) | length > 0 %}
@blocked {
{% for ip in site.blacklist | default([]) %}
remote_ip {{ ip }}
{% endfor %}
}
respond @blocked "Access Denied" 403
{% endif %}
{% if site.bot_barrier | default (false) %}
bot_barrier {
secret {{ caddy_bot_barrier_secret }}
complexity 18
valid_for 30m
seed_cookie_name __chall_{{ site.name }}_seed
solution_cookie_name __chall_{{ site.name }}_solution
mac_cookie_name __chall_{{ site.name }}_mac
template {{ caddy_config_dir }}/bot_barrier_template.html
}
{% endif %}
rate_limit {
# distributed
zone remote_ip {
key {remote.ip}
events {{ site.rate_events | default('1000') }}
window {{ site.rate_window | default('1m') }}
}
}
{% if site.enable_crs | default(caddy_waf_defaults.enable_crs) %}
coraza_waf {
load_owasp_crs
directives `
Include "{{ caddy_config_dir }}/sites/{{ site.name }}/coraza.conf"
Include "{{ caddy_config_dir }}/sites/{{ site.name }}/crs-setup.conf"
{% for plugin in site.crs_plugins | default(caddy_waf_defaults.crs_plugins) %}
Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-config.conf"
Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-before.conf"
{% endfor %}
Include "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-request-before.conf"
Include "{{ caddy_config_dir }}/coreruleset-{{ caddy_owasp_crs_version }}/rules/*.conf"
Include "{{ caddy_config_dir }}/sites/{{ site.name }}/exclusions-response-after.conf"
{% for plugin in site.crs_plugins | default(caddy_waf_defaults.crs_plugins) %}
Include "{{ caddy_config_dir }}/crs-plugins/{{ plugin }}-after.conf"
{% endfor %}
SecRuleEngine On
`
}
{% endif %}
header {
Content-Security-Policy "{{ caddy_waf_defaults.header_content_security_policy | combine(site.header_content_security_policy | default({})) | dict2str(sep1=' ', sep2='; ', sep3=';') }}"
Cross-Origin-Embedder-Policy "{{ site.header_cross_origin_embedder_policy | default(caddy_waf_defaults.header_cross_origin_embedder_policy) }}"
Cross-Origin-Opener-Policy "{{ site.header_cross_origin_opener_policy | default(caddy_waf_defaults.header_cross_origin_opener_policy) }}"
Cross-Origin-Resource-Policy "{{ site.header_cross_origin_resource_policy | default(caddy_waf_defaults.header_cross_origin_resource_policy) }}"
Feature-Policy "{{ caddy_waf_defaults.header_feature_policy | combine(site.header_feature_policy | default({})) | dict2str(sep1=' ', sep2='; ', sep3=';') }}"
Permissions-Policy "{{ caddy_waf_defaults.header_permissions_policy | combine(site.header_permissions_policy | default({})) | dict2str(sep2=', ') }}"
Referrer-Policy "{{ site.header_referrer_policy | default(caddy_waf_defaults.header_referrer_policy) }}"
Strict-Transport-Security "{{ site.header_strict_transport_security | default(caddy_waf_defaults.header_strict_transport_security) }}"
X-Content-Type-Options "{{ site.header_x_content_type_options | default(caddy_waf_defaults.header_x_content_type_options) }}"
X-Frame-Options "{{ site.header_x_frame_options | default(caddy_waf_defaults.header_x_frame_options) }}"
X-Robots-Tag "{{ site.header_x_robots_tag | default(caddy_waf_defaults.header_x_robots_tag) }}"
?Content-Type "{{ site.default_content_type | default(caddy_waf_defaults.default_content_type) }}"
>Set-Cookie "(.*)" "$1; {{ site.cookies_attributes | default(caddy_waf_defaults.cookies_attributes) }}"
{% for header in site.delete_headers | default(caddy_waf_defaults.delete_headers) %}
-{{ header }}
{% endfor %}
{% for header in site.custom_headers | default(caddy_waf_defaults.custom_headers) %}
{{ header.name }} "{{ header.value }}"
{% endfor %}
}
handle_errors 403 {
header X-Blocked "true"
respond "Your request was blocked."
}
log {
format json
output file {{ caddy_log_dir }}/site_{{ site.name }}.log
}
}
{% endfor %}

View File

@@ -0,0 +1,20 @@
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User={{ caddy_system_user }}
Group={{ caddy_system_group }}
ExecStart=/usr/local/bin/caddy run --environ --config {{ caddy_config_dir }}/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config {{ caddy_config_dir }}/Caddyfile --force
TimeoutStopSec=15s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,158 @@
# -- Rule engine initialization ----------------------------------------------
# Enable Coraza, attaching it to every transaction. Use detection
# only to start with, because that minimises the chances of post-installation
# disruption.
SecRuleEngine DetectionOnly
# -- Request body handling ---------------------------------------------------
# Allow Coraza to access request bodies. If you don't, Coraza
# won't be able to see any POST parameters, which opens a large security
# hole for attackers to exploit.
SecRequestBodyAccess On
# Enable XML request body parser.
# Initiate XML Processor in case of xml content-type
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
# Enable JSON request body parser.
# Initiate JSON Processor in case of JSON content-type; change accordingly
# if your application does not use 'application/json'
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
# Enable JSON request body parser for more subtypes.
# Adapt this rule if you want to engage the JSON Processor for "+json" subtypes
SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
"id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
# Maximum request body size we will accept for buffering. If you support
# file uploads, this value must has to be as large as the largest file
# you are willing to accept.
SecRequestBodyLimit 13107200
# Maximum request body size that Coraza will store in memory. If the body
# size exceeds this value, it will be saved to a temporary file on disk.
SecRequestBodyInMemoryLimit 131072
# Maximum request body size we will accept for buffering, with files excluded.
# You want to keep that value as low as practical.
# Note: SecRequestBodyNoFilesLimit is currently NOT supported by Coraza
# SecRequestBodyNoFilesLimit 131072
# What to do if the request body size is above our configured limit.
# Keep in mind that this setting will automatically be set to ProcessPartial
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
# disruptions when initially deploying Coraza.
# Warning: Setting this directive to ProcessPartial introduces a potential bypass
# risk, as attackers could prepend junk data equal to or greater than the inspected body size.
SecRequestBodyLimitAction Reject
# Verify that we've correctly processed the request body.
# As a rule of thumb, when failing to process a request body
# you should reject the request (when deployed in blocking mode)
# or log a high-severity alert (when deployed in detection-only mode).
SecRule REQBODY_ERROR "!@eq 0" \
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
# By default be strict with what we accept in the multipart/form-data
# request body. If the rule below proves to be too strict for your
# environment consider changing it to detection-only.
# Do NOT remove it, as it will catch many evasion attempts.
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:'200003',phase:2,t:none,log,deny,status:400, \
msg:'Multipart request body failed strict validation.'"
# -- Response body handling --------------------------------------------------
# Allow Coraza to access response bodies.
# You should have this directive enabled in order to identify errors
# and data leakage issues.
# Do keep in mind that enabling this directive does increases both
# memory consumption and response latency.
SecResponseBodyAccess On
# Which response MIME types do you want to inspect? You should adjust the
# configuration below to catch documents but avoid static files
# (e.g., images and archives).
SecResponseBodyMimeType text/plain text/html text/xml
# Buffer response bodies of up to 512 KB in length.
SecResponseBodyLimit 524288
# What happens when we encounter a response body larger than the configured
# limit? By default, we process what we have and let the rest through.
# That's somewhat less secure, but does not break any legitimate pages.
SecResponseBodyLimitAction ProcessPartial
# -- Filesystem configuration ------------------------------------------------
# The location where Coraza will keep its persistent data. This default setting
# is chosen due to all systems have /tmp available however, it
# too should be updated to a place that other users can't access.
SecDataDir /tmp/
# -- File uploads handling configuration -------------------------------------
# The location where Coraza stores intercepted uploaded files. This
# location must be private to Coraza. You don't want other users on
# the server to access the files, do you?
#SecUploadDir /opt/coraza/var/upload/
# If On, the WAF will store the uploaded files in the SecUploadDir
# directory.
# Note: SecUploadKeepFiles is currently NOT supported by Coraza
#SecUploadKeepFiles Off
# Uploaded files are by default created with permissions that do not allow
# any other user to access them. You may need to relax that if you want to
# interface Coraza to an external program (e.g., an anti-virus).
# Note: SecUploadFileMode is currently NOT supported by Coraza
#SecUploadFileMode 0600
# -- Debug log configuration -------------------------------------------------
# Default debug log path
# Debug levels:
# 0: No logging (least verbose)
# 1: Error
# 2: Warn
# 3: Info
# 4-8: Debug
# 9: Trace (most verbose)
SecDebugLog {{ caddy_log_dir }}/coraza-debug.log
SecDebugLogLevel 4
# -- Audit log configuration -------------------------------------------------
# Log the transactions that are marked by a rule, as well as those that
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
# level response status codes).
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:(5|4)(0|1)[0-9])$"
# Define which parts of the transaction are going to be recorded in the audit log
SecAuditLogParts ABIJDEFHZ
# Use a single file for logging. This is much easier to look at, but
# assumes that you will use the audit log only occasionally.
SecAuditLogType Serial
# The format used to write the audit log.
# Can be one of JSON|JsonLegacy|Native|OCSF
SecAuditLogFormat JSON
# The following settings are not supported by Coraza
# SecCookieFormat 0
# SecArgumentSeparator &
# SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
# "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
# SecRule TX:/^COR_/ "!@streq 0" \
# "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'"

View File

@@ -0,0 +1,305 @@
## Log destination
SecDefaultAction "phase:1,log,auditlog,pass"
SecDefaultAction "phase:2,log,auditlog,pass"
## Paranoia level
SecAction \
"id:900000,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.blocking_paranoia_level={{ site.paranoia_level | default(caddy_waf_defaults.paranoia_level) }}"
{% if site.detection_paranoia_level | default(caddy_waf_defaults.detection_paranoia_level) != None %}
## Detection paranoia level
SecAction \
"id:900001,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.detection_paranoia_level={{ site.detection_paranoia_level | default(caddy_waf_defaults.detection_paranoia_level) }}"
{% endif %}
## Enforce Body Processor URLENCODED
SecAction \
"id:900010,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.enforce_bodyproc_urlencoded={{ site.enforce_bodyproc_urlencoded | default(caddy_waf_defaults.enforce_bodyproc_urlencoded) }}"
## Anomaly Scoring Mode Severity Levels
SecAction \
"id:900100,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.critical_anomaly_score={{ site.critical_anomaly_score | default(caddy_waf_defaults.critical_anomaly_score) }},\
setvar:tx.error_anomaly_score={{ site.error_anomaly_score | default(caddy_waf_defaults.error_anomaly_score) }},\
setvar:tx.warning_anomaly_score={{ site.warning_anomaly_score | default(caddy_waf_defaults.warning_anomaly_score) }},\
setvar:tx.notice_anomaly_score={{ site.notice_anomaly_score | default(caddy_waf_defaults.notice_anomaly_score) }}"
## Anomaly Scoring Mode Blocking Threshold Levels
SecAction \
"id:900110,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.inbound_anomaly_score_threshold={% if site.log_only | default(caddy_waf_defaults.log_only) %}1000{% else %}{{ site.inbound_anomaly_score_threshold | default(caddy_waf_defaults.inbound_anomaly_score_threshold) }}{% endif %},\
setvar:tx.outbound_anomaly_score_threshold={% if site.log_only | default(caddy_waf_defaults.log_only) %}1000{% else %}{{ site.outbound_anomaly_score_threshold | default(caddy_waf_defaults.outbound_anomaly_score_threshold) }}{% endif %}"
## Application Specific Rule Exclusions
# FIXME
# In CRS 4, these are no longer part of the CRS itself, but they are available
# as "CRS plugins". Some plugins improve support for web applications, and others
# may bring new functionality. Plugins are not installed by default, but can be
# downloaded from the plugin registry:
# https://github.com/coreruleset/plugin-registry
# For detailed information about using and installing plugins, please see:
# https://coreruleset.org/docs/concepts/plugins/
## Anomaly Score Reporting Level
# 0 - Reporting disabled
# 1 - Reporting for requests with a blocking anomaly score >= a threshold
# 2 - Reporting for requests with a detection anomaly score >= a threshold
# 3 - Reporting for requests with a blocking anomaly score greater than 0
# 4 - Reporting for requests with a detection anomaly score greater than 0
# 5 - Reporting for all requests
SecAction \
"id:900115,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.reporting_level={{ site.reporting_level | default(caddy_waf_defaults.reporting_level) }}"
## Early Anomaly Scoring Mode Blocking
SecAction \
"id:900120,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.early_blocking=0"
## Initialize Default Collections
SecAction \
"id:900130,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.enable_default_collections=1"
## HTTP Policy Settings
SecAction \
"id:900200,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.allowed_methods={{ site.allowed_methods | default(caddy_waf_defaults.allowed_methods) | join(" ") }}'"
## Content-Types that a client is allowed to send in a request
SecAction \
"id:900220,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.allowed_request_content_type={{ site.allowed_request_content_type | default(caddy_waf_defaults.allowed_request_content_type) | join(" ") }}'"
## Allowed HTTP versions
SecAction \
"id:900230,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.allowed_http_versions={{ site.allowed_http_versions | default(caddy_waf_defaults.allowed_http_versions) | join(" ") }}'"
## Forbidden file extensions
SecAction \
"id:900240,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.restricted_extensions={{ site.restricted_extensions | default(caddy_waf_defaults.restricted_extensions) | join(" ") }}'"
## Restricted request headers
SecAction \
"id:900250,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.restricted_headers_basic={{ site.restricted_headers_basic | default(caddy_waf_defaults.restricted_headers_basic) | join(" ") }}'"
## Extended restricted request headers (forbidden at a higher paranoia level)
SecAction \
"id:900255,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.restricted_headers_extended={{ site.restricted_headers_extended | default(caddy_waf_defaults.restricted_headers_extended) | join(" ") }}'"
## Content-Types charsets that a client is allowed to send in a request
SecAction \
"id:900280,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:'tx.allowed_request_content_type_charset={{ caddy_waf_defaults.allowed_request_content_type_charset | default(caddy_waf_defaults.allowed_request_content_type_charset) | join(" ") }}'"
## Block request if number of arguments is too high
SecAction \
"id:900300,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.max_num_args={{ site.max_num_args | default(caddy_waf_defaults.max_num_args) }}"
## Block request if the length of any argument name is too high
SecAction \
"id:900310,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.arg_name_length={{ site.arg_name_length | default(caddy_waf_defaults.arg_name_length) }}"
## Block request if the length of any argument value is too high
SecAction \
"id:900320,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.arg_length={{ site.arg_length | default(caddy_waf_defaults.arg_length) }}"
## Block request if the total length of all combined arguments is too high
SecAction \
"id:900330,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.total_arg_length={{ site.total_arg_length | default(caddy_waf_defaults.total_arg_length) }}"
## Block request if the file size of any individual uploaded file is too high
SecAction \
"id:900340,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.max_file_size={{ site.max_file_size | default(caddy_waf_defaults.max_file_size) }}"
## Block request if the total size of all combined uploaded files is too high
SecAction \
"id:900350,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.combined_file_sizes={{ site.combined_file_sizes | default(caddy_waf_defaults.combined_file_sizes) }}"
## Easing In / Sampling Percentage
SecAction \
"id:900400,\
phase:1,\
pass,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.sampling_percentage={{ site.sampling_percentage | default(caddy_waf_defaults.sampling_percentage) }}"
## Check UTF-8 encoding
SecAction \
"id:900950,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.crs_validate_utf8_encoding=1"
## Skip Checking Responses
# CRS will perform analysis of the response contents if this is enabled and you have
# the directive `SecResponseBodyAccess On`.
# Warning: this feature is _enabled_ by default, but depending on your applications
# you might be targeted in a Request Filter Denial of Service (RFDoS) attack.
# References: https://blog.sicuranext.com/response-filter-denial-of-service-a-new-way-to-shutdown-a-website/
# Uncomment this rule to _skip checking responses_.
#SecAction \
# "id:900500,\
# phase:1,\
# pass,\
# t:none,\
# nolog,\
# tag:'OWASP_CRS',\
# ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
# setvar:tx.crs_skip_response_analysis=1"
## End of setup
SecAction \
"id:900990,\
phase:1,\
pass,\
t:none,\
nolog,\
tag:'OWASP_CRS',\
ver:'OWASP_CRS/{{ caddy_owasp_crs_version }}',\
setvar:tx.crs_setup_version={{ caddy_owasp_crs_version | regex_replace('\.', '') }}"

View File

@@ -0,0 +1,3 @@
{% for exclusion in site.crs_exceptions.before_request | default([]) %}
{{ exclusion }}
{% endfor %}

View File

@@ -0,0 +1,3 @@
{% for exclusion in site.crs_exceptions.after_response | default([]) %}
{{ exclusion }}
{% endfor %}

View File

@@ -0,0 +1 @@
caddy_xcaddy_download: "https://github.com/caddyserver/xcaddy/releases/download"

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Common

View File

@@ -0,0 +1,2 @@
common_default_packages: []
common_proxy_server: null

View File

@@ -0,0 +1,7 @@
- name: Setup proxy client
when: "common_proxy_server != None"
ansible.builtin.include_tasks: "setup_proxy_client.yml"
- name: Setup packages
when: "common_default_packages | length > 0"
ansible.builtin.include_tasks: "setup_packages.yml"

View File

@@ -0,0 +1,9 @@
- name: Keep packages up to date
ansible.builtin.apt:
update_cache: true
upgrade: "safe"
- name: Install default packages
ansible.builtin.apt:
name: "{{ common_default_packages }}"
autoclean: true

View File

@@ -0,0 +1,24 @@
- name: Create 00proxy.conf file
ansible.builtin.template:
src: "templates/apt_proxy.conf"
dest: "/etc/apt/apt.conf.d/00proxy.conf"
mode: "644"
owner: "root"
group: "root"
- name: Add http_proxy and https_proxy variable to file /etc/environment
ansible.builtin.lineinfile:
path: "/etc/environment"
regexp: '^{{ item.key }}='
line: '{{ item.key }}="{{ item.value }}"'
create: true
mode: "644"
owner: "root"
group: "root"
with_items:
- key: 'http_proxy'
value: '{{ common_proxy_server }}'
- key: 'https_proxy'
value: '{{ common_proxy_server }}'
- key: 'no_proxy'
value: 'localhost,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16'

View File

@@ -0,0 +1,2 @@
Acquire::http::Proxy "http://{{ common_proxy_server }}";
Acquire::https::Proxy "https://{{ common_proxy_server }}";

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Consul

View File

@@ -0,0 +1,22 @@
# Required
consul_datacenter: null
consul_domain: null
consul_secret_key: null
consul_server: false
consul_servers: []
# Required for registering services
consul_services: []
# Defaults
consul_version: "1.22.2"
consul_key_file: "/etc/ssl/private/server.{{ consul_datacenter }}.{{ consul_domain }}.key"
consul_cert_file: "/etc/ssl/certs/server.{{ consul_datacenter }}.{{ consul_domain }}.crt"
consul_ca_file: "/etc/ssl/certs/local-ca.crt"
consul_client_addr: "0.0.0.0"
consul_bind_addr: "{{ ansible_default_ipv4.address }}"
consul_advertise_addr: "{{ ansible_default_ipv4.address }}"
consul_default_check_interval: "15s"
consul_default_check_timeout: "2s"
consul_acl_bootstraped: false

View File

@@ -0,0 +1,5 @@
- name: Restart Consul
ansible.builtin.service:
name: "consul"
state: "restarted"
daemon_reload: true

View File

@@ -0,0 +1,131 @@
- name: Install requirements
ansible.builtin.apt:
name:
- "unzip"
# Not required for Ubuntu 24, was a test for Debian 13
- name: Disable IPv6 on all interfaces
ansible.posix.sysctl:
name: "net.ipv6.conf.all.disable_ipv6"
value: "1"
sysctl_set: true
- name: Create Consul directory
ansible.builtin.file:
path: "/usr/local/consul-{{ consul_version }}"
state: "directory"
mode: "0755"
- name: Install Consul
ansible.builtin.unarchive:
src: "https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip"
dest: "/usr/local/consul-{{ consul_version }}"
remote_src: true
notify:
- "Restart Consul"
- name: Link Consul
ansible.builtin.file:
src: "/usr/local/consul-{{ consul_version }}/consul"
dest: "/usr/local/bin/consul"
state: "link"
notify:
- "Restart Consul"
- name: Create consul system group
ansible.builtin.group:
name: "consul"
system: true
- name: Create consul system user
ansible.builtin.user:
name: "consul"
group: "consul"
groups: ["ssl-cert"]
password: "!"
system: true
home: "/opt/consul"
- name: Create consul config directory
ansible.builtin.file:
path: "/etc/consul"
state: "directory"
group: "consul"
mode: "0750"
- name: Create consul log dir
ansible.builtin.file:
path: "/var/log/consul"
state: "directory"
group: "consul"
mode: "0770"
- name: Generate Consul config
ansible.builtin.template:
src: "templates/config.json"
dest: "/etc/consul/config.json"
group: "consul"
mode: "0640"
notify:
- "Restart Consul"
- name: Deploy Consul service file
ansible.builtin.template:
src: "templates/consul.service"
dest: "/etc/systemd/system/consul.service"
mode: "0644"
notify:
- "Restart Consul"
- name: Enable and start Consul
ansible.builtin.service:
name: "consul"
state: "started"
enabled: true
daemon_reload: true
# consul acl bootstrap -format=json
# {
# "CreateIndex": 1042,
# "ModifyIndex": 1042,
# "AccessorID": "50f8bfb6-b259-4c08-f9aa-2dc0eeb08e99",
# "SecretID": "b7f16a8a-0b4f-4661-fea2-f79d95dd011b",
# "Description": "Bootstrap Token (Global Management)",
# "Policies": [
# {
# "ID": "00000000-0000-0000-0000-000000000001",
# "Name": "global-management"
# }
# ],
# "Local": false,
# "CreateTime": "2026-01-19T18:41:13.294800712+03:00",
# "Hash": "X2AgaFhnQGRhSSF/h0m6qpX1wj/HJWbyXcxkEM/5GrY="
# }
- name: Register services to consul
loop: "{{ consul_services }}"
community.general.consul_agent_service:
name: "{{ item.name }}"
service_port: "{{ item.port }}"
tags: "{{ item.tags | default([]) }}"
- name: Register HTTP checks to consul
loop: "{{ consul_services }}"
when: "item.check_http | default(None) != None"
community.general.consul_agent_check:
name: "{{ item.name }}_check"
service_id: "{{ item.name }}"
interval: "{{ item.check_interval | default(consul_default_check_interval) }}"
timeout: "{{ item.check_timeout | default(consul_default_check_timeout) }}"
http: "{{ item.check_http }}"
scheme: "{{ item.check_scheme | default('http') }}"
- name: Register TCP checks to consul
loop: "{{ consul_services }}"
when: "item.check_tcp | default(None) != None"
community.general.consul_agent_check:
name: "{{ item.name }}_check"
service_id: "{{ item.name }}"
interval: "{{ item.check_interval | default(consul_default_check_interval) }}"
timeout: "{{ item.check_timeout | default(consul_default_check_timeout) }}"
tcp: "{{ item.check_tcp }}"

View File

@@ -0,0 +1,28 @@
{
"log_level": "INFO",
"domain": "{{ consul_domain }}",
"datacenter": "{{ consul_datacenter | mandatory }}",
"key_file": "{{ consul_key_file }}",
"cert_file": "{{ consul_cert_file }}",
"ca_file": "{{ consul_ca_file }}",
"verify_incoming": true,
"verify_outgoing": true,
"verify_server_hostname": true,
"encrypt": "{{ consul_secret_key }}",
"data_dir": "/opt/consul",
{% if consul_server %}
"node_name": "{{ ansible_hostname }}",
"server": true,
"ui": true,
"leave_on_terminate": true,
"client_addr": "{{ consul_client_addr }}",
"bind_addr": "{{ consul_bind_addr }}",
"advertise_addr": "{{ consul_advertise_addr }}",
"bootstrap_expect": {{ (consul_servers | length) - 1 }},
"enable_syslog": true,
"performance": {
"raft_multiplier": 1
},
{% endif %}
"retry_join": ["{{ consul_servers | join('", "') }}"]
}

View File

@@ -0,0 +1,18 @@
[Unit]
Description="HashiCorp Consul - A service mesh solution"
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/consul/config.json
[Service]
Type=notify
User=consul
Group=consul
ExecStart=/usr/local/bin/consul agent -config-file=/etc/consul/config.json
KillMode=process
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Keepalived

View File

@@ -0,0 +1,16 @@
# Required
keepalived_script_user: null
keepalived_monitored_process: null
keepalived_vrrp_password: null # Max 8 characters
keepalived_vrrp_ips: null
# - master: <string>
# virtual_router_id: <int>
# addr: <string>
# Optionnal
keepalived_alerts_to: []
keepalived_unicast_peers: []
# Required if length keepalived_alerts_to > 0
keepalived_smtp_from: null
keepalived_smtp_relay: null

View File

@@ -0,0 +1,4 @@
- name: Restart keepalived
ansible.builtin.service:
name: "keepalived"
state: "restarted"

View File

@@ -0,0 +1,28 @@
- name: Installation des paquetages
ansible.builtin.apt:
name:
- "keepalived"
notify:
- "Restart keepalived"
- name: Tell kernel to allow binding non-local IP
ansible.posix.sysctl:
name: "net.ipv4.ip_nonlocal_bind"
value: "1"
sysctl_set: true
notify:
- "Restart keepalived"
- name: Generate keepalived.conf file
ansible.builtin.template:
src: "templates/keepalived.conf"
dest: "/etc/keepalived/keepalived.conf"
mode: "0640"
notify:
- "Restart keepalived"
- name: Activate and start keepalived
ansible.builtin.service:
name: "keepalived"
state: "started"
enabled: true

View File

@@ -0,0 +1,58 @@
# FIXME: NOTICE: setting config option max_auto_priority should result in better keepalived performance
global_defs {
enable_script_security
script_user "{{ keepalived_script_user }}"
{% if keepalived_alerts_to | length > 0 %}
notification_email {
{% for dest in keepalived_alerts_to %}
{{ dest }}
{% endfor %}
}
notification_email_from {{ keepalived_smtp_from }}
smtp_server {{ keepalived_smtp_relay }}
smtp_connect_timeout 30
router_id {{ ansible_hostname }}
{% endif %}
}
vrrp_script chk_{{ keepalived_monitored_process }} {
script "/usr/bin/killall -0 {{ keepalived_monitored_process }}" # Check if process is running
interval 2 # Check every 2 seconds
weight 2 # Weight to influence master election
}
{% for ip in keepalived_vrrp_ips %}
# master: {{ ip.master }}
vrrp_instance VI_{{ ip.virtual_router_id }} {
state {{ (ansible_hostname==ip.master) | ternary('MASTER', 'BACKUP') }}
interface {{ ansible_default_ipv4.interface }}
virtual_router_id {{ ip.virtual_router_id }}
priority {{ (ansible_hostname==ip.master) | ternary('101', '100') }}
advert_int 1
promote_secondaries
{% if keepalived_alerts_to | length > 0 %}
smtp_alert true
{% endif %}
authentication {
auth_type PASS
auth_pass {{ keepalived_vrrp_password }}
}
virtual_ipaddress {
{{ ip.addr }}
}
{% if keepalived_unicast_peers | length > 0 %}
unicast_peer {
{% for peer in keepalived_unicast_peers %}
{% if peer != ansible_default_ipv4.address %}
{{ peer }}
{% endif %}
{% endfor %}
}
{% endif %}
track_script {
chk_{{ keepalived_monitored_process }}
}
}
{% endfor %}

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Local CA

View File

@@ -0,0 +1,11 @@
# Defaults
local_ca_key_size: 4096
local_ca_key_type: "RSA"
# Required
local_ca_common_name: null
local_ca_passphrase: null
local_ca_country_name: null
local_ca_locality_name: null
local_ca_organization_name: null
local_ca_email_address: null

View File

@@ -0,0 +1,51 @@
- name: Create CA directory
delegate_to: "localhost"
run_once: true
ansible.builtin.file:
path: "ca"
state: "directory"
mode: "0700"
- name: Create private key with password protection
delegate_to: "localhost"
run_once: true
community.crypto.openssl_privatekey:
path: "ca/ca-certificate.key"
passphrase: "{{ local_ca_passphrase }}"
size: "{{ local_ca_key_size }}"
type: "{{ local_ca_key_type }}"
mode: "0640"
- name: Create certificate signing request (CSR) for CA certificate
delegate_to: "localhost"
run_once: true
community.crypto.openssl_csr:
path: "ca/ca-certificate.csr"
privatekey_path: "ca/ca-certificate.key"
privatekey_passphrase: "{{ local_ca_passphrase }}"
common_name: "{{ local_ca_common_name }}"
use_common_name_for_san: false # since we do not specify SANs, don't use CN as a SAN
country_name: "{{ local_ca_country_name }}"
locality_name: "{{ local_ca_locality_name }}"
organization_name: "{{ local_ca_organization_name }}"
email_address: "{{ local_ca_email_address }}"
basic_constraints:
- "CA:TRUE"
basic_constraints_critical: true
key_usage:
- "keyCertSign"
key_usage_critical: true
return_content: true
register: local_ca_csr
- name: Create self-signed CA certificate from CSR
delegate_to: "localhost"
run_once: true
# noqa: no-handler
when: "local_ca_csr.changed"
community.crypto.x509_certificate:
path: "ca/ca-certificate.crt"
csr_content: "{{ local_ca_csr.csr }}"
privatekey_path: "ca/ca-certificate.key"
privatekey_passphrase: "{{ local_ca_passphrase }}"
provider: "selfsigned"

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Local CA certificates

View File

@@ -0,0 +1,19 @@
# Defaults
local_ca_certs_default_country_name: null
local_ca_certs_default_locality_name: null
local_ca_certs_default_organization_name: null
local_ca_certs_default_email_address: null
local_ca_certs_default_not_after: "+365d" # valid for one year
local_ca_certs_default_not_before: "-1d" # valid since yesterday
# Required
local_ca_certs_ca_passphrase: null
local_ca_certs_list: {}
# - cn: <string>
# domains: <array of strings>
# country_name: <string>
# locality_name: <string>
# organization_name: <string>
# email_address: <string>
# not_after: <string>
# not_before: <string>

View File

@@ -0,0 +1,2 @@
- name: Reboot host
ansible.builtin.reboot:

View File

@@ -0,0 +1,42 @@
- name: Create private key for new certificate
community.crypto.openssl_privatekey:
path: "/etc/ssl/private/{{ cert.cn }}.key"
mode: "0640"
group: "ssl-cert"
- name: Create CSR for new certificate
community.crypto.openssl_csr:
path: "/etc/ssl/certs/{{ cert.cn }}.csr"
privatekey_path: "/etc/ssl/private/{{ cert.cn }}.key"
common_name: "{{ cert.cn }}"
subject_alt_name: "{{ cert.domains }}"
country_name: "{{ cert.country_name | default(local_ca_certs_default_country_name) | mandatory }}"
locality_name: "{{ cert.locality_name | default(local_ca_certs_default_locality_name) | mandatory }}"
organization_name: "{{ cert.organization_name | default(local_ca_certs_default_organization_name) | mandatory }}"
email_address: "{{ cert.email_address | default(local_ca_certs_default_email_address) | mandatory }}"
return_content: true
register: local_ca_certs_csr
- name: Sign certificate with our CA
delegate_to: "localhost"
# noqa: no-handler
when: "local_ca_certs_csr.changed"
community.crypto.x509_certificate_pipe:
csr_content: "{{ local_ca_certs_csr.csr }}"
provider: "ownca"
ownca_path: "ca/ca-certificate.crt"
ownca_privatekey_path: "ca/ca-certificate.key"
ownca_privatekey_passphrase: "{{ local_ca_certs_ca_passphrase }}"
ownca_not_after: "{{ cert.not_after | default(local_ca_certs_default_not_after) }}"
ownca_not_before: "{{ cert.not_before | default(local_ca_certs_default_not_before) }}"
register: local_ca_certs_certificate
- name: Write certificate file on server
# noqa: no-handler
when: "local_ca_certs_csr.changed"
ansible.builtin.copy:
dest: "/etc/ssl/certs/{{ cert.cn }}.crt"
content: "{{ local_ca_certs_certificate.certificate }}"
mode: "0644"
notify:
- "Reboot host"

View File

@@ -0,0 +1,16 @@
- name: Install requirements
ansible.builtin.apt:
name:
- "ssl-cert"
- name: Copy CA certificate file on server
ansible.builtin.copy:
src: "ca/ca-certificate.crt"
dest: "/etc/ssl/certs/local-ca.crt"
mode: "0644"
- name: Create certs
loop: "{{ local_ca_certs_list }}"
loop_control:
loop_var: "cert"
ansible.builtin.include_tasks: "create_cert.yml"

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## MySQL

View File

@@ -0,0 +1,6 @@
# Required
mysql_databases: []
mysql_users: []
# - name: <string
# password: <string>
# priv: <string>

View File

@@ -0,0 +1,5 @@
- name: Restart MySQL
ansible.builtin.service:
name: "mysql"
state: "restarted"
daemon_reload: true

View File

@@ -0,0 +1,31 @@
- name: Installation de MySQL
ansible.builtin.apt:
name:
- "mysql-server"
- "python3-pymysql"
notify:
- "Restart MySQL"
- name: Set MySQL bind_address parameter
ansible.builtin.lineinfile:
path: "/etc/mysql/mysql.conf.d/mysqld.cnf"
regexp: "^bind-address\\s+="
line: "bind-address = *"
notify:
- "Restart MySQL"
- name: Création des bases de données MySQL
loop: "{{ mysql_databases }}"
community.mysql.mysql_db:
login_unix_socket: "/var/run/mysqld/mysqld.sock"
name: "{{ item }}"
encoding: "utf8"
- name: Création des utilisateur MySQL
loop: "{{ mysql_users }}"
community.mysql.mysql_user:
login_unix_socket: "/var/run/mysqld/mysqld.sock"
name: "{{ item.name }}"
password: "{{ item.password }}"
priv: "{{ item.priv }}"
host: "{{ item.host }}"

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## NFS client

View File

@@ -0,0 +1,8 @@
# Required
nfs_client_mounts: []
# - mount_point: <string>
# server: <string>
# options: <string> (default "rw,sync,hard")
# owner: <int>
# group: <int>
# mode: <string> (default "0750")

View File

@@ -0,0 +1,23 @@
- name: Installation de NFS Common
ansible.builtin.apt:
name:
- "nfs-common"
- name: Création des points de montage
loop: "{{ nfs_client_mounts }}"
ansible.builtin.file:
path: "{{ item.mount_point }}"
state: "directory"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode | default('0750') }}"
- name: Mount filesystems
loop: "{{ nfs_client_mounts }}"
ansible.posix.mount:
path: "{{ item.mount_point }}"
src: "{{ item.server }}"
fstype: "nfs4"
opts: "{{ item.options | default('rw,sync,hard') }}"
state: "mounted"
boot: true

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## NFS Server

View File

@@ -0,0 +1,9 @@
# Required
nfs_server_exports: []
# - path: <string>
# owner: <int>
# group: <int>
# mode: <string> (default "0750")
# clients:
# - address: <string>
# options: <string> (default "ro")

View File

@@ -0,0 +1,5 @@
- name: Restart NFS-Server
ansible.builtin.service:
name: "nfs-kernel-server"
enabled: true
state: "restarted"

View File

@@ -0,0 +1,36 @@
- name: Installation du serveur NFS
ansible.builtin.apt:
name:
- "nfs-kernel-server"
notify:
- "Restart NFS-Server"
- name: Activation de NFS 4
ansible.builtin.lineinfile:
dest: "/etc/nfs.conf"
regex: "{{ item.regex }}"
line: "{{ item.line }}"
loop:
- regex: "vers3=y"
line: "vers3=n"
- regex: "vers4=y"
line: "vers4=y"
notify:
- "Restart NFS-Server"
- name: Création des répertoires exportés
loop: "{{ nfs_server_exports }}"
ansible.builtin.file:
path: "{{ item.path }}"
state: "directory"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode | default('750') }}"
- name: Copie du fichier exports
ansible.builtin.template:
src: "templates/exports"
dest: "/etc/exports"
mode: "0644"
notify:
- "Restart NFS-Server"

View File

@@ -0,0 +1,3 @@
{% for export in nfs_server_exports %}
{{ export.path }}{% for c in export.clients %} {{ c.address }}({{ c.options | default ("ro") }}){% endfor %}
{% endfor %}

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## OVH Log Data Platform

View File

@@ -0,0 +1,6 @@
# Required
ovh_ldp_token: null
ovh_ldp_cluster: null
# Defaults
ovh_ldp_tls_syslog_port: 6514

View File

@@ -0,0 +1,5 @@
- name: Restart syslog-ng
ansible.builtin.service:
name: "syslog-ng"
state: "restarted"
daemon_reload: true

View File

@@ -0,0 +1,13 @@
- name: Install syslog-ng
ansible.builtin.apt:
name:
- "syslog-ng"
- "ca-certificates"
- name: Generate syslog-ng config file for OVH LDP
ansible.builtin.template:
src: "templates/ldp-ovh.conf"
dest: "/etc/syslog-ng/conf.d/ldp-ovh.conf"
mode: "0640"
notify:
- "Restart syslog-ng"

View File

@@ -0,0 +1,22 @@
rewrite ovh-token {
set("{{ ovh_ldp_token }}", value(".SDATA.token@29084.X-OVH-TOKEN"));
};
destination ovhPaaSLogs {
network("{{ ovh_ldp_cluster }}"
port({{ ovh_ldp_tls_syslog_port }})
transport("tls")
flags(syslog-protocol)
ts_format("iso")
frac-digits(6)
tls(peer-verify("required-trusted") ca_dir("/etc/ssl/certs/"))
keep-alive(yes)
so_keepalive(yes)
);
};
log {
source(s_src);
rewrite(ovh-token);
destination(ovhPaaSLogs);
};

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Unbound

View File

@@ -0,0 +1,14 @@
unbound_listen_interfaces:
- "0.0.0.0"
unbound_access_control:
- "0.0.0.0/0 allow"
unbound_forward_zones:
- name: "."
forward_to: "8.8.8.8"
unbound_stub_zones: []
# - name: <string>
# stub_to: <string>
# no_cache: no (default yes)

View File

@@ -0,0 +1,4 @@
- name: Restart unbound
ansible.builtin.service:
name: "unbound"
state: "restarted"

View File

@@ -0,0 +1,14 @@
- name: Install Unbound
ansible.builtin.apt:
name:
- "unbound"
notify:
- "Restart unbound"
- name: Deploy unbounf config file
ansible.builtin.template:
src: "templates/unbound.conf"
dest: "/etc/unbound/unbound.conf.d/unbound.conf"
mode: "0644"
notify:
- "Restart unbound"

View File

@@ -0,0 +1,21 @@
server:
do-not-query-localhost: no
{% for interface in unbound_listen_interfaces %}
interface: {{ interface }}
{% endfor %}
{% for access in unbound_access_control %}
access-control: {{ access }}
{% endfor %}
{% for zone in unbound_stub_zones %}
stub-zone:
name: "{{ zone.name }}"
stub-addr: {{ zone.stub_to }}
stub-no-cache: {{ zone.no_cache | default('yes') }}
{% endfor %}
{% for zone in unbound_forward_zones %}
forward-zone:
name: "{{ zone.name }}"
forward-addr: {{ zone.forward_to }}
{% endfor %}

View File

@@ -0,0 +1,5 @@
# BYOW - Build Your Own WAF
A WAF _à la carte_.
## Wordpress

View File

@@ -0,0 +1,24 @@
# Required
wordpress_url: null
wordpress_site: null
wordpress_site_title: null
wordpress_admin_user: null
wordpress_admin_password: null
wordpress_admin_email: null
wordpress_db_pass: null
wordpress_themes: []
wordpress_active_theme: null
# Defaults
wordpress_user: "wordpress"
wordpress_plugins: []
wordpress_locale: "fr_FR"
wordpress_db_host: "localhost"
wordpress_db_name: "{{ wordpress_user }}"
wordpress_db_user: "{{ wordpress_user }}"
wordpress_db_prefix: "wp_"
wordpress_db_charset: "utf8mb4"
# Optionnal
wordpress_import_ocdi_indexes: []
wordpress_create_master: null

View File

@@ -0,0 +1,17 @@
- name: Restart Nginx
ansible.builtin.systemd:
name: "nginx"
state: "restarted"
- name: Flush Wordpress
become: true
become_user: "{{ wordpress_user }}"
ansible.builtin.shell:
executable: "/usr/bin/bash"
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: |
set -o pipefail
wp cache flush
wp transient delete --all
wp rewrite flush --hard
changed_when: true

View File

@@ -0,0 +1,12 @@
- name: Installation des plugins Wordpress
become: true
become_user: "{{ wordpress_user }}"
loop: "{{ wordpress_plugins_to_install }}"
ansible.builtin.command:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: "wp plugin install {{ item }} --activate"
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-content/plugins/{{ item | basename | splitext | first }}"
register: "wordpress_out_plugin_install"
changed_when: "'Success: Installed 1 of 1 plugins' in wordpress_out_plugin_install.stdout"
notify:
- "Flush Wordpress"

View File

@@ -0,0 +1,214 @@
- name: Installation des pré-requis
ansible.builtin.apt:
name:
- "nginx"
- "php-fpm"
- "php-mysql"
- "php-curl"
- "php-json"
- "php-xml"
- "php-mbstring"
- "php-zip"
- "php-igbinary"
- "php-imagick"
- "php-intl"
- "php-apcu"
- "zip"
- name: Installation de wp-cli
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
ansible.builtin.get_url:
url: "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
dest: "/usr/local/bin/wp"
owner: "root"
group: "root"
mode: "755"
- name: Création de l'utilisateur système pour Wordpress
ansible.builtin.user:
name: "{{ wordpress_user | mandatory }}"
home: "/var/www/{{ wordpress_user }}"
group: "www-data"
shell: "/usr/bin/bash"
password_lock: true
create_home: true
- name: Création du répertoire de Wordpress
ansible.builtin.file:
path: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
state: "directory"
owner: "{{ wordpress_user }}"
group: "www-data"
mode: "0750"
- name: Installation de Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
ansible.builtin.command:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: "wp core download --locale='{{ wordpress_locale }}' --skip-content"
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-config-sample.php"
# TODO: https://make.wordpress.org/cli/handbook/references/config/#config-files
- name: Configuration de Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
ansible.builtin.shell:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
executable: "/usr/bin/bash"
cmd: |
set -o pipefail
wp config create \
--dbhost='{{ wordpress_db_host }}' \
--dbname='{{ wordpress_db_name }}' \
--dbuser='{{ wordpress_db_user }}' \
--dbpass='{{ wordpress_db_pass }}' \
--dbprefix='{{ wordpress_db_prefix }}' \
--dbcharset='{{ wordpress_db_charset }}' \
--extra-php <<PHP
define('FS_METHOD', 'direct');
define('FORCE_SSL_ADMIN', false);
define('WP_HOME', 'https://{{ wordpress_site }}');
define('WP_SITEURL', 'https://{{ wordpress_site }}');
error_reporting(0);
@ini_set('display_errors', 0);
define('DISALLOW_FILE_EDIT', true);
if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO']) && \$_SERVER['HTTP_X_FORWARDED_PROTO']=='https')
\$_SERVER['HTTPS'] = 'on';
PHP
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-config.php"
notify:
- "Flush Wordpress"
- name: Create Wordpress database tables
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
ansible.builtin.command:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: >
wp core install
--url='{{ wordpress_url }}'
--title='{{ wordpress_site_title }}'
--admin_user='{{ wordpress_admin_user }}'
--admin_password='{{ wordpress_admin_password }}'
--admin_email='{{ wordpress_admin_email }}'
--locale='{{ wordpress_locale }}'
--skip-email
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-content/uploads/ }}"
register: "wordpress_out_core_install"
changed_when: "'Success: WordPress installed successfully.' in wordpress_out_core_install.stdout"
notify:
- "Flush Wordpress"
- name: Installation des thèmes Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
loop: "{{ wordpress_themes }}"
ansible.builtin.command:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: "wp theme install '{{ item }}'"
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-content/themes/{{ item | basename | splitext | first }}"
register: "wordpress_out_theme_install"
changed_when: "'Success: Installed 1 of 1 themes' in wordpress_out_theme_install.stdout"
notify:
- "Flush Wordpress"
- name: Activation du thème Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
ansible.builtin.command:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
cmd: "wp theme activate '{{ wordpress_active_theme }}'"
register: "wordpress_out_theme_activate"
changed_when: "'Success: Switched to ' in wordpress_out_theme_activate.stdout"
notify:
- "Flush Wordpress"
- name: Installation des plugins wordpress-importer et one-click-demo-import
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
vars:
wordpress_plugins_to_install:
- "wordpress-importer"
- "one-click-demo-import"
ansible.builtin.include_tasks: "install_plugins.yml"
- name: Importation des plugins OCDI
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
become: true
become_user: "{{ wordpress_user }}"
loop: "{{ wordpress_import_ocdi_indexes }}"
ansible.builtin.shell:
chdir: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}"
executable: "/usr/bin/bash"
cmd: |
set -o pipefail
wp ocdi import --predefined='{{ item }}' \
&& touch /var/www/{{ wordpress_user }}/{{ wordpress_site }}/.ocdi_index{{ item }}_imported
creates: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/.ocdi_index{{ item }}_imported"
register: "wordpress_out_ocdi_import"
changed_when: "'Success: Content import finished!' in wordpress_out_ocdi_import.stdout"
notify:
- "Flush Wordpress"
- name: Installation des plugins Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
vars:
wordpress_plugins_to_install: "{{ wordpress_plugins }}"
ansible.builtin.include_tasks: "install_plugins.yml"
- name: Création du répertoire cache de Wordpress
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
ansible.builtin.file:
path: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-content/cache"
state: "directory"
owner: "{{ wordpress_user }}"
group: "www-data"
mode: "0750"
- name: Apply permissions
when: "wordpress_create_master != None and wordpress_create_master == ansible_hostname"
loop:
- "cache"
- "upgrade"
- "uploads"
ansible.builtin.file:
path: "/var/www/{{ wordpress_user }}/{{ wordpress_site }}/wp-content/{{ item }}/"
owner: "{{ wordpress_user }}"
group: "www-data"
mode: "u=rwX,g=rwX,o=rX"
recurse: true
- name: Configuration de Nginx
ansible.builtin.template:
src: "templates/nginx_vhost.conf"
dest: "/etc/nginx/sites-available/{{ wordpress_site }}.conf"
owner: "root"
group: "root"
mode: "0644"
notify:
- "Restart Nginx"
- name: Link Nginx vhost config
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ wordpress_site }}.conf"
dest: "/etc/nginx/sites-enabled/{{ wordpress_site }}.conf"
state: "link"
notify:
- "Restart Nginx"
- name: Unlink Nginx default vhost config
ansible.builtin.file:
path: "/etc/nginx/sites-enabled/default"
state: "absent"
notify:
- "Restart Nginx"
# FIXME: php. ini config
# Mainly: upload_max_filesize, post_max_size, max_execution_time

View File

@@ -0,0 +1,50 @@
server {
listen 80;
server_name {{ wordpress_site }} www.{{ wordpress_site }};
root /var/www/{{ wordpress_user }}/{{ wordpress_site }};
index index.html index.htm index.php;
client_max_body_size 50M;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
location ~ /\.ht {
deny all;
}
location ~ /wp-config\.php\$ {
deny all;
}
location ~ /wp-content/uploads/.*\.php\$ {
deny all;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
}
# Réécritures pour WordPress
location ~ ^/index\.php$ { }
location / {
if (!-e $request_filename){
rewrite ^/(.*)$ /index.php last;
break;
}
}
# location / {
# try_files \$uri \$uri/ /index.php?\$args;
# }
# location ~ \.php\$ {
# include snippets/fastcgi-php.conf;
# fastcgi_pass unix:/var/run/php/php-fpm.sock;
# fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
# }
}

Binary file not shown.