diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..4b164b9 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,36 @@ +# 🤖 Instruções para Agentes Gemini + +Este arquivo contém diretrizes críticas para qualquer agente de IA que interaja com este repositório. + +## 🚀 Workflow de Deploy (OBRIGATÓRIO) + +**NUNCA faça commit direto na branch `producao` sem antes validar a configuração.** + +O repositório conta com um script de automação híbrido (`producao/scripts/deploy_pathfinder.py`) que deve ser usado para **todo e qualquer deploy**. + +### Passo a Passo para Agentes: + +1. **Faça suas alterações** nos arquivos de configuração (`nginx/`). +2. **Valide e Deploye** rodando o script abaixo no terminal do Windows: + ```powershell + python producao/scripts/deploy_pathfinder.py sync --all + ``` +3. **Verifique a Saída**: + - O script fará o upload, testará a configuração (`nginx -t`) no servidor e fará o reload. + - Se houver erro, **corrija antes de prosseguir**. O script fará rollback automático no servidor, mas seu código local estará "quebrado". +4. **Commit**: Somente após o sucesso do comando acima ("Deploy Remoto Concluído com Sucesso!"), faça o commit das alterações. + +## 🛠️ Comandos Úteis + +- **Sincronizar Tudo (Nginx + Fail2Ban + GeoIP)**: + `python producao/scripts/deploy_pathfinder.py sync --all` +- **Deploy de Novo Site**: + `python producao/scripts/deploy_pathfinder.py site --deploy dominio.com` +- **Atualizar GeoIP Manualmente**: + `python producao/scripts/deploy_pathfinder.py geoip --update` + +## ⚠️ Pontos de Atenção + +- **GeoIP**: O script baixa automaticamente os bancos GeoIP se faltarem. Não precisa baixar manualmente. +- **Paramiko**: O script usa `paramiko` para SSH. Se não estiver instalado, instale com `pip install paramiko`. +- **Credenciais**: As credenciais de acesso ao servidor estão embutidas no cabeçalho do script. Não as exponha em logs públicos. diff --git a/README.md b/README.md index 3f74d04..685d838 100644 --- a/README.md +++ b/README.md @@ -113,22 +113,32 @@ Siga este procedimento para colocar um novo sistema no ar com segurança máxima --- -## 🛠️ Automação de Deploy (Pathfinder Automator V2) +## 🛠️ Automação de Deploy (Pathfinder Automator V2 - Hybrid) -O Pathfinder conta com o orquestrador `scripts/deploy_pathfinder.py`, que garante operações seguras e auditadas via **Syslog**. +O Pathfinder conta com o orquestrador `scripts/deploy_pathfinder.py`, que agora funciona em modo **Híbrido (Windows Client -> Linux Server)**. Você roda o script na sua máquina local e ele faz todo o trabalho sujo. -### Comandos de Sincronização -- `python3 deploy_pathfinder.py sync --all`: Sincronização completa de configurações. -- `python3 deploy_pathfinder.py sync --file `: Sincroniza um único arquivo (ex: `snippets/security_maps.conf`) com **Backup Atômico** e Rollback se o Nginx falhar. +### Pré-requisitos +- Python 3 instalado no Windows. +- Biblioteca Paramiko: `pip install paramiko` -### Gerenciamento de Sites -- `python3 deploy_pathfinder.py site --deploy `: Workflow completo para novo site (Cria VHost, valida DNS, emite SSL e ativa renovação). -- `python3 deploy_pathfinder.py site --update `: Atualiza apenas o arquivo de configuração de um site existente. -- `python3 deploy_pathfinder.py site --remove `: Limpeza total (Remove VHost, apaga certificados SSL e **deleta todos os logs** atuais e comprimidos). +### Comandos Principais +- **`python producao/scripts/deploy_pathfinder.py sync --all`**: + - Empacota suas configs locais. + - Conecta no servidor via SSH. + - Atualiza bancos GeoIP automaticamente. + - Sincroniza configurações e recarrega o Nginx. + - **Faz Rollback Automático** se o `nginx -t` falhar. + +- **`python producao/scripts/deploy_pathfinder.py site --deploy `**: + - Sobe um novo VHost + Certificado SSL + Teste de DNS. + +- **`python producao/scripts/deploy_pathfinder.py geoip --update`**: + - Força a atualização dos bancos de dados GeoIP2 (Mirror GitHub). ### 🛡️ Segurança de Operação - **Backup & Rollback Atômico**: Cada alteração gera um `.bak`. Se `nginx -t` falhar, o script desfaz a alteração imediatamente. -- **Auditoria Syslog**: Todas as ações são registradas no syslog com o IP de origem e a função executada. +- **Auditoria Syslog**: Todas as ações são registradas no syslog do servidor. +- **Validação Local**: O script retorna `Exit Code 1` no Windows se falhar no Linux, ideal para CI/CD. - **DNS Safeguard**: O deploy de SSL só ocorre se o DNS já estiver apontando para o IP do servidor, evitando bloqueios no Let's Encrypt. --- diff --git a/scripts/deploy_pathfinder.py b/scripts/deploy_pathfinder.py index 470a629..36aa295 100644 --- a/scripts/deploy_pathfinder.py +++ b/scripts/deploy_pathfinder.py @@ -4,29 +4,53 @@ import sys import argparse import subprocess import socket -import syslog import shutil +import platform +import tarfile +import time from datetime import datetime +# Tenta importar syslog (Apenas Linux) +try: + import syslog + SYSLOG_AVAILABLE = True +except ImportError: + SYSLOG_AVAILABLE = False + +# Tenta importar paramiko (Apenas para Cliente Windows) +try: + import paramiko + PARAMIKO_AVAILABLE = True +except ImportError: + PARAMIKO_AVAILABLE = False + # ============================================================================== # CONFIGURAÇÕES TÉCNICAS # ============================================================================== +# Credenciais +SERVER_HOST = "172.17.0.253" +SERVER_USER = "itguys" PASSWORD = "vR7Ag$Pk" + +# Caminhos (Linux Server) NGINX_CONF_DIR = "/etc/nginx" -NGINX_CONF_BACKUP = "/etc/nginx.bak" FAIL2BAN_CONF_DIR = "/etc/fail2ban" TMP_SYNC_BASE = "/tmp/pathfinder_sync" LOG_DIR = "/var/log/nginx" # Endereço IP Público do Host de Produção (para validação DNS) -HOST_PUBLIC_IP = "" +HOST_PUBLIC_IP = "177.104.182.28" # ============================================================================== -# UTILITÁRIOS DE SISTEMA E AUDITORIA +# UTILITÁRIOS DE SISTEMA E AUDITORIA (SERVER-SIDE) # ============================================================================== def log_syslog(task, function, details=""): - """Registra a ação no Syslog para auditoria.""" + """Registra a ação no Syslog para auditoria (Apenas Linux).""" + if not SYSLOG_AVAILABLE: + print(f"[*] [SYSLOG_MOCK] {task} | {function} | {details}") + return + try: hostname = socket.gethostname() remote_ip = os.environ.get('SSH_CLIENT', 'localhost').split()[0] @@ -59,7 +83,7 @@ def check_nginx(): return rc == 0, err # ============================================================================== -# LÓGICA DE BACKUP E ROLLBACK ATÔMICO +# LÓGICA DE BACKUP E ROLLBACK ATÔMICO (SERVER-SIDE) # ============================================================================== BACKUP_MAP = {} # Rastreia arquivos alterados para rollback @@ -88,7 +112,7 @@ def rollback_all(): check_nginx() # ============================================================================== -# AUXILIARES DE REDE (DNS/IP/SSL) +# AUXILIARES DE REDE (DNS/IP/SSL) (SERVER-SIDE) # ============================================================================== def get_public_ip(): @@ -103,14 +127,7 @@ def get_public_ip(): HOST_PUBLIC_IP = response.read().decode('utf-8') return HOST_PUBLIC_IP except: - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - HOST_PUBLIC_IP = s.getsockname()[0] - s.close() - return HOST_PUBLIC_IP - except: - return "127.0.0.1" + return "127.0.0.1" def validate_dns(domain): """Verifica se o domínio aponta para este host.""" @@ -145,21 +162,147 @@ def setup_ssl(domain): return False # ============================================================================== -# FUNCIONALIDADES DO SCRIPT +# CLIENTE WINDOWS (REMOTE DEPLOY) +# ============================================================================== + +def create_tarball(source_dirs, output_filename="deploy_package.tar.gz"): + """Cria um tar.gz dos diretórios selecionados.""" + print(f"[*] Criando pacote de deploy: {output_filename}") + with tarfile.open(output_filename, "w:gz") as tar: + for folder in source_dirs: + if os.path.exists(folder): + print(f" - Adicionando: {folder}") + tar.add(folder, arcname=os.path.basename(folder)) + else: + print(f"[!] AVISO: Pasta não encontrada: {folder}") + +def windows_deploy(args): + """Orquestra o deploy remoto a partir do Windows.""" + if not PARAMIKO_AVAILABLE: + print("[!] Erro: Biblioteca 'paramiko' não instalada.") + print(" Execute: pip install paramiko") + sys.exit(1) + + print("="*60) + print(f"🚀 PATHFINDER REMOTE DEPLOYER - Target: {SERVER_HOST}") + print("="*60) + + # 1. Definir caminhos locais relativos à raiz do projeto + # Assume que o script está sendo rodado da raiz ou de producao/scripts + base_dir = os.getcwd() + if os.path.basename(base_dir) == "scripts": + base_dir = os.path.dirname(os.path.dirname(base_dir)) # Sobe para raiz + elif os.path.basename(base_dir) == "producao": + base_dir = os.path.dirname(base_dir) # Sobe para raiz + + prod_dir = os.path.join(base_dir, "producao") + nginx_dir = os.path.join(prod_dir, "nginx") + scripts_dir = os.path.join(prod_dir, "scripts") + + if not os.path.exists(nginx_dir): + print(f"[!] Erro: Não encontrei a pasta 'nginx' em {prod_dir}") + sys.exit(1) + + # 2. Empacotar arquivos + tar_name = "pathfinder_deploy.tar.gz" + create_tarball([nginx_dir, scripts_dir], tar_name) + + # 3. Conexão SSH + print(f"[*] Conectando em {SERVER_HOST} como {SERVER_USER}...") + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(SERVER_HOST, username=SERVER_USER, password=PASSWORD) + + # 4. Upload SFTP + print(f"[*] Uploading {tar_name} para /tmp/...") + sftp = ssh.open_sftp() + sftp.put(tar_name, f"/tmp/{tar_name}") + sftp.close() + + # 5. Execução Remota + # Reconstrói os argumentos passados para o script local + remote_args = " ".join(sys.argv[1:]) + if not remote_args: + remote_args = "--help" + + print(f"[*] Executando remotamente: deploy_pathfinder.py {remote_args}") + print("-" * 60) + + # Sequência de comandos remotos (Redirecionando erro para arquivo) + remote_cmd = ( + f"bash -c 'cd /tmp && " + f"tar -xzf {tar_name} && " + f"sudo -S -p \"\" python3 scripts/deploy_pathfinder.py {remote_args} > /tmp/deployment_error.log 2>&1'" + ) + + stdin, stdout, stderr = ssh.exec_command(remote_cmd, get_pty=False) + + # Envia senha para o sudo se solicitado + stdin.write(PASSWORD + "\n") + stdin.flush() + + # Stream de saída (stdout) + while True: + if stdout.channel.recv_ready(): + output = stdout.channel.recv(1024).decode('utf-8', errors='replace') + print(output, end="") + if stdout.channel.exit_status_ready() and not stdout.channel.recv_ready(): + break + time.sleep(0.1) + + exit_status = stdout.channel.recv_exit_status() + + if exit_status != 0: + print(f"\n❌ Falha no Deploy Remoto (Exit Code: {exit_status})") + print("--- ERRO DETALHADO DO SERVIDOR ---") + # Ler arquivo de log remoto + _, err_stdout, _ = ssh.exec_command("cat /tmp/deployment_error.log") + print(err_stdout.read().decode('utf-8')) + print("----------------------------------") + else: + print(f"\n✅ Deploy Remoto Concluído com Sucesso!") + + ssh.close() + + # Limpeza local + if os.path.exists(tar_name): + os.remove(tar_name) + + except Exception as e: + print(f"\n[!] Erro fatal na conexão SSH: {e}") + sys.exit(1) + +# ============================================================================== +# FUNCIONALIDADES DO SCRIPT (SERVER-SIDE LÓGICA) # ============================================================================== def sync_all(): """Sincronização completa (legado).""" log_syslog("SYNC", "sync_all", "Sincronização total de Nginx e Fail2Ban") backup_file(NGINX_CONF_DIR) - backup_file(FAIL2BAN_CONF_DIR) - src_nginx = os.path.join(TMP_SYNC_BASE, "nginx", ".") - run_sudo(['cp', '-rf', src_nginx, NGINX_CONF_DIR]) + # Ajuste: No modo remoto, os arquivos são descompactados em /tmp/nginx diretamente + # Se rodar manual, assume TMP_SYNC_BASE antigo + # Verifica onde estão os arquivos fontes (Prioridade para o pacote descompactado em /tmp) + src_nginx = "/tmp/nginx" + if not os.path.exists(src_nginx): + src_nginx = os.path.join(TMP_SYNC_BASE, "nginx") + + if not os.path.exists(src_nginx): + print(f"[!] Erro: Diretório fonte não encontrado em /tmp/nginx ou {TMP_SYNC_BASE}") + return False + + print(f"[*] Sincronizando de: {src_nginx}") + run_sudo(['cp', '-rf', os.path.join(src_nginx, "."), NGINX_CONF_DIR]) + + # Atualiza GeoIP se solicitado (ou sempre no sync --all) + update_geoip_db() + ok, err = check_nginx() if not ok: - print(f"[!] Erro na configuração: {err}") + print(f"[!] Erro na configuração (Nginx -t falhou): {err}") rollback_all() return False @@ -167,9 +310,56 @@ def sync_all(): print("[+] Sincronização total concluída com sucesso.") return True +def update_geoip_db(): + """Baixa os bancos de dados GeoIP mais recentes (Mirror GitHub).""" + # URLs de Mirror Confiáveis (P3TERX ou similar) + GEOIP_URLS = { + "GeoLite2-Country.mmdb": "https://git.io/GeoLite2-Country.mmdb", + "GeoLite2-City.mmdb": "https://git.io/GeoLite2-City.mmdb" + } + GEOIP_DIR = "/usr/share/GeoIP" + + print("[*] Verificando bancos de dados GeoIP2...") + + if not os.path.exists(GEOIP_DIR): + run_sudo(['mkdir', '-p', GEOIP_DIR]) + + import urllib.request + + for db_name, url in GEOIP_URLS.items(): + target = os.path.join(GEOIP_DIR, db_name) + # Lógica simplificada: Baixar sempre no sync --all para garantir atualização + # Para produção real, poderia checar headers ETag/Last-Modified, mas o mirror redireciona. + + print(f" - Baixando {db_name}...") + try: + # Baixa para /tmp primeiro + tmp_target = os.path.join("/tmp", db_name) + + # Usar curl é mais robusto em ambientes restritos que o urllib do python + # Segue redirecionamentos (-L) e falha em erro (-f) + rc, out, err = run_sudo(['curl', '-L', '-f', '-o', tmp_target, url]) + + if rc == 0: + run_sudo(['mv', '-f', tmp_target, target]) + run_sudo(['chmod', '644', target]) + print(f" [OK] Atualizado: {target}") + else: + print(f" [!] Falha no download de {db_name}: {err}") + + except Exception as e: + print(f" [!] Erro ao atualizar GeoIP: {e}") + def sync_item(relative_path): - """Sincroniza um arquivo ou diretório (ex: snippets/ ou modsec/).""" - src = os.path.join(TMP_SYNC_BASE, "nginx", relative_path) + """Sincroniza um arquivo ou diretório.""" + # Definição do source (Dual mode: /tmp/nginx direto ou TMP_SYNC_BASE) + src_base_1 = "/tmp/nginx" + src_base_2 = os.path.join(TMP_SYNC_BASE, "nginx") + + src = os.path.join(src_base_1, relative_path) + if not os.path.exists(src): + src = os.path.join(src_base_2, relative_path) + dst = os.path.join(NGINX_CONF_DIR, relative_path) if not os.path.exists(src): @@ -177,11 +367,8 @@ def sync_item(relative_path): return False log_syslog("SYNC_ITEM", "sync_item", f"Sincronizando {relative_path}") - - # Backup recursivo se for diretório ou arquivo backup_file(dst) - # Usa -rf para suportar diretórios (como modsec/) if os.path.isdir(src): run_sudo(['cp', '-rf', os.path.join(src, '.'), dst]) else: @@ -189,7 +376,7 @@ def sync_item(relative_path): ok, err = check_nginx() if not ok: - print(f"[!] Falha na validação após sincronizar {relative_path}. Revertendo...") + print(f"[!] Falha na validação. Revertendo...") rollback_all() return False @@ -199,11 +386,15 @@ def sync_item(relative_path): def site_deploy(domain): """Deploy completo de um novo site.""" - src_vhost = os.path.join(TMP_SYNC_BASE, "nginx", "conf.d", f"{domain}.conf") + # Busca fonte + src_vhost = os.path.join("/tmp/nginx/conf.d", f"{domain}.conf") + if not os.path.exists(src_vhost): + src_vhost = os.path.join(TMP_SYNC_BASE, "nginx", "conf.d", f"{domain}.conf") + dst_vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf") if not os.path.exists(src_vhost): - print(f"[!] Arquivo de VHost não encontrado em: {src_vhost}") + print(f"[!] Arquivo de VHost não encontrado: {src_vhost}") return False log_syslog("SITE_DEPLOY", "site_deploy", f"Iniciando deploy de {domain}") @@ -220,8 +411,8 @@ def site_deploy(domain): dns_ok, domain_ip = validate_dns(domain) if not dns_ok: - print(f"[!] AVISO: DNS de {domain} ({domain_ip}) não aponta para este host ({get_public_ip()}).") - print("[!] SSL Certbot será pulado. Rode 'site --update' após corrigir o DNS.") + print(f"[!] AVISO: DNS de {domain} ({domain_ip}) incorreto.") + print("[!] SSL Certbot será pulado.") return True setup_ssl(domain) @@ -240,18 +431,14 @@ def site_update(domain): def site_remove(domain): """Remove site, SSL e Logs.""" log_syslog("SITE_REMOVE", "site_remove", f"Removendo site {domain}") - - # 1. Nginx Config vhost = os.path.join(NGINX_CONF_DIR, "conf.d", f"{domain}.conf") if os.path.exists(vhost): backup_file(vhost) run_sudo(['rm', '-f', vhost]) - # 2. SSL Certbot print(f"[*] Removendo certificados para {domain}...") run_sudo(['certbot', 'delete', '--cert-name', domain]) - # 3. Logs (Atuais e GZ) print(f"[*] Limpando logs de {domain}...") run_sudo(['bash', '-c', f"rm -f {LOG_DIR}/{domain}*"]) @@ -267,6 +454,13 @@ def site_remove(domain): # ============================================================================== def main(): + # Detecta SO para decidir modo de operação + if platform.system() == "Windows": + # Modo Cliente (Windows) + windows_deploy(sys.argv) + return + + # Modo Servidor (Linux) parser = argparse.ArgumentParser(description="Pathfinder Automator V2 - Nginx/SSL Orchestration") subparsers = parser.add_subparsers(dest="command", help="Comando a executar") @@ -279,6 +473,9 @@ def main(): site_parser.add_argument("--update", type=str, help="Atualizar site existente (Domínio)") site_parser.add_argument("--remove", type=str, help="Remover site completamente (Domínio)") + geoip_parser = subparsers.add_parser("geoip", help="Gerenciamento de GeoIP") + geoip_parser.add_argument("--update", action="store_true", help="Baixar/Atualizar bancos GeoIP") + args = parser.parse_args() if args.command == "sync": @@ -290,9 +487,14 @@ def main(): if args.deploy: site_deploy(args.deploy) elif args.update: site_update(args.update) elif args.remove: site_remove(args.remove) + elif args.command == "geoip": + if args.update: + update_geoip_db() if __name__ == "__main__": - if os.getuid() == 0: - print("[!] Não execute diretamente como root. Use um usuário com sudo.") + try: + main() + except KeyboardInterrupt: + print("\n[!] Operação cancelada pelo usuário.") sys.exit(1) - main() +