premier commit

This commit is contained in:
Daniel Allaire 2026-03-03 09:42:18 -05:00
commit 92732aa22a
34 changed files with 885 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*.swp
*.swo
*.log
.DS_Store
__pycache__/
.vscode/
.env

42
00-ARCHITECTURE.md Normal file
View file

@ -0,0 +1,42 @@
# Modèle de référence Architecture
Ce modèle sapplique aux instances ERPLibre (Odoo) **équivalentes** :
- Odoo conteneurisé
- PostgreSQL conteneurisé
- Nginx sur lhôte
- Exposition publique via 80/443
- Docker standard
- Debian/Ubuntu moderne
Exemple de domaine utilisé dans la documentation : **www.rencontres-linux.quebec** (illustratif)
## Topologie
```
Internet
nftables (policy DROP + ASN TTL léger)
Nginx (hôte)
Docker network dédié (erplibre_net)
Odoo
PostgreSQL
```
## Règles dor
1. **Ne jamais publier** 8069/8072 (Odoo) vers 0.0.0.0.
2. **Ne jamais publier** 5432 (PostgreSQL) vers 0.0.0.0.
3. **Nginx est lunique** point dentrée public.
4. Pare-feu : **DROP par défaut**, exceptions explicites (80/443).
5. Mitigation : **comportementale**, pas de géoblocage massif.
## Flux
- Client → Nginx (443)
- Nginx → Odoo (réseau Docker dédié)
- Odoo → PostgreSQL (réseau Docker dédié)

40
01-SECURITY-PHILOSOPHY.md Normal file
View file

@ -0,0 +1,40 @@
# Sécurité numérique responsable (mode A)
## Positionnement
Lobjectif est de protéger un service public (ou communautaire) sans sur-complexifier :
- Réduction de la surface dattaque
- Défense multicouche proportionnée
- Mesures réversibles
- Confort opérationnel
## Pourquoi pas de géoblocage ?
Un site multilingue a une audience potentiellement mondiale. Les blocages géographiques :
- créent des faux positifs massifs,
- napportent pas une mitigation fiable,
- sont difficiles à justifier publiquement.
## Pourquoi ASN TTL ?
LASN TTL :
- cible des sources de bruit « structurel »,
- reste réversible (TTL),
- permet une escalade mesurée.
Recommandation mode A :
- Fail2ban : 24h
- ASN : seuil ≥ 40 IP bannies / 24h
- TTL : 7 jours
## Pourquoi WAF léger ?
Un WAF strict génère des faux positifs coûteux à gérer. Un WAF léger (CRS niveau 1) :
- bloque lévidence,
- demande peu de tuning,
- protège sans pénaliser les usagers.
## Pourquoi nftables ?
nftables est moderne, lisible, et permet des structures (sets) efficaces (ex. ASN TTL).

5
LICENSE Normal file
View file

@ -0,0 +1,5 @@
MIT License
Copyright (c) 2026
Permission is hereby granted...

3
Makefile Normal file
View file

@ -0,0 +1,3 @@
.PHONY: lint
lint:
nginx -t || true

84
README.md Normal file
View file

