mirror of https://github.com/interlegis/sapl.git
Browse Source
- nginx: sendfile on, tcp_nopush, reduced keepalive/proxy timeouts - nginx: GeoIP2 ASN-based bot blocking (cloud providers + known scrapers) - nginx: UA blocklist (GPTBot, ClaudeBot, Chrome/98.0.4758 impersonator, etc.) - nginx: rate-limit zones (30r/m general, 10r/m heavy/relatorios), 429/500 error pages - nginx: proper ETags + Cache-Control on /media/ to stop 30GB logo re-transfers - Dockerfile: install libnginx-mod-http-geoip2; download GeoLite2-ASN.mmdb via BuildKit secret (key never baked into image layers); ARG GEOIP_CACHE_BUST for forced re-download without --no-cache - Gunicorn: workers 3->2, threads 8->4, timeout 300->120, max_memory 300->400MB - Django: FILE_UPLOAD_MAX_MEMORY_SIZE=2MB, FILE_UPLOAD_TEMP_DIR for large uploads - relatorios/views.py: fix N+1 in get_etiqueta_protocolos with bulk-fetch MateriaLegislativa + DocumentoAdministrativo using select_related + dict lookups - Add robots.txt, 429.html, 500.html static pages - docker-compose.yaml: use sapl:local for local dev - docker/README.md: build instructions with MAXMIND_LICENSE_KEY - rate-limiter-v2.md: canonical planning document (Architecture through Phase 5) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>rate-limiter-2026
12 changed files with 1714 additions and 53 deletions
@ -0,0 +1,107 @@ |
|||
# SAPL Docker Build |
|||
|
|||
## Building locally |
|||
|
|||
### 1. Prerequisites |
|||
|
|||
- Docker 23+ with BuildKit enabled (default since Docker 23) |
|||
- A free [MaxMind account](https://www.maxmind.com/en/geolite2/signup) with a license key |
|||
|
|||
### 2. Set your MaxMind license key |
|||
|
|||
Add the key to the project root `.env` file (already gitignored): |
|||
|
|||
``` |
|||
MAXMIND_LICENSE_KEY=your_key_here |
|||
``` |
|||
|
|||
The key is used **only at build time** to download the `GeoLite2-ASN.mmdb` database for |
|||
nginx ASN-based bot blocking. It is injected via a BuildKit secret and is **never stored |
|||
in any image layer** — it will not appear in `docker history` or any registry push. |
|||
|
|||
### 3. Build the image |
|||
|
|||
```bash |
|||
docker build \ |
|||
--secret id=maxmind_key,src=.env \ |
|||
-f docker/Dockerfile \ |
|||
-t sapl:local \ |
|||
. |
|||
``` |
|||
|
|||
Run from the **project root** (not from inside `docker/`), so the build context includes |
|||
the full source tree. |
|||
|
|||
#### Optional build args |
|||
|
|||
| Arg | Default | Description | |
|||
|---|---|---| |
|||
| `WITH_NGINX` | `1` | Include nginx in the image | |
|||
| `WITH_GRAPHVIZ` | `1` | Include Graphviz | |
|||
| `WITH_POPPLER` | `1` | Include Poppler (PDF utilities) | |
|||
| `WITH_PSQL_CLIENT` | `1` | Include `psql` client | |
|||
|
|||
Example — build without Graphviz: |
|||
|
|||
```bash |
|||
docker build \ |
|||
--secret id=maxmind_key,src=.env \ |
|||
--build-arg WITH_GRAPHVIZ=0 \ |
|||
-f docker/Dockerfile \ |
|||
-t sapl:local \ |
|||
. |
|||
``` |
|||
|
|||
### 4. If the MaxMind key is not provided |
|||
|
|||
The build will succeed but nginx will log an error on startup because |
|||
`/etc/nginx/geoip/GeoLite2-ASN.mmdb` will be missing. ASN-based bot blocking will |
|||
be inactive. All other Phase 0 mitigations (UA blocklist, rate limits, ETags) still apply. |
|||
|
|||
You can mount the database file at runtime as a workaround: |
|||
|
|||
```bash |
|||
docker run \ |
|||
-v /path/to/GeoLite2-ASN.mmdb:/etc/nginx/geoip/GeoLite2-ASN.mmdb:ro \ |
|||
sapl:local |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Production — Harbor |
|||
|
|||
Official images are built and pushed through **Harbor**. Before the next release, configure |
|||
the MaxMind license key as a build secret in the Harbor / CI pipeline: |
|||
|
|||
1. Add `MAXMIND_LICENSE_KEY` as a **masked CI/CD secret** in the Harbor build project |
|||
(do not put it in any Helm values file or ConfigMap). |
|||
2. Pass it to the build step: |
|||
```bash |
|||
docker build \ |
|||
--secret id=maxmind_key,env=MAXMIND_LICENSE_KEY \ |
|||
-f docker/Dockerfile \ |
|||
-t harbor.your-registry/sapl/sapl:$VERSION \ |
|||
. |
|||
``` |
|||
Note: `env=` variant reads the secret from an environment variable instead of a file — |
|||
useful in CI where `.env` files are not present. |
|||
3. Push as normal — the key will not be present in the pushed image. |
|||
|
|||
### Keeping GeoLite2-ASN up to date |
|||
|
|||
MaxMind updates the database every Tuesday. On production hosts, install the weekly refresh |
|||
cron (run as root): |
|||
|
|||
```bash |
|||
cat > /etc/cron.weekly/update-geoip << 'EOF' |
|||
#!/bin/bash |
|||
MAXMIND_KEY="$(kubectl get secret sapl-build-secrets -n interlegis-infra \ |
|||
-o jsonpath='{.data.MAXMIND_LICENSE_KEY}' | base64 -d)" |
|||
curl -fsSL \ |
|||
"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_KEY}&suffix=tar.gz" \ |
|||
| tar -xz -C /tmp --wildcards '*.mmdb' |
|||
mv /tmp/GeoLite2-ASN_*/GeoLite2-ASN.mmdb /etc/nginx/geoip/GeoLite2-ASN.mmdb |
|||
nginx -s reload |
|||
EOF |
|||
chmod +x /etc/cron.weekly/update-geoip |
|||
``` |
|||
File diff suppressed because it is too large
@ -0,0 +1,42 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="pt-BR"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>429 – Muitas Requisições</title> |
|||
<style> |
|||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
|||
body { |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
|||
background: #f5f5f5; |
|||
color: #333; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 100vh; |
|||
} |
|||
.card { |
|||
background: #fff; |
|||
border-top: 4px solid #e74c3c; |
|||
border-radius: 4px; |
|||
padding: 2.5rem 3rem; |
|||
max-width: 480px; |
|||
width: 90%; |
|||
text-align: center; |
|||
box-shadow: 0 2px 8px rgba(0,0,0,.08); |
|||
} |
|||
.code { font-size: 4rem; font-weight: 700; color: #e74c3c; line-height: 1; } |
|||
h1 { font-size: 1.25rem; margin: .75rem 0 1rem; } |
|||
p { font-size: .95rem; line-height: 1.6; color: #555; } |
|||
.retry { margin-top: 1.5rem; font-size: .85rem; color: #888; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="card"> |
|||
<div class="code">429</div> |
|||
<h1>Muitas Requisições</h1> |
|||
<p>Você realizou muitas requisições em um curto período. Aguarde um momento e tente novamente.</p> |
|||
<p class="retry">Se o problema persistir, entre em contato com o suporte da sua Câmara Municipal.</p> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,42 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="pt-BR"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>500 – Erro Interno do Servidor</title> |
|||
<style> |
|||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
|||
body { |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
|||
background: #f5f5f5; |
|||
color: #333; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-height: 100vh; |
|||
} |
|||
.card { |
|||
background: #fff; |
|||
border-top: 4px solid #e67e22; |
|||
border-radius: 4px; |
|||
padding: 2.5rem 3rem; |
|||
max-width: 480px; |
|||
width: 90%; |
|||
text-align: center; |
|||
box-shadow: 0 2px 8px rgba(0,0,0,.08); |
|||
} |
|||
.code { font-size: 4rem; font-weight: 700; color: #e67e22; line-height: 1; } |
|||
h1 { font-size: 1.25rem; margin: .75rem 0 1rem; } |
|||
p { font-size: .95rem; line-height: 1.6; color: #555; } |
|||
.retry { margin-top: 1.5rem; font-size: .85rem; color: #888; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="card"> |
|||
<div class="code">500</div> |
|||
<h1>Erro Interno do Servidor</h1> |
|||
<p>Ocorreu um erro inesperado. Nossa equipe foi notificada e trabalhará para resolver o problema.</p> |
|||
<p class="retry">Tente novamente em alguns instantes. Se o problema persistir, entre em contato com o suporte da sua Câmara Municipal.</p> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,19 @@ |
|||
User-agent: GPTBot |
|||
Disallow: / |
|||
Crawl-delay: 10 |
|||
|
|||
User-agent: ClaudeBot |
|||
Disallow: / |
|||
Crawl-delay: 10 |
|||
|
|||
User-agent: meta-externalagent |
|||
Disallow: / |
|||
Crawl-delay: 10 |
|||
|
|||
User-agent: OAI-SearchBot |
|||
Disallow: / |
|||
Crawl-delay: 10 |
|||
|
|||
User-agent: * |
|||
Disallow: /relatorios/ |
|||
Crawl-delay: 10 |
|||
Loading…
Reference in new issue