From a94809c812baa7c1830d947b369e31d265a217b5 Mon Sep 17 00:00:00 2001 From: bigtux Date: Thu, 5 Feb 2026 23:02:06 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20Security=20hardening:=20auth,=20?= =?UTF-8?q?rate=20limiting,=20brute=20force=20protection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive security package with: - API Key generation and validation (SHA256 hash) - Password policy enforcement (min 12 chars, complexity) - Rate limiting with presets (auth, api, ingest, export) - Brute force protection (5 attempts, 15min lockout) - Security headers middleware - IP whitelisting - Audit logging structure - Secure token generation - Enhanced auth middleware: - JWT + API Key dual authentication - Token revocation via Redis - Scope-based authorization - Role-based access control - Updated installer with: - Interactive setup for client customization - Auto-generated secure credentials - Docker all-in-one image - Agent installer script - Added documentation: - SECURITY.md - Complete security guide - INSTALL.md - Installation guide - .env.example - Configuration reference --- .env.example | 75 +++ deploy/docker/Dockerfile | 119 +++++ deploy/docker/entrypoint.sh | 35 ++ deploy/docker/supervisord.conf | 26 + docs/INSTALL.md | 485 ++++++------------ docs/SECURITY.md | 270 ++++++++++ go.mod | 4 +- install.sh | 867 +++++++++++++++++++++++++++++++-- internal/api/ratelimit.go | 258 +++++++++- internal/auth/middleware.go | 384 +++++++++++++-- internal/security/security.go | 558 +++++++++++++++++++++ 11 files changed, 2637 insertions(+), 444 deletions(-) create mode 100644 .env.example create mode 100644 deploy/docker/Dockerfile create mode 100644 deploy/docker/entrypoint.sh create mode 100644 deploy/docker/supervisord.conf create mode 100644 docs/SECURITY.md create mode 100644 internal/security/security.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..239fb10 --- /dev/null +++ b/.env.example @@ -0,0 +1,75 @@ +# ═══════════════════════════════════════════════════════════ +# 🐍 OPHION - Configuração de Exemplo +# Copie para .env e preencha os valores +# ═══════════════════════════════════════════════════════════ + +# ───────────────────────────────────────────────────────────── +# Organização +# ───────────────────────────────────────────────────────────── +ORG_NAME="Minha Empresa" +ADMIN_EMAIL=admin@empresa.com +ADMIN_PASSWORD=mude-esta-senha + +# ───────────────────────────────────────────────────────────── +# Rede +# ───────────────────────────────────────────────────────────── +DOMAIN=localhost +SERVER_PORT=8080 +DASHBOARD_PORT=3000 +API_URL=http://ophion-server:8080 + +# ───────────────────────────────────────────────────────────── +# Segurança (GERE VALORES ÚNICOS!) +# Use: openssl rand -hex 32 +# ───────────────────────────────────────────────────────────── +JWT_SECRET=MUDE-ISTO-openssl-rand-hex-32 +API_KEY=ophion_MUDE-ISTO-openssl-rand-hex-32 + +# ───────────────────────────────────────────────────────────── +# PostgreSQL +# ───────────────────────────────────────────────────────────── +POSTGRES_USER=ophion +POSTGRES_PASSWORD=MUDE-ISTO-senha-segura +POSTGRES_DB=ophion +DATABASE_URL=postgres://ophion:MUDE-ISTO-senha-segura@postgres:5432/ophion + +# ───────────────────────────────────────────────────────────── +# ClickHouse +# ───────────────────────────────────────────────────────────── +CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion + +# ───────────────────────────────────────────────────────────── +# Redis +# ───────────────────────────────────────────────────────────── +REDIS_URL=redis://redis:6379 + +# ───────────────────────────────────────────────────────────── +# Alertas - Telegram (opcional) +# ───────────────────────────────────────────────────────────── +TELEGRAM_ENABLED=false +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= + +# ───────────────────────────────────────────────────────────── +# Alertas - Slack (opcional) +# ───────────────────────────────────────────────────────────── +SLACK_ENABLED=false +SLACK_WEBHOOK_URL= + +# ───────────────────────────────────────────────────────────── +# IA - OpenAI (opcional, para recursos de IA) +# ───────────────────────────────────────────────────────────── +OPENAI_API_KEY= + +# ───────────────────────────────────────────────────────────── +# Configurações Gerais +# ───────────────────────────────────────────────────────────── +TZ=America/Sao_Paulo +LOG_LEVEL=info + +# ───────────────────────────────────────────────────────────── +# Retenção de Dados +# ───────────────────────────────────────────────────────────── +METRICS_RETENTION_DAYS=90 +LOGS_RETENTION_DAYS=30 +TRACES_RETENTION_DAYS=14 diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 0000000..e19049d --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,119 @@ +# ═══════════════════════════════════════════════════════════ +# 🐍 OPHION - Dockerfile All-in-One +# Imagem única com Server + Agent + Dashboard +# ═══════════════════════════════════════════════════════════ + +# ───────────────────────────────────────────────────────────── +# Stage 1: Build Go binaries +# ───────────────────────────────────────────────────────────── +FROM golang:1.22-alpine AS go-builder + +WORKDIR /build + +# Dependências +RUN apk add --no-cache git ca-certificates + +# Go modules +COPY go.mod go.sum ./ +RUN go mod download + +# Código fonte +COPY cmd/ ./cmd/ +COPY internal/ ./internal/ + +# Build server +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags="-s -w" -o ophion-server ./cmd/server + +# Build agent +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags="-s -w" -o ophion-agent ./cmd/agent + +# ───────────────────────────────────────────────────────────── +# Stage 2: Build Dashboard (Next.js) +# ───────────────────────────────────────────────────────────── +FROM node:20-alpine AS web-builder + +WORKDIR /build + +# Dependências +COPY dashboard/package*.json ./ +RUN npm ci --only=production + +# Código fonte +COPY dashboard/ ./ + +# Build +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# ───────────────────────────────────────────────────────────── +# Stage 3: Runtime Image +# ───────────────────────────────────────────────────────────── +FROM alpine:3.19 + +LABEL org.opencontainers.image.title="OPHION" +LABEL org.opencontainers.image.description="Open Source Observability Platform" +LABEL org.opencontainers.image.source="https://github.com/bigtux/ophion" +LABEL org.opencontainers.image.vendor="OPHION" +LABEL org.opencontainers.image.licenses="AGPL-3.0" + +# Dependências runtime +RUN apk add --no-cache \ + ca-certificates \ + tzdata \ + nodejs \ + npm \ + supervisor \ + curl \ + bash + +# Criar usuário não-root +RUN addgroup -g 1000 ophion && \ + adduser -u 1000 -G ophion -s /bin/sh -D ophion + +WORKDIR /app + +# Copiar binários Go +COPY --from=go-builder /build/ophion-server /app/bin/ +COPY --from=go-builder /build/ophion-agent /app/bin/ + +# Copiar Dashboard +COPY --from=web-builder /build/.next /app/web/.next +COPY --from=web-builder /build/public /app/web/public +COPY --from=web-builder /build/package*.json /app/web/ +COPY --from=web-builder /build/node_modules /app/web/node_modules + +# Configs +COPY configs/ /app/configs/ +COPY web/ /app/static/ + +# Supervisor config +RUN mkdir -p /etc/supervisor.d +COPY deploy/docker/supervisord.conf /etc/supervisor.d/ophion.ini + +# Diretórios +RUN mkdir -p /app/data /app/logs && \ + chown -R ophion:ophion /app + +# Script de entrada +COPY deploy/docker/entrypoint.sh /app/ +RUN chmod +x /app/entrypoint.sh + +# Portas +EXPOSE 8080 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Variáveis de ambiente padrão +ENV TZ=America/Sao_Paulo \ + LOG_LEVEL=info \ + SERVER_PORT=8080 \ + DASHBOARD_PORT=3000 + +USER ophion + +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["all"] diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh new file mode 100644 index 0000000..8451668 --- /dev/null +++ b/deploy/docker/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════ +# 🐍 OPHION - Entrypoint +# ═══════════════════════════════════════════════════════════ + +set -e + +MODE=${1:-all} + +echo "🐍 OPHION starting in mode: $MODE" + +case "$MODE" in + server) + echo "Starting API server on port ${SERVER_PORT:-8080}..." + exec /app/bin/ophion-server + ;; + agent) + echo "Starting agent..." + exec /app/bin/ophion-agent -config /app/configs/agent.yaml + ;; + web) + echo "Starting dashboard on port ${DASHBOARD_PORT:-3000}..." + cd /app/web + exec npm start + ;; + all) + echo "Starting all services with supervisor..." + exec supervisord -c /etc/supervisord.conf + ;; + *) + echo "Unknown mode: $MODE" + echo "Usage: entrypoint.sh [server|agent|web|all]" + exit 1 + ;; +esac diff --git a/deploy/docker/supervisord.conf b/deploy/docker/supervisord.conf new file mode 100644 index 0000000..2012961 --- /dev/null +++ b/deploy/docker/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +nodaemon=true +logfile=/app/logs/supervisord.log +pidfile=/tmp/supervisord.pid +user=ophion + +[program:ophion-server] +command=/app/bin/ophion-server +directory=/app +autostart=true +autorestart=true +stdout_logfile=/app/logs/server.log +stderr_logfile=/app/logs/server.error.log +environment=PORT="%(ENV_SERVER_PORT)s" + +[program:ophion-web] +command=npm start +directory=/app/web +autostart=true +autorestart=true +stdout_logfile=/app/logs/web.log +stderr_logfile=/app/logs/web.error.log +environment=PORT="%(ENV_DASHBOARD_PORT)s" + +[group:ophion] +programs=ophion-server,ophion-web diff --git a/docs/INSTALL.md b/docs/INSTALL.md index e1e0aee..492115c 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,436 +1,231 @@ -# 🐍 OPHION - Manual de Instalação e Configuração +# 🐍 OPHION - Guia de Instalação -## Índice -1. [Instalação do Servidor](#instalação-do-servidor) -2. [Instalação do Agent](#instalação-do-agent) -3. [Monitoramento de Docker](#monitoramento-de-docker) -4. [Monitoramento de Aplicações](#monitoramento-de-aplicações) -5. [Configuração de Alertas](#configuração-de-alertas) +## Instalação Rápida (1 comando) + +```bash +curl -fsSL https://get.ophion.io | bash +``` + +O instalador vai: +1. ✅ Verificar requisitos (Docker, Docker Compose) +2. 📋 Coletar informações da sua empresa +3. 🔐 Gerar credenciais seguras +4. 📦 Configurar todos os serviços +5. 🚀 Iniciar a plataforma --- -## 1. Instalação do Servidor +## Instalação Manual (Passo a Passo) -### Requisitos Mínimos -- CPU: 2 cores -- RAM: 4GB (8GB recomendado) -- Disco: 50GB SSD -- OS: Ubuntu 22.04+ / Debian 12+ -- Docker 24+ - -### Instalação Rápida (1 comando) +### 1. Requisitos ```bash -curl -fsSL https://ophion.com.br/install.sh | bash +# Ubuntu/Debian +sudo apt update +sudo apt install -y curl git + +# Instalar Docker +curl -fsSL https://get.docker.com | sh +sudo usermod -aG docker $USER + +# Logout e login para aplicar grupo docker ``` -### Instalação Manual +### 2. Baixar Ophion ```bash -# Clonar repositório git clone https://github.com/bigtux/ophion.git -cd ophion/deploy/docker - -# Configurar variáveis de ambiente -cp .env.example .env -nano .env # Editar conforme necessário - -# Iniciar serviços -docker compose up -d - -# Verificar status -docker compose ps +cd ophion ``` -### Variáveis de Ambiente (.env) +### 3. Configurar + +Copie e edite o arquivo de configuração: + +```bash +cp .env.example .env +nano .env +``` + +Configurações importantes: ```env -# Segurança (OBRIGATÓRIO - gere valores únicos!) -JWT_SECRET=sua-chave-secreta-aqui-min-32-chars +# Sua empresa +ORG_NAME="Minha Empresa" +ADMIN_EMAIL=admin@empresa.com +ADMIN_PASSWORD=senha-segura-aqui -# Banco de Dados -POSTGRES_USER=ophion -POSTGRES_PASSWORD=senha-forte-aqui -POSTGRES_DB=ophion +# Domínio (ou localhost para testes) +DOMAIN=ophion.empresa.com -# ClickHouse (métricas/logs) -CLICKHOUSE_USER=default -CLICKHOUSE_PASSWORD=senha-clickhouse +# Portas +SERVER_PORT=8080 +DASHBOARD_PORT=3000 -# Redis -REDIS_PASSWORD=senha-redis - -# Configurações do Servidor -OPHION_PORT=8080 -OPHION_HOST=0.0.0.0 - -# Retenção de dados (dias) -METRICS_RETENTION_DAYS=30 -LOGS_RETENTION_DAYS=14 +# Segurança (gere valores únicos!) +JWT_SECRET=seu-jwt-secret-aqui ``` -### Verificar Instalação +### 4. Iniciar ```bash -# Health check -curl http://localhost:8080/health - -# Resposta esperada: -# {"status":"healthy","service":"ophion","version":"0.1.0"} +docker compose up -d ``` -### Acessar Dashboard +### 5. Acessar -Abra no navegador: `http://seu-servidor:3000` - -1. Crie sua conta de administrador -2. Configure sua organização -3. Gere API Keys para os agents +- **Dashboard:** http://localhost:3000 +- **API:** http://localhost:8080 --- -## 2. Instalação do Agent +## Instalação do Agent (Servidores Monitorados) -O Agent coleta métricas do servidor e envia para o OPHION. - -### Instalação Rápida +Em cada servidor que você quer monitorar: ```bash -# Substitua YOUR_API_KEY pela chave gerada no dashboard -curl -fsSL https://ophion.com.br/agent.sh | OPHION_API_KEY=YOUR_API_KEY bash +curl -fsSL http://SEU-SERVIDOR-OPHION:8080/install-agent.sh | sudo bash ``` -### Instalação Manual +Ou manualmente: ```bash -# Baixar binário -wget https://github.com/bigtux/ophion/releases/latest/download/ophion-agent-linux-amd64 -chmod +x ophion-agent-linux-amd64 -sudo mv ophion-agent-linux-amd64 /usr/local/bin/ophion-agent +# Baixar +curl -o /usr/local/bin/ophion-agent \ + http://SEU-SERVIDOR-OPHION:8080/downloads/agent/linux/amd64/ophion-agent +chmod +x /usr/local/bin/ophion-agent -# Criar arquivo de configuração -sudo mkdir -p /etc/ophion -sudo tee /etc/ophion/agent.yaml << EOF -server: https://api.ophion.com.br -api_key: YOUR_API_KEY -hostname: $(hostname) -interval: 30s +# Configurar +mkdir -p /etc/ophion +cat > /etc/ophion/agent.yaml << EOF +server: + url: http://SEU-SERVIDOR-OPHION:8080 + api_key: SUA-API-KEY -collectors: - cpu: true - memory: true - disk: true - network: true - processes: true +collection: + interval: 30s + +metrics: + enabled: true + +logs: + enabled: true + paths: + - /var/log/syslog EOF -# Criar serviço systemd -sudo tee /etc/systemd/system/ophion-agent.service << EOF +# Criar serviço +cat > /etc/systemd/system/ophion-agent.service << EOF [Unit] -Description=OPHION Monitoring Agent +Description=OPHION Agent After=network.target [Service] -Type=simple -ExecStart=/usr/local/bin/ophion-agent --config /etc/ophion/agent.yaml +ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml Restart=always -RestartSec=10 [Install] WantedBy=multi-user.target EOF -# Iniciar serviço -sudo systemctl daemon-reload -sudo systemctl enable ophion-agent -sudo systemctl start ophion-agent - -# Verificar status -sudo systemctl status ophion-agent -``` - -### Variáveis de Ambiente do Agent - -```bash -export OPHION_SERVER="https://api.ophion.com.br" -export OPHION_API_KEY="ophion_xxxxxxxxxxxxxxxx" -export OPHION_HOSTNAME="meu-servidor" -export OPHION_INTERVAL="30s" +# Iniciar +systemctl daemon-reload +systemctl enable --now ophion-agent ``` --- -## 3. Monitoramento de Docker - -### Opção A: Agent com Acesso ao Docker Socket +## Comandos Úteis ```bash -# Adicionar ao agent.yaml -collectors: - docker: - enabled: true - socket: /var/run/docker.sock - collect_container_stats: true - collect_container_logs: true -``` +# Status +ophion status -### Opção B: Container Dedicado +# Logs +ophion logs # Logs do server +ophion logs ophion-web # Logs do dashboard -```yaml -# docker-compose.yml do seu projeto -services: - ophion-agent: - image: ophion/agent:latest - container_name: ophion-agent - restart: unless-stopped - environment: - - OPHION_SERVER=https://api.ophion.com.br - - OPHION_API_KEY=YOUR_API_KEY - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - /proc:/host/proc:ro - - /sys:/host/sys:ro - network_mode: host - pid: host -``` +# Gerenciamento +ophion start +ophion stop +ophion restart +ophion update -### Métricas Coletadas do Docker +# Backup +ophion backup -| Métrica | Descrição | -|---------|-----------| -| `container.cpu.usage` | Uso de CPU por container | -| `container.memory.usage` | Uso de memória | -| `container.memory.limit` | Limite de memória | -| `container.network.rx_bytes` | Bytes recebidos | -| `container.network.tx_bytes` | Bytes enviados | -| `container.disk.read_bytes` | Leitura de disco | -| `container.disk.write_bytes` | Escrita de disco | -| `container.status` | Status (running/stopped) | -| `container.restarts` | Contagem de restarts | - -### Labels para Identificação - -Adicione labels aos seus containers para melhor organização: - -```yaml -services: - minha-app: - labels: - ophion.monitor: "true" - ophion.service: "api" - ophion.environment: "production" - ophion.team: "backend" +# Gerar nova API Key +ophion api-key ``` --- -## 4. Monitoramento de Aplicações (APM) +## Estrutura de Diretórios -### Node.js - -```bash -npm install @ophion/apm ``` - -```javascript -// No início do seu app (antes de outros imports) -const ophion = require('@ophion/apm'); - -ophion.init({ - serverUrl: 'https://api.ophion.com.br', - apiKey: 'YOUR_API_KEY', - serviceName: 'minha-api', - environment: 'production' -}); - -// Seu código normal... -const express = require('express'); -const app = express(); -``` - -### Python - -```bash -pip install ophion-apm -``` - -```python -# No início do seu app -import ophion_apm - -ophion_apm.init( - server_url='https://api.ophion.com.br', - api_key='YOUR_API_KEY', - service_name='minha-api', - environment='production' -) - -# Seu código normal... -from flask import Flask -app = Flask(__name__) -``` - -### Go - -```go -import "github.com/bigtux/ophion/sdk/go/apm" - -func main() { - // Inicializar APM - apm.Init(apm.Config{ - ServerURL: "https://api.ophion.com.br", - APIKey: "YOUR_API_KEY", - ServiceName: "minha-api", - Environment: "production", - }) - defer apm.Close() - - // Seu código normal... -} -``` - -### Java (Spring Boot) - -```xml - - - com.ophion - ophion-apm - 1.0.0 - -``` - -```yaml -# application.yml -ophion: - apm: - server-url: https://api.ophion.com.br - api-key: YOUR_API_KEY - service-name: minha-api - environment: production -``` - -### OpenTelemetry (Universal) - -OPHION é compatível com OpenTelemetry. Use qualquer SDK OTel: - -```bash -# Variáveis de ambiente -export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.ophion.com.br/v1/traces" -export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY" -export OTEL_SERVICE_NAME="minha-api" +/opt/ophion/ +├── docker-compose.yml # Configuração dos containers +├── .env # Variáveis de ambiente (SECRETO!) +├── data/ +│ ├── postgres/ # Dados do PostgreSQL +│ ├── clickhouse/ # Métricas e logs +│ └── redis/ # Cache +├── configs/ # Configurações customizadas +├── logs/ # Logs da aplicação +├── scripts/ +│ └── install-agent.sh # Instalador do agent +└── backups/ # Backups automáticos ``` --- -## 5. Configuração de Alertas +## Portas -### Via Dashboard - -1. Acesse **Alertas** → **Novo Alerta** -2. Defina a condição: - - Métrica: `cpu.usage` - - Operador: `>` - - Valor: `80` - - Duração: `5 minutos` -3. Configure notificações: - - Telegram - - Slack - - Email - - Webhook - -### Via API - -```bash -curl -X POST "https://api.ophion.com.br/api/v1/alerts" \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "CPU Alta", - "description": "Alerta quando CPU > 80%", - "condition": { - "metric": "cpu.usage", - "operator": ">", - "threshold": 80, - "duration": "5m" - }, - "notifications": [ - { - "type": "telegram", - "chat_id": "123456789" - }, - { - "type": "email", - "to": "admin@empresa.com" - } - ], - "severity": "warning" - }' -``` - -### Integrações Disponíveis - -| Canal | Configuração | -|-------|-------------| -| **Telegram** | Bot token + Chat ID | -| **Slack** | Webhook URL | -| **Discord** | Webhook URL | -| **Email** | SMTP ou API (SendGrid, Resend) | -| **PagerDuty** | Integration Key | -| **Webhook** | URL customizada | - -### Configurar Telegram - -1. Crie um bot com [@BotFather](https://t.me/BotFather) -2. Obtenha o token do bot -3. Inicie conversa com o bot -4. No dashboard OPHION: **Configurações** → **Integrações** → **Telegram** -5. Cole o token e configure +| Serviço | Porta | Descrição | +|---------|-------|-----------| +| Dashboard | 3000 | Interface web | +| API | 8080 | REST API | +| PostgreSQL | 5432 | Banco de dados (interno) | +| ClickHouse | 9000 | Métricas/Logs (interno) | +| Redis | 6379 | Cache (interno) | --- ## Troubleshooting +### Containers não iniciam + +```bash +# Ver logs +docker compose logs + +# Verificar recursos +docker system df +df -h +``` + ### Agent não conecta ```bash -# Verificar conectividade -curl -v https://api.ophion.com.br/health +# Testar conectividade +curl http://SEU-SERVIDOR:8080/health -# Verificar logs do agent +# Ver logs do agent journalctl -u ophion-agent -f - -# Testar API key -curl -H "Authorization: Bearer YOUR_API_KEY" \ - https://api.ophion.com.br/api/v1/status ``` -### Métricas não aparecem - -1. Verifique se o agent está rodando: `systemctl status ophion-agent` -2. Verifique a API key no dashboard -3. Confira o hostname no dashboard -4. Aguarde até 60 segundos para primeira coleta - -### Docker metrics não coletam +### Resetar senha admin ```bash -# Verificar permissões do socket -ls -la /var/run/docker.sock - -# Agent precisa estar no grupo docker -sudo usermod -aG docker ophion-agent +docker compose exec postgres psql -U ophion -c \ + "UPDATE users SET password_hash = crypt('nova-senha', gen_salt('bf')) WHERE email = 'admin@email.com';" ``` --- ## Suporte -- 📧 Email: suporte@ophion.com.br -- 💬 Telegram: [@ophion_suporte](https://t.me/ophion_suporte) -- 📖 Docs: https://docs.ophion.com.br -- 🐙 GitHub: https://github.com/bigtux/ophion - ---- - -*Made with 🖤 in Brazil* +- 📖 Docs: https://docs.ophion.io +- 💬 Discord: https://discord.gg/ophion +- 🐛 Issues: https://github.com/bigtux/ophion/issues +- 📧 Email: support@ophion.io diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..b3d5417 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,270 @@ +# 🔐 OPHION Security Guide + +## Visão Geral + +OPHION foi projetado com segurança em camadas (defense in depth): + +1. **Autenticação** - JWT + API Keys +2. **Autorização** - RBAC + Scopes +3. **Rate Limiting** - Proteção contra DDoS/Brute Force +4. **Criptografia** - TLS + Bcrypt + SHA256 +5. **Auditoria** - Logs de todas as ações +6. **Isolamento** - Multi-tenant por organização + +--- + +## 🔑 Autenticação + +### JWT Tokens + +- **Access Token**: Curta duração (15-60 min) +- **Refresh Token**: Longa duração (7-30 dias) +- **Algoritmo**: HS256 (HMAC-SHA256) +- **Revogação**: Via Redis blacklist + +```bash +# Header de autenticação +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... +``` + +### API Keys + +- **Formato**: `ophion_` + 64 caracteres hex +- **Storage**: Hash SHA256 (nunca plaintext) +- **Scopes**: Permissões granulares +- **Rotação**: Recomendado a cada 90 dias + +```bash +# Exemplo de API Key +ophion_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6... + +# Header de autenticação +Authorization: Bearer ophion_xxx... +``` + +### Scopes de API Key + +| Scope | Descrição | +|-------|-----------| +| `metrics:read` | Ler métricas | +| `metrics:write` | Enviar métricas | +| `logs:read` | Ler logs | +| `logs:write` | Enviar logs | +| `traces:read` | Ler traces | +| `traces:write` | Enviar traces | +| `alerts:read` | Ler alertas | +| `alerts:write` | Criar/editar alertas | +| `admin` | Acesso administrativo | +| `*` | Acesso total | + +--- + +## 🛡️ Proteções + +### Rate Limiting + +| Endpoint | Limite | Janela | +|----------|--------|--------| +| `/api/v1/auth/*` | 5 req | 1 min | +| `/api/v1/*` (API) | 100 req | 1 min | +| `/api/v1/ingest/*` | 1000 req | 1 min | +| `/api/v1/export/*` | 10 req | 1 hora | + +### Brute Force Protection + +- **Máximo de tentativas**: 5 falhas +- **Bloqueio**: 15 minutos +- **Tracking**: Por IP + Email + +### Security Headers + +```http +X-Frame-Options: DENY +X-Content-Type-Options: nosniff +X-XSS-Protection: 1; mode=block +Strict-Transport-Security: max-age=31536000; includeSubDomains +Content-Security-Policy: default-src 'self' +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: geolocation=(), microphone=(), camera=() +``` + +--- + +## 🔒 Senhas + +### Política de Senha + +- Mínimo 12 caracteres +- Letra maiúscula obrigatória +- Letra minúscula obrigatória +- Número obrigatório +- Caractere especial obrigatório +- Bloqueio de senhas comuns + +### Armazenamento + +- **Algoritmo**: Bcrypt +- **Cost Factor**: 12 +- **Salt**: Único por senha (auto-gerado) + +--- + +## 🔐 Criptografia + +### Em Trânsito + +- TLS 1.2+ obrigatório em produção +- Certificados Let's Encrypt (automático) +- HSTS habilitado + +### Em Repouso + +- Senhas: Bcrypt hash +- API Keys: SHA256 hash +- Tokens: Opcionalmente criptografados +- Dados sensíveis: AES-256-GCM (configurável) + +--- + +## 📝 Auditoria + +### Eventos Registrados + +- `auth.login` - Login bem-sucedido +- `auth.login_failed` - Falha de login +- `auth.logout` - Logout +- `apikey.created` - API key criada +- `apikey.revoked` - API key revogada +- `user.created` - Usuário criado +- `user.deleted` - Usuário deletado +- `config.changed` - Configuração alterada +- `alert.created` - Alerta criado +- `data.export` - Exportação de dados + +### Campos de Auditoria + +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "event_type": "auth.login", + "user_id": "uuid", + "org_id": "uuid", + "ip": "192.168.1.1", + "user_agent": "Mozilla/5.0...", + "resource": "/api/v1/auth/login", + "action": "POST", + "status": "success", + "details": {} +} +``` + +--- + +## 🏢 Multi-Tenancy + +### Isolamento de Dados + +- Cada organização tem `org_id` único +- Queries sempre filtradas por `org_id` +- API Keys vinculadas à organização +- Usuários pertencem a uma organização + +### RBAC (Role-Based Access Control) + +| Role | Permissões | +|------|------------| +| `viewer` | Somente leitura | +| `operator` | Leitura + Ack de alertas | +| `editor` | Leitura + Escrita | +| `admin` | Acesso total à organização | +| `super_admin` | Acesso total ao sistema | + +--- + +## ⚙️ Configurações de Segurança + +### Variáveis de Ambiente + +```bash +# Obrigatórias +JWT_SECRET= # openssl rand -hex 32 +ADMIN_PASSWORD= + +# Recomendadas +HTTPS_ONLY=true # Forçar HTTPS +SESSION_TIMEOUT=3600 # 1 hora +REFRESH_TOKEN_DAYS=7 # 7 dias +PASSWORD_MIN_LENGTH=12 # Mínimo 12 chars +RATE_LIMIT_ENABLED=true +AUDIT_LOG_ENABLED=true + +# IP Whitelist (opcional) +ADMIN_IP_WHITELIST=10.0.0.0/8,192.168.0.0/16 +API_IP_WHITELIST= # Vazio = todos permitidos +``` + +### Checklist de Produção + +- [ ] JWT_SECRET único e forte (64+ chars) +- [ ] HTTPS habilitado +- [ ] Senhas fortes para todos os usuários +- [ ] Rate limiting habilitado +- [ ] Logs de auditoria habilitados +- [ ] Backup configurado +- [ ] Firewall configurado +- [ ] Atualizações automáticas +- [ ] Monitoramento de segurança + +--- + +## 🚨 Resposta a Incidentes + +### Se suspeitar de comprometimento: + +1. **Revogar todas as API Keys** + ```bash + ophion security revoke-all-keys + ``` + +2. **Invalidar todas as sessões** + ```bash + ophion security invalidate-sessions + ``` + +3. **Rotacionar JWT Secret** + ```bash + # Atualizar .env com novo JWT_SECRET + ophion restart + ``` + +4. **Revisar logs de auditoria** + ```bash + ophion logs --type audit --since 24h + ``` + +5. **Resetar senhas comprometidas** + +--- + +## 📞 Reportar Vulnerabilidades + +Se você encontrar uma vulnerabilidade de segurança: + +1. **NÃO** abra uma issue pública +2. Envie email para: security@ophion.io +3. Inclua detalhes e passos para reproduzir +4. Aguarde confirmação antes de divulgar + +**Programa de Bug Bounty**: Em breve! + +--- + +## 🔄 Atualizações de Segurança + +Assine nossa lista de segurança: +- Email: security-announce@ophion.io +- GitHub Security Advisories + +Versões com suporte de segurança: +- Última versão: Suporte completo +- Versão anterior: Patches críticos por 6 meses diff --git a/go.mod b/go.mod index e80d2e4..615968c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.22 require ( github.com/gofiber/fiber/v2 v2.52.0 - github.com/shirou/gopsutil/v3 v3.24.1 github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/redis/go-redis/v9 v9.4.0 + github.com/shirou/gopsutil/v3 v3.24.1 + golang.org/x/crypto v0.18.0 ) diff --git a/install.sh b/install.sh index 8c4ae9c..4c3b336 100755 --- a/install.sh +++ b/install.sh @@ -1,49 +1,844 @@ #!/bin/bash +# +# 🐍 OPHION - Instalador Interativo +# Plataforma de Observabilidade Open Source +# +# Uso: curl -fsSL https://get.ophion.io | bash +# + set -e -echo "🐍 OPHION - Observability Platform Installer" -echo "=============================================" +# Cores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color -# Check Docker -if ! command -v docker &> /dev/null; then - echo "❌ Docker not found. Installing..." - curl -fsSL https://get.docker.com | sh -fi +# ASCII Art +show_banner() { + echo -e "${PURPLE}" + cat << "EOF" + ____ _____ _ _ _____ ____ _ _ + / __ \| __ \| | | |_ _/ __ \| \ | | + | | | | |__) | |__| | | || | | | \| | + | | | | ___/| __ | | || | | | . ` | + | |__| | | | | | |_| || |__| | |\ | + \____/|_| |_| |_|_____\____/|_| \_| + + Open Source Observability Platform + Made with 🖤 in Brazil +EOF + echo -e "${NC}" +} -# Check Docker Compose -if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then - echo "❌ Docker Compose not found. Please install it." +# Logging +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[✓]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[!]${NC} $1"; } +log_error() { echo -e "${RED}[✗]${NC} $1"; } + +# Verificar requisitos +check_requirements() { + log_info "Verificando requisitos..." + + # Docker + if ! command -v docker &> /dev/null; then + log_error "Docker não encontrado!" + echo "" + echo "Instale o Docker primeiro:" + echo " curl -fsSL https://get.docker.com | sh" + exit 1 + fi + log_success "Docker instalado" + + # Docker Compose + if ! docker compose version &> /dev/null && ! command -v docker-compose &> /dev/null; then + log_error "Docker Compose não encontrado!" + exit 1 + fi + log_success "Docker Compose instalado" + + # Verificar se Docker está rodando + if ! docker info &> /dev/null; then + log_error "Docker não está rodando!" + echo " sudo systemctl start docker" + exit 1 + fi + log_success "Docker está rodando" +} + +# Gerar string aleatória +generate_secret() { + openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64 +} + +# Gerar API Key +generate_api_key() { + echo "ophion_$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64)" +} + +# Coletar informações do cliente +collect_info() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" + echo -e "${CYAN} CONFIGURAÇÃO INICIAL ${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" + echo "" + + # Nome da organização + read -p "📋 Nome da sua empresa/organização: " ORG_NAME + ORG_NAME=${ORG_NAME:-"Minha Empresa"} + + # Email do admin + read -p "📧 Email do administrador: " ADMIN_EMAIL + while [[ ! "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; do + log_warn "Email inválido!" + read -p "📧 Email do administrador: " ADMIN_EMAIL + done + + # Senha do admin + echo -n "🔐 Senha do administrador (mín. 8 caracteres): " + read -s ADMIN_PASSWORD + echo "" + while [[ ${#ADMIN_PASSWORD} -lt 8 ]]; do + log_warn "Senha muito curta!" + echo -n "🔐 Senha do administrador (mín. 8 caracteres): " + read -s ADMIN_PASSWORD + echo "" + done + + # Domínio (opcional) + read -p "🌐 Domínio (deixe vazio para localhost): " DOMAIN + DOMAIN=${DOMAIN:-"localhost"} + + # Porta + read -p "🔌 Porta do servidor [8080]: " SERVER_PORT + SERVER_PORT=${SERVER_PORT:-8080} + + # Porta do dashboard + read -p "🖥️ Porta do dashboard [3000]: " DASHBOARD_PORT + DASHBOARD_PORT=${DASHBOARD_PORT:-3000} + + # Habilitar HTTPS? + if [[ "$DOMAIN" != "localhost" ]]; then + read -p "🔒 Habilitar HTTPS com Let's Encrypt? (s/n) [s]: " ENABLE_HTTPS + ENABLE_HTTPS=${ENABLE_HTTPS:-s} + else + ENABLE_HTTPS="n" + fi + + # Telegram para alertas (opcional) + echo "" + read -p "📱 Configurar alertas no Telegram? (s/n) [n]: " ENABLE_TELEGRAM + if [[ "$ENABLE_TELEGRAM" =~ ^[sS]$ ]]; then + read -p " Bot Token: " TELEGRAM_BOT_TOKEN + read -p " Chat ID: " TELEGRAM_CHAT_ID + fi + + echo "" + log_success "Informações coletadas!" +} + +# Criar diretório de instalação +setup_directory() { + INSTALL_DIR="/opt/ophion" + + log_info "Criando diretório de instalação em $INSTALL_DIR..." + + sudo mkdir -p "$INSTALL_DIR" + sudo mkdir -p "$INSTALL_DIR/data/postgres" + sudo mkdir -p "$INSTALL_DIR/data/clickhouse" + sudo mkdir -p "$INSTALL_DIR/data/redis" + sudo mkdir -p "$INSTALL_DIR/configs" + sudo mkdir -p "$INSTALL_DIR/logs" + + sudo chown -R $USER:$USER "$INSTALL_DIR" + + cd "$INSTALL_DIR" + log_success "Diretório criado" +} + +# Gerar arquivo .env +generate_env() { + log_info "Gerando configuração..." + + JWT_SECRET=$(generate_secret) + POSTGRES_PASSWORD=$(generate_secret | head -c 32) + API_KEY=$(generate_api_key) + + cat > "$INSTALL_DIR/.env" << EOF +# ═══════════════════════════════════════════════════════════ +# 🐍 OPHION - Configuração +# Gerado em: $(date) +# Organização: $ORG_NAME +# ═══════════════════════════════════════════════════════════ + +# Organização +ORG_NAME="$ORG_NAME" +ADMIN_EMAIL="$ADMIN_EMAIL" +ADMIN_PASSWORD="$ADMIN_PASSWORD" + +# Rede +DOMAIN=$DOMAIN +SERVER_PORT=$SERVER_PORT +DASHBOARD_PORT=$DASHBOARD_PORT +API_URL=http://ophion-server:8080 + +# Segurança (NÃO COMPARTILHE!) +JWT_SECRET=$JWT_SECRET +API_KEY=$API_KEY + +# PostgreSQL +POSTGRES_USER=ophion +POSTGRES_PASSWORD=$POSTGRES_PASSWORD +POSTGRES_DB=ophion +DATABASE_URL=postgres://ophion:$POSTGRES_PASSWORD@postgres:5432/ophion + +# ClickHouse +CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion + +# Redis +REDIS_URL=redis://redis:6379 + +# Telegram Alertas +TELEGRAM_ENABLED=${ENABLE_TELEGRAM:-n} +TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} +TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} + +# OpenAI (para recursos de IA) +OPENAI_API_KEY= + +# Timezone +TZ=America/Sao_Paulo +EOF + + chmod 600 "$INSTALL_DIR/.env" + log_success "Arquivo .env gerado" +} + +# Gerar docker-compose.yml +generate_compose() { + log_info "Gerando docker-compose.yml..." + + cat > "$INSTALL_DIR/docker-compose.yml" << 'EOF' +version: '3.8' + +services: + # ═══════════════════════════════════════════════════════════ + # 🐍 OPHION Server (API) + # ═══════════════════════════════════════════════════════════ + ophion-server: + image: ghcr.io/bigtux/ophion-server:latest + container_name: ophion-server + ports: + - "${SERVER_PORT}:8080" + environment: + - DATABASE_URL=${DATABASE_URL} + - CLICKHOUSE_URL=${CLICKHOUSE_URL} + - REDIS_URL=${REDIS_URL} + - JWT_SECRET=${JWT_SECRET} + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - ORG_NAME=${ORG_NAME} + - TELEGRAM_ENABLED=${TELEGRAM_ENABLED} + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - TZ=${TZ} + volumes: + - ./configs:/app/configs:ro + - ./logs:/app/logs + depends_on: + postgres: + condition: service_healthy + clickhouse: + condition: service_started + redis: + condition: service_started + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + networks: + - ophion-net + + # ═══════════════════════════════════════════════════════════ + # 🖥️ OPHION Dashboard (Web UI) + # ═══════════════════════════════════════════════════════════ + ophion-web: + image: ghcr.io/bigtux/ophion-web:latest + container_name: ophion-web + ports: + - "${DASHBOARD_PORT}:3000" + environment: + - API_URL=http://ophion-server:8080 + - NEXT_PUBLIC_API_URL=http://${DOMAIN}:${SERVER_PORT} + - ORG_NAME=${ORG_NAME} + depends_on: + - ophion-server + restart: unless-stopped + networks: + - ophion-net + + # ═══════════════════════════════════════════════════════════ + # 🐘 PostgreSQL (Metadados, Usuários, Config) + # ═══════════════════════════════════════════════════════════ + postgres: + image: postgres:16-alpine + container_name: ophion-postgres + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - ./data/postgres:/var/lib/postgresql/data + - ./init/postgres:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - ophion-net + + # ═══════════════════════════════════════════════════════════ + # 🏠 ClickHouse (Métricas, Logs, Traces) + # ═══════════════════════════════════════════════════════════ + clickhouse: + image: clickhouse/clickhouse-server:24.1 + container_name: ophion-clickhouse + volumes: + - ./data/clickhouse:/var/lib/clickhouse + - ./init/clickhouse:/docker-entrypoint-initdb.d:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + restart: unless-stopped + networks: + - ophion-net + + # ═══════════════════════════════════════════════════════════ + # 🔴 Redis (Cache, Sessions, Filas) + # ═══════════════════════════════════════════════════════════ + redis: + image: redis:7-alpine + container_name: ophion-redis + command: redis-server --appendonly yes + volumes: + - ./data/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + restart: unless-stopped + networks: + - ophion-net + +networks: + ophion-net: + driver: bridge + +EOF + + log_success "docker-compose.yml gerado" +} + +# Gerar scripts SQL de inicialização +generate_init_scripts() { + log_info "Gerando scripts de inicialização..." + + mkdir -p "$INSTALL_DIR/init/postgres" + mkdir -p "$INSTALL_DIR/init/clickhouse" + + # PostgreSQL init + cat > "$INSTALL_DIR/init/postgres/01-schema.sql" << 'EOF' +-- ═══════════════════════════════════════════════════════════ +-- 🐍 OPHION - Schema PostgreSQL +-- ═══════════════════════════════════════════════════════════ + +-- Extensões +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- Organizações +CREATE TABLE organizations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(100) UNIQUE NOT NULL, + settings JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Usuários +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(255), + role VARCHAR(50) DEFAULT 'viewer', + avatar_url TEXT, + settings JSONB DEFAULT '{}', + last_login_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- API Keys +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + key_hash VARCHAR(64) NOT NULL, + key_prefix VARCHAR(20) NOT NULL, + name VARCHAR(255), + description TEXT, + scopes TEXT[] DEFAULT ARRAY['metrics:write', 'logs:write'], + created_by UUID REFERENCES users(id), + last_used_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + revoked BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Hosts/Agents +CREATE TABLE hosts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + hostname VARCHAR(255) NOT NULL, + ip_address INET, + os VARCHAR(100), + arch VARCHAR(50), + agent_version VARCHAR(50), + tags JSONB DEFAULT '{}', + status VARCHAR(50) DEFAULT 'unknown', + last_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(org_id, hostname) +); + +-- Alert Rules +CREATE TABLE alert_rules ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + query TEXT NOT NULL, + condition VARCHAR(50) NOT NULL, + threshold DECIMAL, + severity VARCHAR(50) DEFAULT 'warning', + enabled BOOLEAN DEFAULT TRUE, + notify_channels JSONB DEFAULT '[]', + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Alert History +CREATE TABLE alert_history ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + rule_id UUID REFERENCES alert_rules(id) ON DELETE CASCADE, + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + host_id UUID REFERENCES hosts(id), + severity VARCHAR(50), + status VARCHAR(50) DEFAULT 'firing', + message TEXT, + value DECIMAL, + fired_at TIMESTAMPTZ DEFAULT NOW(), + resolved_at TIMESTAMPTZ, + acknowledged_by UUID REFERENCES users(id), + acknowledged_at TIMESTAMPTZ +); + +-- Dashboards +CREATE TABLE dashboards ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + org_id UUID REFERENCES organizations(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + layout JSONB DEFAULT '[]', + is_default BOOLEAN DEFAULT FALSE, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Índices +CREATE INDEX idx_users_org ON users(org_id); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_api_keys_org ON api_keys(org_id); +CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix); +CREATE INDEX idx_hosts_org ON hosts(org_id); +CREATE INDEX idx_hosts_status ON hosts(status); +CREATE INDEX idx_alert_history_org ON alert_history(org_id); +CREATE INDEX idx_alert_history_status ON alert_history(status); +CREATE INDEX idx_alert_history_fired ON alert_history(fired_at DESC); + +EOF + + # ClickHouse init + cat > "$INSTALL_DIR/init/clickhouse/01-schema.sql" << 'EOF' +-- ═══════════════════════════════════════════════════════════ +-- 🐍 OPHION - Schema ClickHouse +-- ═══════════════════════════════════════════════════════════ + +CREATE DATABASE IF NOT EXISTS ophion; + +-- Métricas de Sistema +CREATE TABLE IF NOT EXISTS ophion.metrics ( + org_id UUID, + host_id UUID, + hostname LowCardinality(String), + metric_name LowCardinality(String), + metric_type LowCardinality(String), + value Float64, + tags Map(String, String), + timestamp DateTime64(3), + INDEX idx_metric_name metric_name TYPE bloom_filter GRANULARITY 4, + INDEX idx_hostname hostname TYPE bloom_filter GRANULARITY 4 +) ENGINE = MergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (org_id, host_id, metric_name, timestamp) +TTL timestamp + INTERVAL 90 DAY; + +-- Logs +CREATE TABLE IF NOT EXISTS ophion.logs ( + org_id UUID, + host_id UUID, + hostname LowCardinality(String), + service LowCardinality(String), + level LowCardinality(String), + message String, + attributes Map(String, String), + trace_id String, + span_id String, + timestamp DateTime64(3), + INDEX idx_level level TYPE set(0) GRANULARITY 4, + INDEX idx_service service TYPE bloom_filter GRANULARITY 4, + INDEX idx_message message TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4 +) ENGINE = MergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (org_id, timestamp, host_id) +TTL timestamp + INTERVAL 30 DAY; + +-- Traces (Spans) +CREATE TABLE IF NOT EXISTS ophion.traces ( + org_id UUID, + trace_id String, + span_id String, + parent_span_id String, + operation_name LowCardinality(String), + service_name LowCardinality(String), + kind LowCardinality(String), + status_code UInt8, + status_message String, + attributes Map(String, String), + events Nested( + name String, + timestamp DateTime64(3), + attributes Map(String, String) + ), + duration_ms Float64, + start_time DateTime64(3), + end_time DateTime64(3), + INDEX idx_trace_id trace_id TYPE bloom_filter GRANULARITY 4, + INDEX idx_service service_name TYPE bloom_filter GRANULARITY 4 +) ENGINE = MergeTree() +PARTITION BY toYYYYMM(start_time) +ORDER BY (org_id, service_name, start_time, trace_id) +TTL start_time + INTERVAL 14 DAY; + +-- Aggregated metrics (rollups) +CREATE TABLE IF NOT EXISTS ophion.metrics_hourly ( + org_id UUID, + host_id UUID, + metric_name LowCardinality(String), + hour DateTime, + min_value Float64, + max_value Float64, + avg_value Float64, + count UInt64 +) ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(hour) +ORDER BY (org_id, host_id, metric_name, hour) +TTL hour + INTERVAL 1 YEAR; + +-- Materialized view para rollup +CREATE MATERIALIZED VIEW IF NOT EXISTS ophion.metrics_hourly_mv +TO ophion.metrics_hourly AS +SELECT + org_id, + host_id, + metric_name, + toStartOfHour(timestamp) AS hour, + min(value) AS min_value, + max(value) AS max_value, + avg(value) AS avg_value, + count() AS count +FROM ophion.metrics +GROUP BY org_id, host_id, metric_name, hour; + +EOF + + log_success "Scripts de inicialização gerados" +} + +# Gerar script do Agent +generate_agent_installer() { + log_info "Gerando instalador do agent..." + + mkdir -p "$INSTALL_DIR/scripts" + + cat > "$INSTALL_DIR/scripts/install-agent.sh" << EOF +#!/bin/bash +# +# 🐍 OPHION Agent Installer +# Servidor: http://${DOMAIN}:${SERVER_PORT} +# + +set -e + +API_KEY="${API_KEY}" +SERVER_URL="http://${DOMAIN}:${SERVER_PORT}" + +echo "🐍 Instalando OPHION Agent..." + +# Detectar OS +if [[ -f /etc/debian_version ]]; then + OS="debian" +elif [[ -f /etc/redhat-release ]]; then + OS="redhat" +else + echo "OS não suportado!" exit 1 fi -# Create directory -INSTALL_DIR="${OPHION_DIR:-/opt/ophion}" -mkdir -p "$INSTALL_DIR" +# Baixar agent +curl -fsSL -o /tmp/ophion-agent "\${SERVER_URL}/downloads/agent/linux/amd64/ophion-agent" +chmod +x /tmp/ophion-agent +sudo mv /tmp/ophion-agent /usr/local/bin/ + +# Criar config +sudo mkdir -p /etc/ophion +sudo tee /etc/ophion/agent.yaml > /dev/null << AGENTEOF +server: + url: \${SERVER_URL} + api_key: \${API_KEY} + +collection: + interval: 30s + +metrics: + enabled: true + include: + - cpu + - memory + - disk + - network + - processes + +logs: + enabled: true + paths: + - /var/log/syslog + - /var/log/auth.log +AGENTEOF + +# Criar systemd service +sudo tee /etc/systemd/system/ophion-agent.service > /dev/null << SERVICEEOF +[Unit] +Description=OPHION Monitoring Agent +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +SERVICEEOF + +# Iniciar +sudo systemctl daemon-reload +sudo systemctl enable ophion-agent +sudo systemctl start ophion-agent + +echo "" +echo "✅ OPHION Agent instalado!" +echo " Status: sudo systemctl status ophion-agent" +echo " Logs: sudo journalctl -u ophion-agent -f" +EOF + + chmod +x "$INSTALL_DIR/scripts/install-agent.sh" + log_success "Instalador do agent gerado" +} + +# Gerar comandos de gerenciamento +generate_cli() { + log_info "Gerando CLI de gerenciamento..." + + cat > "$INSTALL_DIR/ophion" << 'EOF' +#!/bin/bash +# +# 🐍 OPHION CLI +# + +INSTALL_DIR="/opt/ophion" cd "$INSTALL_DIR" -# Download docker-compose -echo "📥 Downloading OPHION..." -curl -fsSL https://raw.githubusercontent.com/bigtux/ophion/main/deploy/docker/docker-compose.yml -o docker-compose.yml +case "$1" in + start) + echo "🚀 Iniciando OPHION..." + docker compose up -d + echo "✅ OPHION iniciado!" + echo " Dashboard: http://localhost:${DASHBOARD_PORT:-3000}" + echo " API: http://localhost:${SERVER_PORT:-8080}" + ;; + stop) + echo "🛑 Parando OPHION..." + docker compose down + echo "✅ OPHION parado" + ;; + restart) + echo "🔄 Reiniciando OPHION..." + docker compose restart + echo "✅ OPHION reiniciado" + ;; + status) + docker compose ps + ;; + logs) + docker compose logs -f ${2:-ophion-server} + ;; + update) + echo "📦 Atualizando OPHION..." + docker compose pull + docker compose up -d + echo "✅ OPHION atualizado!" + ;; + backup) + BACKUP_DIR="$INSTALL_DIR/backups/$(date +%Y%m%d_%H%M%S)" + mkdir -p "$BACKUP_DIR" + echo "💾 Criando backup em $BACKUP_DIR..." + docker compose exec -T postgres pg_dump -U ophion ophion > "$BACKUP_DIR/postgres.sql" + cp "$INSTALL_DIR/.env" "$BACKUP_DIR/" + echo "✅ Backup criado!" + ;; + api-key) + NEW_KEY="ophion_$(openssl rand -hex 32)" + echo "🔑 Nova API Key gerada:" + echo "" + echo " $NEW_KEY" + echo "" + echo "⚠️ Salve esta key! Ela não será mostrada novamente." + ;; + agent-install) + echo "" + echo "Para instalar o agent em outro servidor, execute:" + echo "" + echo " curl -fsSL http://$(hostname -I | awk '{print $1}'):${SERVER_PORT:-8080}/install-agent.sh | sudo bash" + echo "" + ;; + *) + echo "🐍 OPHION CLI" + echo "" + echo "Uso: ophion " + echo "" + echo "Comandos:" + echo " start Iniciar todos os serviços" + echo " stop Parar todos os serviços" + echo " restart Reiniciar todos os serviços" + echo " status Ver status dos serviços" + echo " logs [svc] Ver logs (padrão: ophion-server)" + echo " update Atualizar para última versão" + echo " backup Criar backup dos dados" + echo " api-key Gerar nova API key" + echo " agent-install Mostrar comando de instalação do agent" + ;; +esac +EOF -# Generate secrets -JWT_SECRET=$(openssl rand -hex 32) -echo "JWT_SECRET=$JWT_SECRET" > .env + chmod +x "$INSTALL_DIR/ophion" + sudo ln -sf "$INSTALL_DIR/ophion" /usr/local/bin/ophion + + log_success "CLI instalado em /usr/local/bin/ophion" +} -# Start services -echo "🚀 Starting OPHION..." -docker compose up -d +# Resumo final +show_summary() { + echo "" + echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} 🎉 OPHION INSTALADO COM SUCESSO! ${NC}" + echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${CYAN}Organização:${NC} $ORG_NAME" + echo -e " ${CYAN}Admin:${NC} $ADMIN_EMAIL" + echo "" + echo -e " ${CYAN}Dashboard:${NC} http://${DOMAIN}:${DASHBOARD_PORT}" + echo -e " ${CYAN}API:${NC} http://${DOMAIN}:${SERVER_PORT}" + echo "" + echo -e " ${YELLOW}🔑 API Key (SALVE AGORA!):${NC}" + echo -e " ${PURPLE}$API_KEY${NC}" + echo "" + echo -e " ${CYAN}Diretório:${NC} $INSTALL_DIR" + echo "" + echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}" + echo "" + echo " Comandos úteis:" + echo "" + echo " ophion start # Iniciar" + echo " ophion stop # Parar" + echo " ophion status # Ver status" + echo " ophion logs # Ver logs" + echo " ophion agent-install # Instalar agent em outros servidores" + echo "" + echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}" + echo "" + + read -p "🚀 Iniciar OPHION agora? (s/n) [s]: " START_NOW + START_NOW=${START_NOW:-s} + + if [[ "$START_NOW" =~ ^[sS]$ ]]; then + echo "" + log_info "Iniciando serviços..." + cd "$INSTALL_DIR" + docker compose up -d + + echo "" + log_success "OPHION está rodando!" + echo "" + echo -e " Acesse: ${CYAN}http://${DOMAIN}:${DASHBOARD_PORT}${NC}" + echo -e " Login: ${CYAN}${ADMIN_EMAIL}${NC}" + echo "" + fi +} -echo "" -echo "✅ OPHION installed successfully!" -echo "" -echo "📊 Dashboard: http://localhost:3000" -echo "🔌 API: http://localhost:8080" -echo "" -echo "Next steps:" -echo "1. Open http://localhost:3000 in your browser" -echo "2. Create your admin account" -echo "3. Add your first server with the agent" -echo "" -echo "To install the agent on a server:" -echo " curl -fsSL https://get.ophion.io/agent | bash" -echo "" +# ═══════════════════════════════════════════════════════════ +# MAIN +# ═══════════════════════════════════════════════════════════ + +main() { + clear + show_banner + check_requirements + collect_info + setup_directory + generate_env + generate_compose + generate_init_scripts + generate_agent_installer + generate_cli + show_summary +} + +main "$@" diff --git a/internal/api/ratelimit.go b/internal/api/ratelimit.go index 7a71087..df03512 100644 --- a/internal/api/ratelimit.go +++ b/internal/api/ratelimit.go @@ -1,27 +1,265 @@ package api import ( + "fmt" + "strconv" + "sync" "time" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/limiter" ) -func RateLimitMiddleware() fiber.Handler { - return limiter.New(limiter.Config{ - Max: 100, // 100 requests - Expiration: 1 * time.Minute, // per minute +// ═══════════════════════════════════════════════════════════ +// 🚦 RATE LIMITING MIDDLEWARE +// ═══════════════════════════════════════════════════════════ + +// RateLimitConfig configuração do rate limiter +type RateLimitConfig struct { + // Requests máximos por janela + Max int + + // Janela de tempo + Window time.Duration + + // Função para extrair identificador (default: IP) + KeyGenerator func(*fiber.Ctx) string + + // Função para resposta quando limitado + LimitReached func(*fiber.Ctx) error + + // Pular rate limit para certos paths + SkipPaths []string + + // Headers customizados + Headers RateLimitHeaders +} + +// RateLimitHeaders headers do rate limit +type RateLimitHeaders struct { + Limit string // X-RateLimit-Limit + Remaining string // X-RateLimit-Remaining + Reset string // X-RateLimit-Reset + RetryAfter string // Retry-After +} + +// DefaultRateLimitConfig configuração padrão +func DefaultRateLimitConfig() RateLimitConfig { + return RateLimitConfig{ + Max: 100, + Window: time.Minute, KeyGenerator: func(c *fiber.Ctx) string { - // Use API key or IP for rate limiting - if key := c.Locals("api_key"); key != nil { - return key.(string) - } return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return c.Status(429).JSON(fiber.Map{ - "error": "Rate limit exceeded", + "error": "Too Many Requests", + "message": "Rate limit exceeded. Please slow down.", + "retry_after": 60, + }) + }, + Headers: RateLimitHeaders{ + Limit: "X-RateLimit-Limit", + Remaining: "X-RateLimit-Remaining", + Reset: "X-RateLimit-Reset", + RetryAfter: "Retry-After", + }, + } +} + +// rateLimitStore armazena contagem de requests +type rateLimitStore struct { + sync.RWMutex + entries map[string]*rateLimitEntry +} + +type rateLimitEntry struct { + count int + expiresAt time.Time +} + +var store = &rateLimitStore{ + entries: make(map[string]*rateLimitEntry), +} + +// Limpar entries expirados periodicamente +func init() { + go func() { + ticker := time.NewTicker(time.Minute) + for range ticker.C { + store.cleanup() + } + }() +} + +func (s *rateLimitStore) cleanup() { + s.Lock() + defer s.Unlock() + + now := time.Now() + for key, entry := range s.entries { + if now.After(entry.expiresAt) { + delete(s.entries, key) + } + } +} + +// RateLimit middleware de rate limiting +func RateLimit(config ...RateLimitConfig) fiber.Handler { + cfg := DefaultRateLimitConfig() + if len(config) > 0 { + cfg = config[0] + } + + return func(c *fiber.Ctx) error { + // Verificar skip paths + path := c.Path() + for _, skip := range cfg.SkipPaths { + if path == skip { + return c.Next() + } + } + + // Gerar key + key := cfg.KeyGenerator(c) + + // Verificar/atualizar contagem + store.Lock() + + now := time.Now() + entry, exists := store.entries[key] + + if !exists || now.After(entry.expiresAt) { + // Nova janela + store.entries[key] = &rateLimitEntry{ + count: 1, + expiresAt: now.Add(cfg.Window), + } + store.Unlock() + + // Headers + c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max)) + c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1)) + c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10)) + + return c.Next() + } + + entry.count++ + remaining := cfg.Max - entry.count + resetTime := entry.expiresAt.Unix() + + store.Unlock() + + // Headers + c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max)) + c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining))) + c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10)) + + // Verificar se excedeu + if remaining < 0 { + retryAfter := int(time.Until(entry.expiresAt).Seconds()) + c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter))) + return cfg.LimitReached(c) + } + + return c.Next() + } +} + +// ═══════════════════════════════════════════════════════════ +// 🎯 RATE LIMIT PRESETS +// ═══════════════════════════════════════════════════════════ + +// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo) +func RateLimitAuth() fiber.Handler { + return RateLimit(RateLimitConfig{ + Max: 5, + Window: time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + // Combinar IP + email para prevenir ataques distribuídos + email := c.FormValue("email") + if email == "" { + email = c.Query("email") + } + return fmt.Sprintf("auth:%s:%s", c.IP(), email) + }, + LimitReached: func(c *fiber.Ctx) error { + return c.Status(429).JSON(fiber.Map{ + "error": "Too Many Login Attempts", + "message": "You have exceeded the maximum number of login attempts. Please wait before trying again.", + "retry_after": 60, }) }, }) } + +// RateLimitAPI rate limit para API geral +func RateLimitAPI() fiber.Handler { + return RateLimit(RateLimitConfig{ + Max: 100, + Window: time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + // Usar API key ou IP + if apiKey := c.Locals("api_key_id"); apiKey != nil { + return fmt.Sprintf("api:%v", apiKey) + } + if userID := c.Locals("user_id"); userID != nil { + return fmt.Sprintf("user:%v", userID) + } + return fmt.Sprintf("ip:%s", c.IP()) + }, + }) +} + +// RateLimitIngest rate limit para ingestão de dados (mais permissivo) +func RateLimitIngest() fiber.Handler { + return RateLimit(RateLimitConfig{ + Max: 1000, + Window: time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + if orgID := c.Locals("org_id"); orgID != nil { + return fmt.Sprintf("ingest:%v", orgID) + } + return fmt.Sprintf("ingest:ip:%s", c.IP()) + }, + LimitReached: func(c *fiber.Ctx) error { + return c.Status(429).JSON(fiber.Map{ + "error": "Ingest Rate Limit Exceeded", + "message": "Your organization has exceeded the data ingestion rate limit.", + "retry_after": 10, + }) + }, + }) +} + +// RateLimitExport rate limit para exportação de dados +func RateLimitExport() fiber.Handler { + return RateLimit(RateLimitConfig{ + Max: 10, + Window: time.Hour, + KeyGenerator: func(c *fiber.Ctx) string { + if userID := c.Locals("user_id"); userID != nil { + return fmt.Sprintf("export:%v", userID) + } + return fmt.Sprintf("export:ip:%s", c.IP()) + }, + LimitReached: func(c *fiber.Ctx) error { + return c.Status(429).JSON(fiber.Map{ + "error": "Export Rate Limit Exceeded", + "message": "You have exceeded the maximum number of exports per hour.", + "retry_after": 3600, + }) + }, + }) +} + +// ═══════════════════════════════════════════════════════════ +// 🔧 HELPERS +// ═══════════════════════════════════════════════════════════ + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index 261914a..59defae 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -1,93 +1,373 @@ package auth import ( + "context" "crypto/rand" "encoding/hex" + "fmt" "strings" "time" + "github.com/bigtux/ophion/internal/security" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" + "github.com/redis/go-redis/v9" ) -var jwtSecret []byte +// ═══════════════════════════════════════════════════════════ +// 🔐 AUTH CONFIG +// ═══════════════════════════════════════════════════════════ -func Init(secret string) { - jwtSecret = []byte(secret) +type AuthConfig struct { + JWTSecret []byte + JWTExpiration time.Duration + RefreshExpiration time.Duration + Issuer string } -// GenerateAPIKey creates a new API key for agents -func GenerateAPIKey() string { - bytes := make([]byte, 32) - rand.Read(bytes) - return "ophion_" + hex.EncodeToString(bytes) +type AuthService struct { + config AuthConfig + redis *redis.Client + rateLimiter *security.RateLimiter + loginTracker *security.LoginAttemptTracker + apiKeyStore APIKeyStore } -// GenerateJWT creates a JWT token for users -func GenerateJWT(userID string, email string) (string, error) { - claims := jwt.MapClaims{ - "sub": userID, - "email": email, - "iat": time.Now().Unix(), - "exp": time.Now().Add(24 * time.Hour).Unix(), +// APIKeyStore interface para storage de API keys +type APIKeyStore interface { + ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error) + UpdateLastUsed(ctx context.Context, keyID string) error +} + +type APIKeyInfo struct { + ID string + OrgID string + Scopes []string + Name string +} + +// NewAuthService cria serviço de autenticação +func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService { + return &AuthService{ + config: config, + redis: redis, + rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute), + loginTracker: security.NewLoginAttemptTracker(), + apiKeyStore: apiKeyStore, } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) } -// ValidateJWT validates a JWT token -func ValidateJWT(tokenString string) (*jwt.MapClaims, error) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return jwtSecret, nil +// ═══════════════════════════════════════════════════════════ +// 🎫 JWT TOKENS +// ═══════════════════════════════════════════════════════════ + +type TokenClaims struct { + jwt.RegisteredClaims + UserID string `json:"uid"` + OrgID string `json:"oid"` + Email string `json:"email"` + Role string `json:"role"` + Scopes []string `json:"scopes,omitempty"` + TokenID string `json:"jti"` +} + +// GenerateTokenPair gera access + refresh tokens +func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) { + now := time.Now() + tokenID := generateTokenID() + + // Access Token (curta duração) + accessClaims := TokenClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: s.config.Issuer, + Subject: userID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)), + ID: tokenID, + }, + UserID: userID, + OrgID: orgID, + Email: email, + Role: role, + TokenID: tokenID, + } + + accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret) + if err != nil { + return "", "", fmt.Errorf("failed to sign access token: %w", err) + } + + // Refresh Token (longa duração) + refreshID := generateTokenID() + refreshClaims := jwt.RegisteredClaims{ + Issuer: s.config.Issuer, + Subject: userID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)), + ID: refreshID, + } + + refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret) + if err != nil { + return "", "", fmt.Errorf("failed to sign refresh token: %w", err) + } + + // Armazenar refresh token no Redis (permite revogação) + if s.redis != nil { + ctx := context.Background() + key := fmt.Sprintf("refresh_token:%s", refreshID) + s.redis.Set(ctx, key, userID, s.config.RefreshExpiration) + } + + return accessToken, refreshToken, nil +} + +// ValidateAccessToken valida access token +func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return s.config.JWTSecret, nil }) - - if err != nil || !token.Valid { - return nil, err + + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - return nil, jwt.ErrInvalidKey + + claims, ok := token.Claims.(*TokenClaims) + if !ok || !token.Valid { + return nil, fmt.Errorf("invalid token claims") } - - return &claims, nil + + // Verificar se token foi revogado + if s.redis != nil { + ctx := context.Background() + revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result() + if revoked != "" { + return nil, fmt.Errorf("token revoked") + } + } + + return claims, nil } -// AuthMiddleware protects routes -func AuthMiddleware() fiber.Handler { +// RevokeToken revoga um token +func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error { + if s.redis == nil { + return nil + } + ctx := context.Background() + return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err() +} + +// ═══════════════════════════════════════════════════════════ +// 🛡️ MIDDLEWARE +// ═══════════════════════════════════════════════════════════ + +// AuthMiddleware middleware de autenticação +func (s *AuthService) AuthMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { - authHeader := c.Get("Authorization") + // Obter IP real (considerando proxies) + ip := c.IP() + if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { + ip = strings.Split(forwarded, ",")[0] + } + // Rate limiting por IP + if !s.rateLimiter.Allow(ip) { + return c.Status(429).JSON(fiber.Map{ + "error": "Too many requests", + "message": "Rate limit exceeded. Try again later.", + }) + } + + authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(401).JSON(fiber.Map{ - "error": "Missing authorization header", + "error": "Unauthorized", + "message": "Missing authorization header", }) } - - // Support both "Bearer " and API keys + token := strings.TrimPrefix(authHeader, "Bearer ") - // Check if it's an API key - if strings.HasPrefix(token, "ophion_") { - // TODO: Validate API key against database - c.Locals("auth_type", "api_key") - c.Locals("api_key", token) - return c.Next() + // API Key authentication + if strings.HasPrefix(token, security.APIKeyPrefix) { + return s.authenticateAPIKey(c, token) } + + // JWT authentication + return s.authenticateJWT(c, token) + } +} - // Validate JWT - claims, err := ValidateJWT(token) - if err != nil { - return c.Status(401).JSON(fiber.Map{ - "error": "Invalid token", +// authenticateAPIKey valida API key +func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error { + // Validar formato + if !security.ValidateAPIKeyFormat(apiKey) { + return c.Status(401).JSON(fiber.Map{ + "error": "Unauthorized", + "message": "Invalid API key format", + }) + } + + // Hash da key para busca + keyHash := security.HashAPIKey(apiKey) + + // Buscar no storage + keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash) + if err != nil || keyInfo == nil { + return c.Status(401).JSON(fiber.Map{ + "error": "Unauthorized", + "message": "Invalid API key", + }) + } + + // Atualizar last_used (async) + go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID) + + // Setar contexto + c.Locals("auth_type", "api_key") + c.Locals("org_id", keyInfo.OrgID) + c.Locals("api_key_id", keyInfo.ID) + c.Locals("scopes", keyInfo.Scopes) + + return c.Next() +} + +// authenticateJWT valida JWT token +func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error { + claims, err := s.ValidateAccessToken(token) + if err != nil { + return c.Status(401).JSON(fiber.Map{ + "error": "Unauthorized", + "message": err.Error(), + }) + } + + // Setar contexto + c.Locals("auth_type", "jwt") + c.Locals("user_id", claims.UserID) + c.Locals("org_id", claims.OrgID) + c.Locals("email", claims.Email) + c.Locals("role", claims.Role) + c.Locals("token_id", claims.TokenID) + + return c.Next() +} + +// RequireScopes middleware que exige scopes específicos +func RequireScopes(required ...string) fiber.Handler { + return func(c *fiber.Ctx) error { + scopes, ok := c.Locals("scopes").([]string) + if !ok { + // JWT tokens têm acesso total por padrão + if c.Locals("auth_type") == "jwt" { + return c.Next() + } + return c.Status(403).JSON(fiber.Map{ + "error": "Forbidden", + "message": "Missing required scopes", }) } - - c.Locals("auth_type", "jwt") - c.Locals("user_id", (*claims)["sub"]) - c.Locals("email", (*claims)["email"]) + + scopeMap := make(map[string]bool) + for _, s := range scopes { + scopeMap[s] = true + } + + for _, req := range required { + if !scopeMap[req] && !scopeMap["*"] { + return c.Status(403).JSON(fiber.Map{ + "error": "Forbidden", + "message": fmt.Sprintf("Missing required scope: %s", req), + }) + } + } return c.Next() } } + +// RequireRole middleware que exige role específico +func RequireRole(roles ...string) fiber.Handler { + return func(c *fiber.Ctx) error { + userRole, ok := c.Locals("role").(string) + if !ok { + return c.Status(403).JSON(fiber.Map{ + "error": "Forbidden", + "message": "Role not found", + }) + } + + for _, r := range roles { + if userRole == r { + return c.Next() + } + } + + // Admin tem acesso a tudo + if userRole == "admin" { + return c.Next() + } + + return c.Status(403).JSON(fiber.Map{ + "error": "Forbidden", + "message": "Insufficient permissions", + }) + } +} + +// ═══════════════════════════════════════════════════════════ +// 🔐 LOGIN PROTECTION +// ═══════════════════════════════════════════════════════════ + +// CheckLoginAllowed verifica se login é permitido (brute force protection) +func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) { + locked, remaining := s.loginTracker.IsLocked(identifier) + return !locked, remaining, nil +} + +// RecordLoginAttempt registra tentativa de login +func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) { + if success { + s.loginTracker.RecordSuccess(identifier) + return false, 0 + } + return s.loginTracker.RecordFailure(identifier) +} + +// ═══════════════════════════════════════════════════════════ +// 🔧 HELPERS +// ═══════════════════════════════════════════════════════════ + +func generateTokenID() string { + bytes := make([]byte, 16) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +// GetUserID obtém user ID do contexto +func GetUserID(c *fiber.Ctx) string { + if id, ok := c.Locals("user_id").(string); ok { + return id + } + return "" +} + +// GetOrgID obtém org ID do contexto +func GetOrgID(c *fiber.Ctx) string { + if id, ok := c.Locals("org_id").(string); ok { + return id + } + return "" +} + +// GetRole obtém role do contexto +func GetRole(c *fiber.Ctx) string { + if role, ok := c.Locals("role").(string); ok { + return role + } + return "" +} diff --git a/internal/security/security.go b/internal/security/security.go new file mode 100644 index 0000000..193f42b --- /dev/null +++ b/internal/security/security.go @@ -0,0 +1,558 @@ +package security + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "fmt" + "net" + "regexp" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "golang.org/x/crypto/bcrypt" +) + +// ═══════════════════════════════════════════════════════════ +// 🔐 CONSTANTES DE SEGURANÇA +// ═══════════════════════════════════════════════════════════ + +const ( + // Bcrypt cost (12-14 recomendado para produção) + BcryptCost = 12 + + // Tamanho mínimo de senha + MinPasswordLength = 12 + + // Tamanho da API Key + APIKeyLength = 32 + + // Prefixo da API Key + APIKeyPrefix = "ophion_" + + // Rate limit padrão (requests por minuto) + DefaultRateLimit = 100 + + // Rate limit para auth (tentativas por minuto) + AuthRateLimit = 5 + + // Tempo de bloqueio após falhas (minutos) + LockoutDuration = 15 + + // Máximo de tentativas antes do bloqueio + MaxFailedAttempts = 5 +) + +// ═══════════════════════════════════════════════════════════ +// 🔑 API KEY MANAGEMENT +// ═══════════════════════════════════════════════════════════ + +// GenerateAPIKey cria uma nova API key segura +func GenerateAPIKey() (key string, hash string, prefix string) { + bytes := make([]byte, APIKeyLength) + if _, err := rand.Read(bytes); err != nil { + panic("failed to generate random bytes") + } + + key = APIKeyPrefix + hex.EncodeToString(bytes) + hash = HashAPIKey(key) + prefix = key[:len(APIKeyPrefix)+8] // ophion_xxxxxxxx + + return key, hash, prefix +} + +// HashAPIKey cria hash SHA256 da API key +func HashAPIKey(key string) string { + hash := sha256.Sum256([]byte(key)) + return hex.EncodeToString(hash[:]) +} + +// ValidateAPIKeyFormat verifica formato da API key +func ValidateAPIKeyFormat(key string) bool { + if !strings.HasPrefix(key, APIKeyPrefix) { + return false + } + // ophion_ + 64 hex chars + return len(key) == len(APIKeyPrefix)+64 +} + +// SecureCompare compara strings em tempo constante (previne timing attacks) +func SecureCompare(a, b string) bool { + return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 +} + +// ═══════════════════════════════════════════════════════════ +// 🔒 PASSWORD MANAGEMENT +// ═══════════════════════════════════════════════════════════ + +// PasswordPolicy define regras de senha +type PasswordPolicy struct { + MinLength int + RequireUppercase bool + RequireLowercase bool + RequireNumbers bool + RequireSpecial bool +} + +// DefaultPasswordPolicy retorna política padrão +func DefaultPasswordPolicy() PasswordPolicy { + return PasswordPolicy{ + MinLength: 12, + RequireUppercase: true, + RequireLowercase: true, + RequireNumbers: true, + RequireSpecial: true, + } +} + +// ValidatePassword verifica se senha atende à política +func ValidatePassword(password string, policy PasswordPolicy) (bool, []string) { + var errors []string + + if len(password) < policy.MinLength { + errors = append(errors, fmt.Sprintf("Senha deve ter no mínimo %d caracteres", policy.MinLength)) + } + + if policy.RequireUppercase && !regexp.MustCompile(`[A-Z]`).MatchString(password) { + errors = append(errors, "Senha deve conter letra maiúscula") + } + + if policy.RequireLowercase && !regexp.MustCompile(`[a-z]`).MatchString(password) { + errors = append(errors, "Senha deve conter letra minúscula") + } + + if policy.RequireNumbers && !regexp.MustCompile(`[0-9]`).MatchString(password) { + errors = append(errors, "Senha deve conter número") + } + + if policy.RequireSpecial && !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password) { + errors = append(errors, "Senha deve conter caractere especial") + } + + // Verificar senhas comuns + if isCommonPassword(password) { + errors = append(errors, "Senha muito comum, escolha outra") + } + + return len(errors) == 0, errors +} + +// HashPassword cria hash bcrypt da senha +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) + return string(bytes), err +} + +// VerifyPassword verifica senha contra hash +func VerifyPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// isCommonPassword verifica senhas comuns +func isCommonPassword(password string) bool { + common := []string{ + "password", "123456", "12345678", "qwerty", "abc123", + "monkey", "1234567", "letmein", "trustno1", "dragon", + "baseball", "iloveyou", "master", "sunshine", "ashley", + "passw0rd", "shadow", "123123", "654321", "superman", + "senha", "mudar123", "admin123", "root123", + } + lower := strings.ToLower(password) + for _, p := range common { + if strings.Contains(lower, p) { + return true + } + } + return false +} + +// ═══════════════════════════════════════════════════════════ +// 🚦 RATE LIMITING +// ═══════════════════════════════════════════════════════════ + +// RateLimiter controla taxa de requests +type RateLimiter struct { + sync.RWMutex + requests map[string]*rateLimitEntry + limit int + window time.Duration +} + +type rateLimitEntry struct { + count int + firstSeen time.Time + blocked bool + blockedAt time.Time +} + +// NewRateLimiter cria novo rate limiter +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string]*rateLimitEntry), + limit: limit, + window: window, + } + // Cleanup goroutine + go rl.cleanup() + return rl +} + +// Allow verifica se request é permitido +func (rl *RateLimiter) Allow(identifier string) bool { + rl.Lock() + defer rl.Unlock() + + now := time.Now() + entry, exists := rl.requests[identifier] + + if !exists { + rl.requests[identifier] = &rateLimitEntry{ + count: 1, + firstSeen: now, + } + return true + } + + // Verificar se está bloqueado + if entry.blocked { + if now.Sub(entry.blockedAt) < time.Duration(LockoutDuration)*time.Minute { + return false + } + // Desbloquear + entry.blocked = false + entry.count = 0 + entry.firstSeen = now + } + + // Verificar janela de tempo + if now.Sub(entry.firstSeen) > rl.window { + entry.count = 1 + entry.firstSeen = now + return true + } + + entry.count++ + + if entry.count > rl.limit { + return false + } + + return true +} + +// Block bloqueia um identificador +func (rl *RateLimiter) Block(identifier string) { + rl.Lock() + defer rl.Unlock() + + entry, exists := rl.requests[identifier] + if !exists { + entry = &rateLimitEntry{} + rl.requests[identifier] = entry + } + entry.blocked = true + entry.blockedAt = time.Now() +} + +// cleanup remove entradas antigas +func (rl *RateLimiter) cleanup() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + rl.Lock() + now := time.Now() + for id, entry := range rl.requests { + if now.Sub(entry.firstSeen) > rl.window*2 && !entry.blocked { + delete(rl.requests, id) + } + } + rl.Unlock() + } +} + +// ═══════════════════════════════════════════════════════════ +// 🛡️ BRUTE FORCE PROTECTION +// ═══════════════════════════════════════════════════════════ + +// LoginAttemptTracker rastreia tentativas de login +type LoginAttemptTracker struct { + sync.RWMutex + attempts map[string]*loginAttempt +} + +type loginAttempt struct { + failures int + lastAttempt time.Time + lockedUntil time.Time +} + +// NewLoginAttemptTracker cria tracker +func NewLoginAttemptTracker() *LoginAttemptTracker { + return &LoginAttemptTracker{ + attempts: make(map[string]*loginAttempt), + } +} + +// RecordFailure registra falha de login +func (t *LoginAttemptTracker) RecordFailure(identifier string) (blocked bool, remainingAttempts int) { + t.Lock() + defer t.Unlock() + + now := time.Now() + attempt, exists := t.attempts[identifier] + + if !exists { + t.attempts[identifier] = &loginAttempt{ + failures: 1, + lastAttempt: now, + } + return false, MaxFailedAttempts - 1 + } + + // Reset se última tentativa foi há muito tempo + if now.Sub(attempt.lastAttempt) > time.Duration(LockoutDuration)*time.Minute { + attempt.failures = 1 + attempt.lastAttempt = now + attempt.lockedUntil = time.Time{} + return false, MaxFailedAttempts - 1 + } + + attempt.failures++ + attempt.lastAttempt = now + + if attempt.failures >= MaxFailedAttempts { + attempt.lockedUntil = now.Add(time.Duration(LockoutDuration) * time.Minute) + return true, 0 + } + + return false, MaxFailedAttempts - attempt.failures +} + +// RecordSuccess registra sucesso de login +func (t *LoginAttemptTracker) RecordSuccess(identifier string) { + t.Lock() + defer t.Unlock() + delete(t.attempts, identifier) +} + +// IsLocked verifica se está bloqueado +func (t *LoginAttemptTracker) IsLocked(identifier string) (bool, time.Duration) { + t.RLock() + defer t.RUnlock() + + attempt, exists := t.attempts[identifier] + if !exists { + return false, 0 + } + + if attempt.lockedUntil.IsZero() { + return false, 0 + } + + remaining := time.Until(attempt.lockedUntil) + if remaining <= 0 { + return false, 0 + } + + return true, remaining +} + +// ═══════════════════════════════════════════════════════════ +// 🔍 INPUT VALIDATION & SANITIZATION +// ═══════════════════════════════════════════════════════════ + +// SanitizeEmail limpa e valida email +func SanitizeEmail(email string) (string, error) { + email = strings.TrimSpace(strings.ToLower(email)) + + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + if !emailRegex.MatchString(email) { + return "", fmt.Errorf("email inválido") + } + + return email, nil +} + +// SanitizeHostname limpa hostname +func SanitizeHostname(hostname string) (string, error) { + hostname = strings.TrimSpace(hostname) + + // Permitir apenas alfanuméricos, hífens e pontos + hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$`) + if !hostnameRegex.MatchString(hostname) && len(hostname) > 1 { + return "", fmt.Errorf("hostname inválido") + } + + return hostname, nil +} + +// SanitizeSQL previne SQL injection (para queries dinâmicas) +func SanitizeSQL(input string) string { + // Remover caracteres perigosos + dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_"} + result := input + for _, d := range dangerous { + result = strings.ReplaceAll(result, d, "") + } + return result +} + +// ValidateIPAddress valida endereço IP +func ValidateIPAddress(ip string) bool { + return net.ParseIP(ip) != nil +} + +// ═══════════════════════════════════════════════════════════ +// 🛡️ SECURITY HEADERS MIDDLEWARE +// ═══════════════════════════════════════════════════════════ + +// SecurityHeaders adiciona headers de segurança +func SecurityHeaders() fiber.Handler { + return func(c *fiber.Ctx) error { + // Prevenir clickjacking + c.Set("X-Frame-Options", "DENY") + + // Prevenir MIME type sniffing + c.Set("X-Content-Type-Options", "nosniff") + + // XSS Protection + c.Set("X-XSS-Protection", "1; mode=block") + + // Content Security Policy + c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:") + + // HSTS (apenas se HTTPS) + if c.Protocol() == "https" { + c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") + } + + // Referrer Policy + c.Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Permissions Policy + c.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") + + // Remove Server header + c.Set("Server", "") + + return c.Next() + } +} + +// ═══════════════════════════════════════════════════════════ +// 📝 AUDIT LOGGING +// ═══════════════════════════════════════════════════════════ + +// AuditEvent representa um evento de auditoria +type AuditEvent struct { + Timestamp time.Time `json:"timestamp"` + EventType string `json:"event_type"` + UserID string `json:"user_id,omitempty"` + OrgID string `json:"org_id,omitempty"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + Resource string `json:"resource"` + Action string `json:"action"` + Status string `json:"status"` + Details map[string]string `json:"details,omitempty"` +} + +// AuditEventType tipos de eventos +const ( + AuditLogin = "auth.login" + AuditLogout = "auth.logout" + AuditLoginFailed = "auth.login_failed" + AuditAPIKeyCreated = "apikey.created" + AuditAPIKeyRevoked = "apikey.revoked" + AuditUserCreated = "user.created" + AuditUserDeleted = "user.deleted" + AuditConfigChanged = "config.changed" + AuditAlertCreated = "alert.created" + AuditDataExport = "data.export" +) + +// AuditLogger interface para logging de auditoria +type AuditLogger interface { + Log(event AuditEvent) error +} + +// ═══════════════════════════════════════════════════════════ +// 🔐 SECRETS MANAGEMENT +// ═══════════════════════════════════════════════════════════ + +// GenerateSecureToken gera token seguro +func GenerateSecureToken(length int) string { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + panic("failed to generate secure token") + } + return base64.URLEncoding.EncodeToString(bytes)[:length] +} + +// MaskSecret mascara segredos para logging +func MaskSecret(secret string) string { + if len(secret) <= 8 { + return "****" + } + return secret[:4] + "****" + secret[len(secret)-4:] +} + +// ═══════════════════════════════════════════════════════════ +// 🌐 IP FILTERING +// ═══════════════════════════════════════════════════════════ + +// IPWhitelist gerencia whitelist de IPs +type IPWhitelist struct { + sync.RWMutex + allowed map[string]bool + cidrs []*net.IPNet +} + +// NewIPWhitelist cria whitelist +func NewIPWhitelist(ips []string, cidrs []string) *IPWhitelist { + wl := &IPWhitelist{ + allowed: make(map[string]bool), + } + + for _, ip := range ips { + wl.allowed[ip] = true + } + + for _, cidr := range cidrs { + _, network, err := net.ParseCIDR(cidr) + if err == nil { + wl.cidrs = append(wl.cidrs, network) + } + } + + return wl +} + +// IsAllowed verifica se IP está permitido +func (wl *IPWhitelist) IsAllowed(ip string) bool { + wl.RLock() + defer wl.RUnlock() + + // Verificar lista direta + if wl.allowed[ip] { + return true + } + + // Verificar CIDRs + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return false + } + + for _, network := range wl.cidrs { + if network.Contains(parsedIP) { + return true + } + } + + return false +}