@ -0,0 +1,84 @@
# ERPLibre Secure Reference Deployment
## Sécurité numérique responsable
Ce dépôt constitue un **modèle de référence** applicable à toute instance ERPLibre (Odoo) **conteneurisée**, exposée publiquement via **Nginx sur lhôte**, avec **PostgreSQL conteneurisé**.
Exemple dimplémentation (illustratif) : **https://www.rencontres-linux.quebec**
---
## Architecture cible
```
Internet
nftables (policy DROP + mitigation ASN TTL légère)
Nginx (hôte)
Réseau Docker dédié (erplibre_net)
Odoo (conteneur, pas de port publié)
PostgreSQL (conteneur, pas de port publié)
```
---
## Principes
- **Surface minimale** : seuls ports **80/443** exposés.
- **Segmentation Docker** : réseau dédié par stack.
- **Pare-feu moderne** : nftables (DROP par défaut), règles explicites.
- **Mitigation proportionnée** : Fail2ban (24h) + ASN TTL (7 jours).
- **WAF léger** : ModSecurity + OWASP CRS (niveau 1), approche prudente.
---
## Démarrage rapide
1. Créer le réseau Docker dédié :
```bash
docker network create erplibre_net
```
2. Déployer la stack (exemple) :
```bash
cd docker
docker compose up -d
```
3. Installer / activer Nginx + config du vhost :
- `nginx/erplibre.conf` (exemple)
- Certificat TLS via Certbot (hors du périmètre de ce dépôt)
4. Activer le pare-feu nftables :
- `nftables/edge.nft`
5. Installer Fail2ban :
- `fail2ban/jail.local` (+ filtres au besoin)
6. Activer la mitigation ASN TTL :
- `asn-mitigation/*` (script + systemd timer)
Voir : `docs/DEPLOYMENT-STEPS.md`
---
## Contenu du dépôt
- `docker/` : stack Docker (Odoo + PostgreSQL) sur réseau dédié
- `nginx/` : vhost Nginx durci (exemple)
- `nftables/` : pare-feu minimal (DROP + ASN TTL)
- `fail2ban/` : jails de base
- `asn-mitigation/` : script + unit/timer systemd
- `logrotate/` : rotation des logs Nginx
- `ansible/` : playbook minimal (mode A) pour appliquer ces artefacts
- `diagram/` : schéma (SVG)
---
## Licence
MIT (voir `LICENSE`).

View file

@ -0,0 +1,2 @@
[erplibre]
erplibre1 ansible_host=CHANGE_ME ansible_user=CHANGE_ME

View file

@ -0,0 +1,12 @@
---
- name: Apply ERPLibre secure reference (mode A)
hosts: erplibre
become: true
vars:
domain_example: "www.rencontres-linux.quebec"
nginx_site_src: "../roles/erplibre_secure_reference/files/nginx/erplibre.conf"
nginx_site_dest: "/etc/nginx/sites-available/erplibre.conf"
roles:
- erplibre_secure_reference

View file

@ -0,0 +1 @@
# Optional CIDR whitelist, one per line (e.g., 203.0.113.10/32)

View file

@ -0,0 +1 @@
# Add ASNs here, one per line (e.g., AS9009)

View file

@ -0,0 +1,8 @@
[Unit]
Description=Refresh ASN TTL blocklist into nftables
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-asn-ttl-blocklist.sh

View file

@ -0,0 +1,9 @@
[Unit]
Description=Daily refresh of ASN TTL blocklist
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
DENYLIST="/etc/edge/asn-denylist.txt"
ALLOWLIST="/etc/edge/asn-allowlist-v4.txt"
TMP="/tmp/asn_prefixes_v4.txt"
SETDENY="inet filter asn_deny_v4"
SETALLOW="inet filter asn_allow_v4"
TTL="7d"
command -v nft >/dev/null
command -v curl >/dev/null
command -v jq >/dev/null
mkdir -p /etc/edge
: > "$TMP"
# Allowlist (static)
if [[ -f "$ALLOWLIST" ]]; then
nft "flush set $SETALLOW" || true
while read -r cidr; do
[[ -z "${cidr}" || "${cidr:0:1}" == "#" ]] && continue
nft "add element $SETALLOW { $cidr }" || true
done < "$ALLOWLIST"
fi
# RIPEstat: announced prefixes for an ASN
while read -r asn; do
[[ -z "${asn}" || "${asn:0:1}" == "#" ]] && continue
asn="${asn#AS}"
url="https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}"
curl -fsS "$url" | jq -r '.data.prefixes[]?.prefix' | awk -F/ '$1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ {print $0}' >> "$TMP"
done < "$DENYLIST"
sort -u -o "$TMP" "$TMP"
# Refresh TTL by re-adding elements
while read -r cidr; do
nft "add element $SETDENY { $cidr timeout $TTL }" || true
done < "$TMP"
echo "ASN deny refreshed: $(wc -l < "$TMP") IPv4 prefixes (TTL=$TTL)"

View file

@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST> - .* "(GET|POST) /(wp-admin|wp-login\.php|xmlrpc\.php|\.env|phpmyadmin|\.git).*" .*
ignoreregex =

View file

@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST> - .* "(POST|GET) /web/login .*" (200|401|403) .*
ignoreregex =

