🔐 Security hardening: auth, rate limiting, brute force protection

- 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
This commit is contained in:
2026-02-05 23:02:06 -03:00
parent dbf9f0497f
commit a94809c812
11 changed files with 2637 additions and 444 deletions

75
.env.example Normal file
View File

@@ -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

119
deploy/docker/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -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
<!-- pom.xml -->
<dependency>
<groupId>com.ophion</groupId>
<artifactId>ophion-apm</artifactId>
<version>1.0.0</version>
</dependency>
```
```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

270
docs/SECURITY.md Normal file
View File

@@ -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=<random-64-chars> # openssl rand -hex 32
ADMIN_PASSWORD=<strong-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

4
go.mod
View File

@@ -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
)

View File

@@ -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"
____ _____ _ _ _____ ____ _ _
/ __ \| __ \| | | |_ _/ __ \| \ | |
| | | | |__) | |__| | | || | | | \| |
| | | | ___/| __ | | || | | | . ` |
| |__| | | | | | |_| || |__| | |\ |
\____/|_| |_| |_|_____\____/|_| \_|
# Check Docker Compose
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
echo "❌ Docker Compose not found. Please install it."
Open Source Observability Platform
Made with 🖤 in Brazil
EOF
echo -e "${NC}"
}
# 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 <comando>"
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
# Start services
echo "🚀 Starting OPHION..."
docker compose up -d
log_success "CLI instalado em /usr/local/bin/ophion"
}
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 ""
# 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
}
# ═══════════════════════════════════════════════════════════
# 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 "$@"

View File

@@ -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
}

View File

@@ -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,
}
}
// ═══════════════════════════════════════════════════════════
// 🎫 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,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
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
}
// 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
// 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 {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
// 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 {
// 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 <token>" 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)
}
// Validate JWT
claims, err := ValidateJWT(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{
"error": "Invalid token",
// JWT authentication
return s.authenticateJWT(c, 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 ""
}

View File

@@ -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
}