Compare commits
2 Commits
be7b271357
...
e4a4714ee5
| Author | SHA1 | Date |
|---|---|---|
|
|
e4a4714ee5 | |
|
|
a5788fc66d |
|
|
@ -93,7 +93,24 @@ O repositório conta com um script de automação híbrido (`producao/scripts/de
|
|||
- **Causa**: O arquivo `snippets/modsecurity.conf` já define `modsecurity_rules_file`. Se você incluir esse snippet E também definir a diretiva `modsecurity_rules_file` no bloco `server` (ex: `ferreirareal.com.br.conf`), o Nginx falhará.
|
||||
- **Solução**: Use apenas `include snippets/modsecurity.conf;` no bloco server. A diretiva `modsecurity_rules_file /etc/nginx/modsec/main.conf;` deve ficar comentada ou removida do vhost.
|
||||
|
||||
### 2. Falhas Silenciosas de Rollback
|
||||
### 2. Scripts de Diagnóstico e Recuperação (2026-02-08)
|
||||
|
||||
Foram criados scripts auxiliares em `producao/scripts/` para situações de emergência ou validação profunda. Use-os com cautela:
|
||||
|
||||
- **`restore_nginx.py`**:
|
||||
- **Função**: Força o upload do `nginx.conf` local para o servidor e reinicia o serviço.
|
||||
- **Uso**: `python producao/scripts/restore_nginx.py`
|
||||
- **Quando usar**: Se o `deploy_pathfinder.py` falhar ou se o Nginx não subir por erro de configuração crítica (ex: variáveis faltando).
|
||||
- **`fetch_logs.py`**:
|
||||
- **Função**: Baixa logs específicos do servidor para análise local.
|
||||
- **Uso**: `python producao/scripts/fetch_logs.py`
|
||||
- **Quando usar**: Para investigar ataques ou erros sem precisar logar via SSH.
|
||||
- **`verify_time_and_logs.py`**:
|
||||
- **Função**: Verifica a data do servidor e os últimos logs de acesso.
|
||||
- **Uso**: `python producao/scripts/verify_time_and_logs.py`
|
||||
- **Quando usar**: Para confirmar se o timezone está correto (-0300) e se o Nginx está gerando logs novos.
|
||||
|
||||
### 3. Falhas Silenciosas de Rollback
|
||||
- **Cuidado**: O script `deploy_pathfinder.py` executa um rollback automático se `nginx -t` falhar.
|
||||
- **O que acontece**: O script restaura o backup anterior e reinicia o Nginx. Isso faz o deploy parecer bem-sucedido (exit code 0), mas seus arquivos novos foram descartados.
|
||||
- **Verificação**: **SEMPRE** verifique o timestamp dos arquivos remotos após um deploy crítico para garantir que foram atualizados:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ A configuração é modular para permitir manutenção rápida e alta disponibil
|
|||
- `snippets/`: Componentes reutilizáveis (SSL, Proxy, Cache, WAF, Headers).
|
||||
- `modsec/`: Configuração do ModSecurity, regras **OWASP CRS v4** e tunings específicos.
|
||||
- `dynamic/`: Arquivos modificados em tempo real (ex: `blacklist.conf` pelo Fail2Ban).
|
||||
- `scripts/`: Scripts de utilidade (Ex: `install_modsecurity.sh`).
|
||||
- `scripts/`: Scripts de automação e diagnóstico (`deploy_pathfinder.py`, `restore_nginx.py`, `fetch_logs.py`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -78,9 +78,9 @@ O Pathfinder Proxy utiliza o **ModSecurity v3** compilado sob medida para o Ngin
|
|||
|
||||
---
|
||||
|
||||
## 🧠 Motor de Segurança PSDE "Elite" (7-Vector Matrix)
|
||||
## 🧠 Motor de Segurança PSDE "Elite" (8-Vector Matrix)
|
||||
|
||||
Diferente de firewalls comuns, o Pathfinder utiliza uma **Matriz de Pontuação Combinatória** no `security_maps.conf` que analisa 7 vetores simultâneos:
|
||||
Diferente de firewalls comuns, o Pathfinder utiliza uma **Matriz de Pontuação Combinatória** no `security_maps.conf` que analisa 8 vetores simultâneos:
|
||||
|
||||
1. **🛡️ Bot:** Bloqueio de 600+ user-agents maliciosos.
|
||||
2. **🌐 URI:** Acesso a arquivos sensíveis e assinaturas de CVEs recentes.
|
||||
|
|
@ -89,9 +89,10 @@ Diferente de firewalls comuns, o Pathfinder utiliza uma **Matriz de Pontuação
|
|||
5. **🌍 Geo:** Risco por país (CN, RU, KP, IR) via **GeoIP2**.
|
||||
6. **🚦 Protocol:** Violações como User-Agents vazios ou falsificados.
|
||||
7. **🔗 Referer:** 400+ domínios de spam e phishing bloqueados instantaneamente.
|
||||
8. **🤯 Header:** Detecção de anomalias em cabeçalhos customizados (ex: React2Shell CVE-2025-55182).
|
||||
|
||||
### 📈 Lógica de Decisão
|
||||
- **Nivel 3 (ATAQUE_CRITICO)**: Payloads maliciosos, Referer Spam ou qualquer combinação de 3+ vetores.
|
||||
- **Nivel 3 (ATAQUE_CRITICO)**: Payloads maliciosos, Referer Spam, Headers Corrompidos ou combinação de 3+ vetores.
|
||||
- **Nivel 2 (RISCO_ALTO)**: Combinação de 2 vetores de risco (ex: Bot + Geo-Risco).
|
||||
- **Nivel 1 (SUSPEITO)**: Detecção de sinais individuais.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
[Definition]
|
||||
actionstart = touch /etc/nginx/snippets/blacklist.conf
|
||||
actionstop =
|
||||
actioncheck =
|
||||
actionban = echo "deny <ip>;" >> /etc/nginx/dynamic/blacklist.conf && docker exec nginx-proxy nginx -s reload
|
||||
actionunban = sed -i "/deny <ip>;/d" /etc/nginx/dynamic/blacklist.conf && docker exec nginx-proxy nginx -s reload
|
||||
|
|
@ -9,7 +9,8 @@ enabled = true
|
|||
port = http,https
|
||||
filter = nginx-pathfinder-filter
|
||||
action = nginx-pathfinder-action
|
||||
logpath = /var/log/nginx/access_json.log
|
||||
logpath = /var/log/nginx/*access.log
|
||||
/var/log/nginx/access_json.log
|
||||
backend = auto
|
||||
|
||||
# --- Política de Tolerância Zero ---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
[INCLUDES]
|
||||
before = paths-debian.conf
|
||||
|
||||
[DEFAULT]
|
||||
# Ignora IPs locais e da rede interna
|
||||
ignoreip = 127.0.0.1/8 ::1 172.16.0.0/12 10.0.0.0/8 192.168.0.0/16
|
||||
|
||||
# Backend padrao para Ubuntu/Debian
|
||||
backend = systemd
|
||||
|
||||
# Logpath padrao
|
||||
var_log_path = /var/log
|
||||
|
||||
# Email de destino
|
||||
destemail = root@localhost
|
||||
sender = root@<fq-hostname>
|
||||
|
||||
# Acao padrao
|
||||
banaction = iptables-multiport
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[Definition]
|
||||
failregex = ^.*"remote_addr":"<ADDR>".*"block_request":"1".*$
|
||||
^.*"remote_addr":"<ADDR>".*"status":403.*$
|
||||
^.*"remote_addr":"<ADDR>".*"status":404.*$
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[nginx-modsec]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-modsec
|
||||
logpath = /var/log/nginx/access_json.log
|
||||
maxretry = 3
|
||||
bantime = 3600
|
||||
findtime = 600
|
||||
action = nginx-blacklist
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
upstream chatwoot_pralog_backend {
|
||||
server 172.17.0.3:8082;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name atendimento.grupopralog.com.br;
|
||||
include snippets/acme_challenge.conf;
|
||||
location / {
|
||||
return 301 https://atendimento.grupopralog.com.br$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 quic;
|
||||
listen 443 ssl;
|
||||
server_name atendimento.grupopralog.com.br;
|
||||
|
||||
# Certificados (Ajustar apos geracao via Certbot se necessario)
|
||||
ssl_certificate /etc/letsencrypt/live/atendimento.grupopralog.com.br/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/atendimento.grupopralog.com.br/privkey.pem;
|
||||
|
||||
access_log /var/log/nginx/atendimento.grupopralog.com.br.access.log detailed_proxy;
|
||||
error_log /var/log/nginx/atendimento.grupopralog.com.br.error.log warn;
|
||||
|
||||
include snippets/ssl_params.conf;
|
||||
include snippets/proxy_params.conf;
|
||||
include snippets/modsecurity.conf;
|
||||
include snippets/well_known.conf;
|
||||
include snippets/security_actions.conf;
|
||||
|
||||
proxy_cache dynamic_cache;
|
||||
set $upstream_proto http;
|
||||
set $upstream_app chatwoot_pralog_backend;
|
||||
|
||||
# Recomendacao Chatwoot/Twilio: Permitir underscores em headers para webhooks
|
||||
underscores_in_headers on;
|
||||
|
||||
location / {
|
||||
expires 15m;
|
||||
proxy_cache_valid 200 15m;
|
||||
proxy_pass http://chatwoot_pralog_backend;
|
||||
|
||||
# Force HTTPS for Rails
|
||||
include snippets/proxy_params.conf;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
}
|
||||
|
||||
# Configuracao obrigatoria para Real-time (WebSockets)
|
||||
location /cable {
|
||||
proxy_pass http://chatwoot_pralog_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
# NGINX Master Configuration - Pathfinder Proxy
|
||||
# Nginx Master Configuration - Pathfinder Proxy
|
||||
env TZ=America/Sao_Paulo;
|
||||
|
||||
# Load essential modules
|
||||
load_module modules/ngx_http_modsecurity_module.so;
|
||||
|
|
@ -62,7 +63,7 @@ http {
|
|||
|
||||
# --- HTTP/2 Hardening (CVE-2025-8671: MadeYouReset Mitigation) ---
|
||||
http2_max_concurrent_streams 64;
|
||||
http2_idle_timeout 3m;
|
||||
|
||||
keepalive_requests 500;
|
||||
|
||||
# 2. Conexões & Timeouts
|
||||
|
|
@ -89,7 +90,7 @@ http {
|
|||
access_log /var/log/nginx/access_json.log detailed_proxy;
|
||||
|
||||
# Logging Human Readable - For Docker Logs / User
|
||||
access_log /dev/stdout combined;
|
||||
# access_log /dev/stdout combined;
|
||||
|
||||
# SSL Settings (Global)
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
|
|
|
|||
|
|
@ -203,9 +203,11 @@ def windows_deploy(args):
|
|||
print(f"[!] Erro: Não encontrei a pasta 'nginx' em {prod_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
fail2ban_dir = os.path.join(prod_dir, "fail2ban")
|
||||
|
||||
# 2. Empacotar arquivos
|
||||
tar_name = "pathfinder_deploy.tar.gz"
|
||||
create_tarball([nginx_dir, scripts_dir], tar_name)
|
||||
create_tarball([nginx_dir, scripts_dir, fail2ban_dir], tar_name)
|
||||
|
||||
# 3. Conexão SSH
|
||||
print(f"[*] Conectando em {SERVER_HOST} como {SERVER_USER}...")
|
||||
|
|
@ -310,6 +312,10 @@ def sync_all():
|
|||
return False
|
||||
|
||||
run_sudo(['systemctl', 'reload', 'nginx'])
|
||||
|
||||
# Sincroniza Fail2Ban também
|
||||
sync_fail2ban()
|
||||
|
||||
print("[+] Sincronização total concluída com sucesso.")
|
||||
return True
|
||||
|
||||
|
|
@ -452,6 +458,60 @@ def site_remove(domain):
|
|||
else:
|
||||
rollback_all()
|
||||
|
||||
# ==============================================================================
|
||||
# FAIL2BAN LOGIC
|
||||
# ==============================================================================
|
||||
|
||||
def sync_fail2ban():
|
||||
"""Sincroniza configurações do Fail2Ban."""
|
||||
# Definição do source (Dual mode: /tmp/fail2ban direto ou TMP_SYNC_BASE)
|
||||
# A estrutura do tarball é fail2ban/data/fail2ban/...
|
||||
src_base_1 = "/tmp/fail2ban/data/fail2ban"
|
||||
src_base_2 = os.path.join(TMP_SYNC_BASE, "fail2ban", "data", "fail2ban")
|
||||
|
||||
src = src_base_1
|
||||
if not os.path.exists(os.path.join(src, "jail.local")):
|
||||
# Tenta o segundo caminho (caso não tenha sido extraído do tarball padrão)
|
||||
# Ou se o tarball foi gerado de forma diferente
|
||||
if os.path.exists(os.path.join("/tmp/fail2ban", "jail.local")):
|
||||
src = "/tmp/fail2ban"
|
||||
else:
|
||||
src = src_base_2
|
||||
|
||||
if not os.path.exists(src):
|
||||
print(f"[!] Diretório fonte do Fail2Ban não encontrado: {src}")
|
||||
# Tenta achar onde foi parar
|
||||
print(f" (DEBUG) Procurando em {src_base_1} e {src_base_2}")
|
||||
return False
|
||||
|
||||
print(f"[*] Sincronizando Fail2Ban de: {src}")
|
||||
|
||||
# Lista de pastas/arquivos a sincronizar para /etc/fail2ban
|
||||
# Não sobrescrevemos jail.conf para respeitar o mantenedor do pacote
|
||||
targets = ["jail.local", "jail.d", "filter.d", "action.d", "paths-common.conf", "paths-lsio.conf"]
|
||||
|
||||
for item in targets:
|
||||
s_item = os.path.join(src, item)
|
||||
d_item = os.path.join(FAIL2BAN_CONF_DIR, item)
|
||||
|
||||
if os.path.exists(s_item):
|
||||
log_syslog("SYNC_F2B", "sync_fail2ban", f"Copiando {item}")
|
||||
if os.path.isdir(s_item):
|
||||
run_sudo(['cp', '-rf', os.path.join(s_item, "."), d_item])
|
||||
else:
|
||||
run_sudo(['cp', '-f', s_item, d_item])
|
||||
else:
|
||||
print(f" [!] Item não encontrado na origem: {item} (Ignorado)")
|
||||
|
||||
print("[*] Reiniciando serviço Fail2Ban...")
|
||||
rc, out, err = run_sudo(['systemctl', 'restart', 'fail2ban'])
|
||||
if rc != 0:
|
||||
print(f"[!] Erro ao reiniciar Fail2Ban: {err}")
|
||||
return False
|
||||
|
||||
print("[+] Fail2Ban sincronizado e reiniciado.")
|
||||
return True
|
||||
|
||||
# ==============================================================================
|
||||
# CLI HANDLER
|
||||
# ==============================================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import paramiko
|
||||
import os
|
||||
import time
|
||||
|
||||
SERVER_HOST = "172.17.0.253"
|
||||
SERVER_USER = "itguys"
|
||||
PASSWORD = "vR7Ag$Pk"
|
||||
|
||||
def fetch_log(remote_path, local_path):
|
||||
print(f"[*] Fetching {remote_path} -> {local_path}")
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
client.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD)
|
||||
|
||||
# Usando sudo cat para garantir acesso
|
||||
stdin, stdout, stderr = client.exec_command(f"echo '{PASSWORD}' | sudo -S tail -n 50 {remote_path}", get_pty=False)
|
||||
|
||||
# Captura a saída
|
||||
content = stdout.read().decode('utf-8')
|
||||
err = stderr.read().decode('utf-8')
|
||||
|
||||
if content:
|
||||
with open(local_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f" [OK] Salvo em {local_path}")
|
||||
else:
|
||||
print(f" [!] Nenhum conteúdo ou erro: {err}")
|
||||
|
||||
client.close()
|
||||
except Exception as e:
|
||||
print(f" [!] Erro: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not os.path.exists("logs"):
|
||||
os.makedirs("logs")
|
||||
|
||||
fetch_log("/etc/nginx/nginx.conf", "logs/remote_nginx.conf")
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD)
|
||||
fetch_log("/var/log/nginx/ferreirareal.com.br.access.log", "logs/ferreirareal.access.log")
|
||||
fetch_log("/var/log/nginx/access.log", "logs/global.access.log")
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import paramiko
|
||||
import os
|
||||
|
||||
SERVER_HOST = "172.17.0.253"
|
||||
SERVER_USER = "itguys"
|
||||
PASSWORD = "vR7Ag$Pk"
|
||||
|
||||
def restore_nginx():
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD)
|
||||
|
||||
local_file = "producao/nginx/nginx.conf"
|
||||
remote_tmp = "/tmp/nginx.conf"
|
||||
dest_file = "/etc/nginx/nginx.conf"
|
||||
|
||||
# 1. Upload to /tmp
|
||||
print(f"[*] Uploading {local_file} -> {remote_tmp}")
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(local_file, remote_tmp)
|
||||
sftp.close()
|
||||
|
||||
# 2. Check File Difference (Optional but good for log)
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S diff {remote_tmp} {dest_file}"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
# print(stdout.read().decode()) # Might be large
|
||||
|
||||
# 3. Move to /etc/nginx/
|
||||
print(f"[*] Overwriting {dest_file}...")
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S cp {remote_tmp} {dest_file}"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
print(stderr.read().decode())
|
||||
|
||||
# 4. Test Config
|
||||
print("[*] Testing Nginx Config...")
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S nginx -t"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
out = stdout.read().decode()
|
||||
err = stderr.read().decode()
|
||||
print(out)
|
||||
print(err)
|
||||
|
||||
if "successful" in err or "successful" in out:
|
||||
# 5. Restart
|
||||
print("[*] Restarting Nginx...")
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S systemctl restart nginx"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
print(stderr.read().decode())
|
||||
print("[*] DONE. Service should be UP.")
|
||||
else:
|
||||
print("[!] Config Test Failed. Not restarting.")
|
||||
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
restore_nginx()
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import paramiko
|
||||
import time
|
||||
|
||||
SERVER_HOST = "172.17.0.253"
|
||||
SERVER_USER = "itguys"
|
||||
PASSWORD = "vR7Ag$Pk"
|
||||
|
||||
def verify():
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD)
|
||||
|
||||
with open("logs/verify_logs.txt", "w", encoding="utf-8") as f:
|
||||
f.write("--- Server Date ---\n")
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S date"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
f.write(stdout.read().decode())
|
||||
|
||||
f.write("\n--- Generating Request ---\n")
|
||||
cmd = "curl -v -k -I https://127.0.0.1/robots.txt"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
f.write(stdout.read().decode())
|
||||
f.write(stderr.read().decode())
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
f.write("\n--- Latest Log Entries ---\n")
|
||||
cmd = f"echo '{PASSWORD}' | sudo -S tail -n 2 /var/log/nginx/access_json.log"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
f.write(stdout.read().decode())
|
||||
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify()
|
||||
Loading…
Reference in New Issue