View file

@ -0,0 +1,17 @@
[nginx-odoo-login]
enabled = true
port = http,https
filter = nginx-odoo-login
logpath = /var/log/nginx/rencontres-linux-access.log
maxretry = 8
findtime = 10m
bantime = 24h
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/rencontres-linux-access.log
maxretry = 20
findtime = 10m
bantime = 24h

View file

@ -0,0 +1,13 @@
/var/log/nginx/rencontres-linux-*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
systemctl reload nginx > /dev/null 2>&1 || true
endscript
}

View file

@ -0,0 +1,25 @@
table inet filter {
set asn_deny_v4 {
type ipv4_addr
flags interval, timeout
timeout 7d
auto-merge
}
chain input {
type filter hook input priority 0;
policy drop;
ct state established,related accept
iif lo accept
# Public: only HTTP/HTTPS
tcp dport {80,443} accept
# ASN TTL drop (optional)
ip saddr @asn_deny_v4 drop
reject with icmpx type port-unreachable
}
}

View file

@ -0,0 +1,91 @@
# Exemple de vhost Nginx (Nginx sur l'hôte) domaine illustratif: www.rencontres-linux.quebec
# À adapter: chemins TLS (Certbot), upstream (IP/nom du conteneur), logs.
# Quick win: réduire fingerprinting
server_tokens off;
# Zones (rate limit) à placer dans http{} si vous centralisez
limit_req_zone $binary_remote_addr zone=LOGIN:10m rate=5r/m;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Upstreams (exemple)
# Recommandé: pointer vers le conteneur via le réseau docker dédié (DNS docker) si Nginx est aussi dans docker.
# Ici Nginx est sur l'hôte: on pointera vers l'IP du conteneur Odoo sur erplibre_net (ou via un reverse proxy local).
upstream odoo_backend {
server 127.0.0.1:18069; # TODO: remplacer par un mécanisme stable (ex: socat, ou IP fixe, ou Nginx dans docker)
keepalive 32;
}
upstream odoo_bus {
server 127.0.0.1:18072; # TODO idem
keepalive 16;
}
server {
listen 80;
server_name www.rencontres-linux.quebec rencontres-linux.quebec;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
location / {
return 301 https://www.rencontres-linux.quebec$request_uri;
}
}
server {
listen 443 ssl http2;
server_name www.rencontres-linux.quebec;
# TODO Certbot
ssl_certificate /etc/letsencrypt/live/www.rencontres-linux.quebec/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.rencontres-linux.quebec/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
access_log /var/log/nginx/rencontres-linux-access.log;
error_log /var/log/nginx/rencontres-linux-error.log warn;
client_max_body_size 512M;
# Web login rate limit
location = /web/login {
limit_req zone=LOGIN burst=10 nodelay;
proxy_pass http://odoo_backend;
}
# Websocket/bus
location /websocket {
proxy_pass http://odoo_bus;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
location / {
proxy_pass http://odoo_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_redirect off;
}
}

View file

@ -0,0 +1,125 @@
---
- name: Install baseline packages
apt:
name:
- nftables
- nginx
- fail2ban
- jq
- curl
state: present
update_cache: true
- name: Ensure directories
file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- /etc/nftables.d
- /etc/edge
- /var/www/letsencrypt
- name: Deploy nftables edge policy
copy:
src: "nftables/edge.nft"
dest: "/etc/nftables.d/edge.nft"
mode: "0644"
- name: Ensure nftables.conf includes /etc/nftables.d/*.nft
lineinfile:
path: /etc/nftables.conf
line: 'include "/etc/nftables.d/*.nft"'
create: true
- name: Enable and apply nftables
systemd:
name: nftables
enabled: true
state: restarted
- name: Deploy nginx site
copy:
src: "nginx/erplibre.conf"
dest: "/etc/nginx/sites-available/erplibre.conf"
mode: "0644"
- name: Enable nginx site
file:
src: /etc/nginx/sites-available/erplibre.conf
dest: /etc/nginx/sites-enabled/erplibre.conf
state: link
force: true
- name: Remove default nginx site if present
file:
path: /etc/nginx/sites-enabled/default
state: absent
- name: Test nginx configuration
command: nginx -t
changed_when: false
- name: Reload nginx
systemd:
name: nginx
state: reloaded
enabled: true
- name: Deploy fail2ban jail
copy:
src: "fail2ban/jail.local"
dest: "/etc/fail2ban/jail.d/erplibre.local"
mode: "0644"
- name: Deploy fail2ban filters
copy:
src: "fail2ban/filters/"
dest: "/etc/fail2ban/filter.d/"
mode: "0644"
- name: Restart fail2ban
systemd:
name: fail2ban
state: restarted
enabled: true
- name: Deploy ASN mitigation script and lists
copy:
src: "asn-mitigation/update-asn-ttl-blocklist.sh"
dest: "/usr/local/sbin/update-asn-ttl-blocklist.sh"
mode: "0750"
- name: Deploy ASN deny/allow lists (templates)
copy:
src: "asn-mitigation/{{ item }}"
dest: "/etc/edge/{{ item }}"
mode: "0644"
loop:
- asn-denylist.txt
- asn-allowlist-v4.txt
- name: Deploy systemd units for ASN timer
copy:
src: "asn-mitigation/{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: "0644"
loop:
- asn-ttl-blocklist.service
- asn-ttl-blocklist.timer
- name: systemd daemon-reload
systemd:
daemon_reload: true
- name: Enable ASN timer
systemd:
name: asn-ttl-blocklist.timer
enabled: true
state: started
- name: Deploy logrotate rule
copy:
src: "logrotate/nginx-erplibre"
dest: "/etc/logrotate.d/nginx-erplibre"
mode: "0644"

View file

@ -0,0 +1 @@
# Optional CIDR whitelist, one per line (e.g., 203.0.113.10/32)

View file

@ -0,0 +1 @@
# Add ASNs here, one per line (e.g., AS9009)

View file

@ -0,0 +1,8 @@
[Unit]
Description=Refresh ASN TTL blocklist into nftables
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-asn-ttl-blocklist.sh

View file

@ -0,0 +1,9 @@
[Unit]
Description=Daily refresh of ASN TTL blocklist
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
DENYLIST="/etc/edge/asn-denylist.txt"
ALLOWLIST="/etc/edge/asn-allowlist-v4.txt"
TMP="/tmp/asn_prefixes_v4.txt"
SETDENY="inet filter asn_deny_v4"
SETALLOW="inet filter asn_allow_v4"
TTL="7d"
command -v nft >/dev/null
command -v curl >/dev/null
command -v jq >/dev/null
mkdir -p /etc/edge
: > "$TMP"
# Allowlist (static)
if [[ -f "$ALLOWLIST" ]]; then
nft "flush set $SETALLOW" || true
while read -r cidr; do
[[ -z "${cidr}" || "${cidr:0:1}" == "#" ]] && continue
nft "add element $SETALLOW { $cidr }" || true
done < "$ALLOWLIST"
fi
# RIPEstat: announced prefixes for an ASN
while read -r asn; do
[[ -z "${asn}" || "${asn:0:1}" == "#" ]] && continue
asn="${asn#AS}"
url="https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}"
curl -fsS "$url" | jq -r '.data.prefixes[]?.prefix' | awk -F/ '$1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ {print $0}' >> "$TMP"
done < "$DENYLIST"
sort -u -o "$TMP" "$TMP"
# Refresh TTL by re-adding elements
while read -r cidr; do
nft "add element $SETDENY { $cidr timeout $TTL }" || true
done < "$TMP"
echo "ASN deny refreshed: $(wc -l < "$TMP") IPv4 prefixes (TTL=$TTL)"

42
diagram/architecture.svg Normal file
View file

@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="520" viewBox="0 0 1100 520">
<defs>
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
<path d="M0,0 L12,6 L0,12 z" />
</marker>
</defs>
<rect x="40" y="40" width="1020" height="440" rx="18" ry="18" fill="none" stroke="black" stroke-width="2"/>
<text x="70" y="80" font-family="Arial" font-size="22">ERPLibre Secure Reference (example: https://www.rencontres-linux.quebec)</text>
<rect x="90" y="120" width="240" height="80" rx="14" ry="14" fill="white" stroke="black" stroke-width="2"/>
<text x="120" y="168" font-family="Arial" font-size="20">Internet</text>
<rect x="410" y="120" width="260" height="80" rx="14" ry="14" fill="white" stroke="black" stroke-width="2"/>
<text x="430" y="153" font-family="Arial" font-size="18">nftables (DROP)</text>
<text x="430" y="178" font-family="Arial" font-size="16">+ ASN TTL (7d)</text>
<rect x="760" y="120" width="260" height="80" rx="14" ry="14" fill="white" stroke="black" stroke-width="2"/>
<text x="780" y="153" font-family="Arial" font-size="18">Nginx (host)</text>
<text x="780" y="178" font-family="Arial" font-size="16">ports 80/443 only</text>
<rect x="90" y="280" width="930" height="170" rx="18" ry="18" fill="white" stroke="black" stroke-width="2"/>
<text x="120" y="315" font-family="Arial" font-size="18">Docker network dedicated: erplibre_net (no published app/db ports)</text>
<rect x="160" y="340" width="360" height="80" rx="14" ry="14" fill="white" stroke="black" stroke-width="2"/>
<text x="180" y="388" font-family="Arial" font-size="18">Odoo / ERPLibre (container)</text>
<rect x="590" y="340" width="360" height="80" rx="14" ry="14" fill="white" stroke="black" stroke-width="2"/>
<text x="610" y="388" font-family="Arial" font-size="18">PostgreSQL (container)</text>
<!-- arrows -->
<line x1="330" y1="160" x2="410" y2="160" stroke="black" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="670" y1="160" x2="760" y2="160" stroke="black" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="890" y1="200" x2="890" y2="280" stroke="black" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="520" y1="380" x2="590" y2="380" stroke="black" stroke-width="2" marker-end="url(#arrow)"/>
<!-- small labels -->
<text x="365" y="148" font-family="Arial" font-size="14">HTTP(S)</text>
<text x="708" y="148" font-family="Arial" font-size="14">TLS termination</text>
<text x="905" y="250" font-family="Arial" font-size="14">reverse proxy</text>
<text x="536" y="368" font-family="Arial" font-size="14">DB traffic</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

34
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,34 @@
version: "3.9"
networks:
erplibre_net:
driver: bridge
services:
postgres:
image: postgres:15
networks:
- erplibre_net
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: change_me
POSTGRES_USER: odoo
POSTGRES_DB: odoo
odoo:
image: odoo:18
depends_on:
- postgres
networks:
- erplibre_net
expose:
- "8069"
- "8072"
environment:
HOST: postgres
USER: odoo
PASSWORD: change_me
volumes:
pgdata:

61
docs/DEPLOYMENT-STEPS.md Normal file
View file

@ -0,0 +1,61 @@
# Déploiement modèle de référence
Exemple illustratif : https://www.rencontres-linux.quebec
## Pré-requis
- Debian/Ubuntu moderne
- Docker Engine + Docker Compose
- Nginx sur lhôte
- nftables
- (optionnel) fail2ban, modsecurity
## Étapes
1. Créer un réseau Docker dédié :
```bash
docker network create erplibre_net
```
2. Adapter `docker/docker-compose.yml` (mots de passe, image ERPLibre).
3. Démarrer la stack :
```bash
cd docker
docker compose up -d
```
4. Vérifier quaucun port applicatif nest publié :
```bash
docker ps --format 'table {.Names}\t{.Ports}'
```
5. Installer Nginx et activer le vhost :
- Copier `nginx/erplibre.conf` vers `/etc/nginx/sites-available/`
- Adapter `server_name` et les chemins TLS Certbot
- `nginx -t && systemctl reload nginx`
6. TLS (Certbot) :
- Préparer `/var/www/letsencrypt`
- Générer/renouveler les certificats
7. Activer nftables :
- Copier `nftables/edge.nft` dans `/etc/nftables.d/`
- Inclure `/etc/nftables.d/*.nft` dans `/etc/nftables.conf`
- `nft -f /etc/nftables.conf`
- `systemctl enable --now nftables`
8. Fail2ban :
- Copier `fail2ban/jail.local` dans `/etc/fail2ban/jail.d/erplibre.local`
- Copier les filtres dans `/etc/fail2ban/filter.d/`
- `systemctl restart fail2ban`
9. Mitigation ASN TTL :
- Copier `asn-mitigation/update-asn-ttl-blocklist.sh` vers `/usr/local/sbin/`
- Copier les listes vers `/etc/edge/`
- Copier les units systemd vers `/etc/systemd/system/`
- `systemctl daemon-reload`
- `systemctl enable --now asn-ttl-blocklist.timer`
10. Logrotate :
- Copier `logrotate/nginx-erplibre` vers `/etc/logrotate.d/`
## Tests
- Depuis lextérieur : seuls 80/443 doivent répondre.
- SSL Labs : viser A/A+.

View file

@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST> - .* "(GET|POST) /(wp-admin|wp-login\.php|xmlrpc\.php|\.env|phpmyadmin|\.git).*" .*
ignoreregex =

View file

@ -0,0 +1,3 @@
[Definition]
failregex = ^<HOST> - .* "(POST|GET) /web/login .*" (200|401|403) .*
ignoreregex =

17
fail2ban/jail.local Normal file
View file

@ -0,0 +1,17 @@
[nginx-odoo-login]
enabled = true
port = http,https
filter = nginx-odoo-login
logpath = /var/log/nginx/rencontres-linux-access.log
maxretry = 8
findtime = 10m
bantime = 24h
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/rencontres-linux-access.log
maxretry = 20
findtime = 10m
bantime = 24h

13
logrotate/nginx-erplibre Normal file
View file

@ -0,0 +1,13 @@
/var/log/nginx/rencontres-linux-*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
systemctl reload nginx > /dev/null 2>&1 || true
endscript
}

25
nftables/edge.nft Normal file
View file

@ -0,0 +1,25 @@
table inet filter {
set asn_deny_v4 {
type ipv4_addr
flags interval, timeout
timeout 7d
auto-merge
}
chain input {
type filter hook input priority 0;
policy drop;
ct state established,related accept
iif lo accept
# Public: only HTTP/HTTPS
tcp dport {80,443} accept
# ASN TTL drop (optional)
ip saddr @asn_deny_v4 drop
reject with icmpx type port-unreachable
}
}

91
nginx/erplibre.conf Normal file
View file

@ -0,0 +1,91 @@
# Exemple de vhost Nginx (Nginx sur l'hôte) domaine illustratif: www.rencontres-linux.quebec
# À adapter: chemins TLS (Certbot), upstream (IP/nom du conteneur), logs.
# Quick win: réduire fingerprinting
server_tokens off;
# Zones (rate limit) à placer dans http{} si vous centralisez
limit_req_zone $binary_remote_addr zone=LOGIN:10m rate=5r/m;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Upstreams (exemple)
# Recommandé: pointer vers le conteneur via le réseau docker dédié (DNS docker) si Nginx est aussi dans docker.
# Ici Nginx est sur l'hôte: on pointera vers l'IP du conteneur Odoo sur erplibre_net (ou via un reverse proxy local).
upstream odoo_backend {
server 127.0.0.1:18069; # TODO: remplacer par un mécanisme stable (ex: socat, ou IP fixe, ou Nginx dans docker)
keepalive 32;
}
upstream odoo_bus {
server 127.0.0.1:18072; # TODO idem
keepalive 16;
}
server {
listen 80;
server_name www.rencontres-linux.quebec rencontres-linux.quebec;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
location / {
return 301 https://www.rencontres-linux.quebec$request_uri;
}
}
server {
listen 443 ssl http2;
server_name www.rencontres-linux.quebec;
# TODO Certbot
ssl_certificate /etc/letsencrypt/live/www.rencontres-linux.quebec/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.rencontres-linux.quebec/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
access_log /var/log/nginx/rencontres-linux-access.log;
error_log /var/log/nginx/rencontres-linux-error.log warn;
client_max_body_size 512M;
# Web login rate limit
location = /web/login {
limit_req zone=LOGIN burst=10 nodelay;
proxy_pass http://odoo_backend;
}
# Websocket/bus
location /websocket {
proxy_pass http://odoo_bus;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
location / {
proxy_pass http://odoo_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_redirect off;
}
}