feat: Add Alateya, Clan, Eonarch agents + fix gateway-router connection
## Agents Added - Alateya: R&D, biotech, innovations - Clan (Spirit): Community spirit agent - Eonarch: Consciousness evolution agent ## Changes - docker-compose.node1.yml: Added tokens for all 3 new agents - gateway-bot/http_api.py: Added configs and webhook endpoints - gateway-bot/clan_prompt.txt: New prompt file - gateway-bot/eonarch_prompt.txt: New prompt file ## Fixes - Fixed ROUTER_URL from :9102 to :8000 (internal container port) - All 9 Telegram agents now working ## Documentation - Created PROJECT-MASTER-INDEX.md - single entry point - Added various status documents and scripts Tokens configured: - Helion, NUTRA, Agromatrix (existing) - Alateya, Clan, Eonarch (new) - Druid, GreenFood, DAARWIZZ (configured)
This commit is contained in:
100
ops/Makefile
Normal file
100
ops/Makefile
Normal file
@@ -0,0 +1,100 @@
|
||||
#
|
||||
# NODE1 Operations Makefile
|
||||
# Usage: make <target>
|
||||
#
|
||||
|
||||
NODE1_HOST := 144.76.224.179
|
||||
NODE1_USER := root
|
||||
SSH_OPTS := -o StrictHostKeyChecking=accept-new
|
||||
|
||||
.PHONY: help status harden-dry-run harden-apply harden-rollback nginx-install nginx-deploy nginx-reload ssl-setup
|
||||
|
||||
help:
|
||||
@echo "NODE1 Operations"
|
||||
@echo ""
|
||||
@echo "Status:"
|
||||
@echo " make status - Run health check on NODE1"
|
||||
@echo ""
|
||||
@echo "Hardening:"
|
||||
@echo " make harden-dry-run - Show firewall changes (dry run)"
|
||||
@echo " make harden-apply - Apply firewall hardening"
|
||||
@echo " make harden-rollback - Rollback firewall to previous state"
|
||||
@echo ""
|
||||
@echo "Nginx:"
|
||||
@echo " make nginx-install - Install nginx on NODE1"
|
||||
@echo " make nginx-deploy - Deploy nginx config to NODE1"
|
||||
@echo " make nginx-reload - Reload nginx on NODE1"
|
||||
@echo " make ssl-setup - Setup Let's Encrypt SSL"
|
||||
@echo ""
|
||||
@echo "Full hardening:"
|
||||
@echo " make full-harden - nginx-install + nginx-deploy + harden-apply"
|
||||
|
||||
# === Status ===
|
||||
status:
|
||||
@echo "Running status check on NODE1..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) '/opt/microdao-daarion/ops/status.sh'
|
||||
|
||||
status-verbose:
|
||||
@echo "Running verbose status check on NODE1..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) '/opt/microdao-daarion/ops/status.sh --verbose'
|
||||
|
||||
# === Hardening ===
|
||||
harden-dry-run:
|
||||
@echo "Dry run firewall hardening..."
|
||||
scp $(SSH_OPTS) ops/hardening/apply-node1-firewall.sh $(NODE1_USER)@$(NODE1_HOST):/opt/microdao-daarion/ops/hardening/
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'chmod +x /opt/microdao-daarion/ops/hardening/apply-node1-firewall.sh && /opt/microdao-daarion/ops/hardening/apply-node1-firewall.sh --dry-run'
|
||||
|
||||
harden-apply:
|
||||
@echo "Applying firewall hardening..."
|
||||
scp $(SSH_OPTS) ops/hardening/apply-node1-firewall.sh $(NODE1_USER)@$(NODE1_HOST):/opt/microdao-daarion/ops/hardening/
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'chmod +x /opt/microdao-daarion/ops/hardening/apply-node1-firewall.sh && /opt/microdao-daarion/ops/hardening/apply-node1-firewall.sh --apply'
|
||||
|
||||
harden-rollback:
|
||||
@echo "Rolling back firewall..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) '/opt/microdao-daarion/ops/hardening/apply-node1-firewall.sh --rollback'
|
||||
|
||||
# === Nginx ===
|
||||
nginx-install:
|
||||
@echo "Installing nginx on NODE1..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'apt-get update && apt-get install -y nginx'
|
||||
|
||||
nginx-deploy:
|
||||
@echo "Deploying nginx config..."
|
||||
scp $(SSH_OPTS) ops/nginx/node1-api.conf $(NODE1_USER)@$(NODE1_HOST):/etc/nginx/conf.d/node1-api.conf
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'nginx -t'
|
||||
|
||||
nginx-reload:
|
||||
@echo "Reloading nginx..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'systemctl reload nginx'
|
||||
|
||||
nginx-status:
|
||||
@echo "Nginx status..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'systemctl status nginx --no-pager'
|
||||
|
||||
ssl-setup:
|
||||
@echo "Setting up SSL with Let's Encrypt..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'apt-get install -y certbot python3-certbot-nginx && certbot --nginx -d api.daarion.io'
|
||||
|
||||
# === Full Hardening ===
|
||||
full-harden: nginx-install nginx-deploy nginx-reload harden-apply
|
||||
@echo ""
|
||||
@echo "=== Full hardening complete ==="
|
||||
@echo "1. Nginx installed and configured"
|
||||
@echo "2. Firewall rules applied"
|
||||
@echo ""
|
||||
@echo "Next steps:"
|
||||
@echo " 1. Run 'make ssl-setup' to enable HTTPS"
|
||||
@echo " 2. Run 'make status' to verify services"
|
||||
@echo " 3. Test rate limiting: curl -I http://$(NODE1_HOST)"
|
||||
|
||||
# === Verification ===
|
||||
verify-ports:
|
||||
@echo "Checking port exposure..."
|
||||
ssh $(SSH_OPTS) $(NODE1_USER)@$(NODE1_HOST) 'ss -ltnp | grep -E ":(9102|9300|6333|9090|3030|80|443)\b"'
|
||||
|
||||
verify-ratelimit:
|
||||
@echo "Testing rate limiting (should get 429 after ~20 requests)..."
|
||||
@for i in $$(seq 1 25); do \
|
||||
curl -s -o /dev/null -w "%{http_code} " http://$(NODE1_HOST)/health; \
|
||||
done
|
||||
@echo ""
|
||||
195
ops/hardening/apply-node1-firewall.sh
Normal file
195
ops/hardening/apply-node1-firewall.sh
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# NODE1 Firewall Hardening Script
|
||||
# Version: 1.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Usage: ./apply-node1-firewall.sh [--apply|--dry-run|--rollback]
|
||||
# --dry-run Show what would be done (default)
|
||||
# --apply Apply firewall rules
|
||||
# --rollback Restore previous rules
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Admin IPs that should have full access (add your IPs here)
|
||||
ADMIN_IPS=(
|
||||
# "YOUR_OFFICE_IP/32"
|
||||
# "YOUR_VPN_IP/32"
|
||||
)
|
||||
|
||||
# Ports to DENY from public (will only be accessible locally)
|
||||
DENY_PORTS=(
|
||||
"9102" # Router
|
||||
"9300" # Gateway (will be proxied via nginx)
|
||||
"6333" # Qdrant
|
||||
"30633" # Qdrant NodePort
|
||||
"9090" # Prometheus
|
||||
"3030" # Grafana
|
||||
"8890" # Swapper
|
||||
"8000" # Memory Service
|
||||
"9500" # RAG Service
|
||||
"8001" # Vision Encoder
|
||||
"8101" # Parser Pipeline
|
||||
)
|
||||
|
||||
# Ports to ALLOW from public
|
||||
ALLOW_PORTS=(
|
||||
"22" # SSH
|
||||
"80" # HTTP (redirect to HTTPS)
|
||||
"443" # HTTPS (nginx proxy)
|
||||
)
|
||||
|
||||
# Parse arguments
|
||||
MODE="dry-run"
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--apply) MODE="apply" ;;
|
||||
--dry-run) MODE="dry-run" ;;
|
||||
--rollback) MODE="rollback" ;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--apply|--dry-run|--rollback]"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "========================================"
|
||||
echo " NODE1 Firewall Hardening"
|
||||
echo " Mode: $MODE"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Backup current rules
|
||||
backup_rules() {
|
||||
echo "Backing up current UFW rules..."
|
||||
sudo cp /etc/ufw/user.rules /etc/ufw/user.rules.backup.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
|
||||
sudo cp /etc/ufw/user6.rules /etc/ufw/user6.rules.backup.$(date +%Y%m%d_%H%M%S) 2>/dev/null || true
|
||||
echo "Backup saved to /etc/ufw/user.rules.backup.*"
|
||||
}
|
||||
|
||||
# Apply deny rules
|
||||
apply_deny_rules() {
|
||||
for port in "${DENY_PORTS[@]}"; do
|
||||
if [ "$MODE" = "apply" ]; then
|
||||
echo -e "${YELLOW}Denying${NC} port $port from public..."
|
||||
sudo ufw deny $port/tcp comment "Hardening: internal only" 2>/dev/null || true
|
||||
else
|
||||
echo "[DRY-RUN] Would deny port $port/tcp"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Apply allow rules for admin IPs
|
||||
apply_admin_allowlist() {
|
||||
if [ ${#ADMIN_IPS[@]} -eq 0 ]; then
|
||||
echo -e "${YELLOW}Warning:${NC} No admin IPs configured in ADMIN_IPS array"
|
||||
echo "Add your IPs to enable remote admin access to internal ports"
|
||||
return
|
||||
fi
|
||||
|
||||
for ip in "${ADMIN_IPS[@]}"; do
|
||||
for port in "${DENY_PORTS[@]}"; do
|
||||
if [ "$MODE" = "apply" ]; then
|
||||
echo -e "${GREEN}Allowing${NC} $ip to port $port..."
|
||||
sudo ufw allow from $ip to any port $port proto tcp comment "Admin access" 2>/dev/null || true
|
||||
else
|
||||
echo "[DRY-RUN] Would allow $ip to port $port/tcp"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Ensure public ports are allowed
|
||||
apply_allow_rules() {
|
||||
for port in "${ALLOW_PORTS[@]}"; do
|
||||
if [ "$MODE" = "apply" ]; then
|
||||
echo -e "${GREEN}Ensuring${NC} port $port is allowed..."
|
||||
sudo ufw allow $port/tcp 2>/dev/null || true
|
||||
else
|
||||
echo "[DRY-RUN] Would ensure port $port/tcp is allowed"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Rollback to previous rules
|
||||
rollback_rules() {
|
||||
echo "Looking for backup files..."
|
||||
LATEST_BACKUP=$(ls -t /etc/ufw/user.rules.backup.* 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$LATEST_BACKUP" ]; then
|
||||
echo -e "${RED}No backup files found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restoring from: $LATEST_BACKUP"
|
||||
sudo cp "$LATEST_BACKUP" /etc/ufw/user.rules
|
||||
|
||||
LATEST_BACKUP6=$(ls -t /etc/ufw/user6.rules.backup.* 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST_BACKUP6" ]; then
|
||||
sudo cp "$LATEST_BACKUP6" /etc/ufw/user6.rules
|
||||
fi
|
||||
|
||||
sudo ufw reload
|
||||
echo -e "${GREEN}Rollback complete${NC}"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
case $MODE in
|
||||
"apply")
|
||||
echo "=== Applying firewall hardening ==="
|
||||
backup_rules
|
||||
echo ""
|
||||
apply_deny_rules
|
||||
echo ""
|
||||
apply_admin_allowlist
|
||||
echo ""
|
||||
apply_allow_rules
|
||||
echo ""
|
||||
echo "Reloading UFW..."
|
||||
sudo ufw reload
|
||||
echo ""
|
||||
echo -e "${GREEN}Hardening applied!${NC}"
|
||||
echo ""
|
||||
echo "=== Current UFW Status ==="
|
||||
sudo ufw status numbered | head -30
|
||||
;;
|
||||
"rollback")
|
||||
rollback_rules
|
||||
;;
|
||||
"dry-run")
|
||||
echo "=== DRY RUN - No changes will be made ==="
|
||||
echo ""
|
||||
echo "Would backup current rules..."
|
||||
echo ""
|
||||
echo "Ports to DENY from public:"
|
||||
for port in "${DENY_PORTS[@]}"; do
|
||||
echo " - $port/tcp"
|
||||
done
|
||||
echo ""
|
||||
echo "Ports to ALLOW from public:"
|
||||
for port in "${ALLOW_PORTS[@]}"; do
|
||||
echo " - $port/tcp"
|
||||
done
|
||||
echo ""
|
||||
if [ ${#ADMIN_IPS[@]} -gt 0 ]; then
|
||||
echo "Admin IPs with full access:"
|
||||
for ip in "${ADMIN_IPS[@]}"; do
|
||||
echo " - $ip"
|
||||
done
|
||||
else
|
||||
echo -e "${YELLOW}Note: No admin IPs configured${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo "Run with --apply to execute these changes"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
35
ops/hardening/fail2ban-nginx.conf
Normal file
35
ops/hardening/fail2ban-nginx.conf
Normal file
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# Fail2ban configuration for NODE1 Nginx
|
||||
# Install: apt-get install fail2ban
|
||||
# Copy to: /etc/fail2ban/jail.d/nginx-node1.conf
|
||||
#
|
||||
|
||||
[nginx-waf]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-waf
|
||||
logpath = /var/log/nginx/waf-blocks.log
|
||||
maxretry = 5
|
||||
findtime = 300
|
||||
bantime = 1800
|
||||
action = iptables-multiport[name=nginx-waf, port="http,https", protocol=tcp]
|
||||
|
||||
[nginx-auth]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-auth
|
||||
logpath = /var/log/nginx/auth-fails.log
|
||||
maxretry = 10
|
||||
findtime = 600
|
||||
bantime = 3600
|
||||
action = iptables-multiport[name=nginx-auth, port="http,https", protocol=tcp]
|
||||
|
||||
[nginx-ratelimit]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-limit-req
|
||||
logpath = /var/log/nginx/api-error.log
|
||||
maxretry = 20
|
||||
findtime = 60
|
||||
bantime = 600
|
||||
action = iptables-multiport[name=nginx-ratelimit, port="http,https", protocol=tcp]
|
||||
213
ops/hardening/security-regression-test.sh
Normal file
213
ops/hardening/security-regression-test.sh
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# NODE1 Security Regression Test
|
||||
# Version: 1.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Run after each deploy to verify security posture
|
||||
#
|
||||
# Usage: ./security-regression-test.sh [--remote]
|
||||
#
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Config
|
||||
HOST="${TEST_HOST:-https://gateway.daarion.city}"
|
||||
HEALTH_TOKEN="${HEALTH_TOKEN:-dg-health-2026-secret-change-me}"
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
warnings=0
|
||||
|
||||
# Test helper
|
||||
test_check() {
|
||||
local name="$1"
|
||||
local result="$2"
|
||||
local expected="$3"
|
||||
|
||||
if [ "$result" = "$expected" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: $name"
|
||||
((passed++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: $name (got: $result, expected: $expected)"
|
||||
((failed++))
|
||||
fi
|
||||
}
|
||||
|
||||
test_contains() {
|
||||
local name="$1"
|
||||
local haystack="$2"
|
||||
local needle="$3"
|
||||
|
||||
if echo "$haystack" | grep -qi "$needle"; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: $name"
|
||||
((passed++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: $name (missing: $needle)"
|
||||
((failed++))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " NODE1 Security Regression Test"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo " Target: $HOST"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# === 1. TLS/HSTS ===
|
||||
echo "=== 1. TLS & Security Headers ==="
|
||||
|
||||
headers=$(curl -sS -k -I "$HOST/ping" 2>&1)
|
||||
|
||||
test_contains "HSTS header present" "$headers" "strict-transport-security"
|
||||
test_contains "X-Frame-Options present" "$headers" "x-frame-options"
|
||||
test_contains "X-Content-Type-Options present" "$headers" "x-content-type-options"
|
||||
test_contains "X-XSS-Protection present" "$headers" "x-xss-protection"
|
||||
test_contains "Content-Security-Policy present" "$headers" "content-security-policy"
|
||||
|
||||
echo ""
|
||||
|
||||
# === 2. HTTP→HTTPS Redirect ===
|
||||
echo "=== 2. HTTP→HTTPS Redirect ==="
|
||||
|
||||
http_host="${HOST/https:/http:}"
|
||||
redirect_code=$(curl -sS -o /dev/null -w "%{http_code}" "$http_host/ping" 2>/dev/null || echo "000")
|
||||
test_check "HTTP redirects to HTTPS" "$redirect_code" "301"
|
||||
|
||||
echo ""
|
||||
|
||||
# === 3. /health Protection ===
|
||||
echo "=== 3. /health Endpoint Protection ==="
|
||||
|
||||
# Without token (should be 401 or blocked)
|
||||
health_no_token=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/health" 2>/dev/null || echo "000")
|
||||
if [ "$health_no_token" = "401" ] || [ "$health_no_token" = "403" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: /health without token blocked ($health_no_token)"
|
||||
((passed++))
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: /health without token NOT blocked ($health_no_token)"
|
||||
((failed++))
|
||||
fi
|
||||
|
||||
# /ping should work without auth
|
||||
ping_code=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/ping" 2>/dev/null || echo "000")
|
||||
test_check "/ping accessible without auth" "$ping_code" "200"
|
||||
|
||||
echo ""
|
||||
|
||||
# === 4. API Auth Gate ===
|
||||
echo "=== 4. API Auth Gate (/v1/*) ==="
|
||||
|
||||
# Without key (should be 401)
|
||||
v1_no_key=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/v1/test" 2>/dev/null || echo "000")
|
||||
test_check "/v1/* without API key returns 401" "$v1_no_key" "401"
|
||||
|
||||
# With invalid key format
|
||||
v1_bad_key=$(curl -sS -k -o /dev/null -w "%{http_code}" -H "Authorization: Bearer invalid" "$HOST/v1/test" 2>/dev/null || echo "000")
|
||||
test_check "/v1/* with invalid key format returns 401" "$v1_bad_key" "401"
|
||||
|
||||
echo ""
|
||||
|
||||
# === 5. WAF Rules ===
|
||||
echo "=== 5. WAF Rules ==="
|
||||
|
||||
# .env should be blocked (444 = connection closed, shows as 000 in curl)
|
||||
env_code=$(curl -sS -k -o /dev/null -w "%{http_code}" --max-time 5 "$HOST/.env" 2>&1 | grep -oE '[0-9]{3}$' | tail -1 || echo "000")
|
||||
# 000 means connection was closed (444), which is blocked
|
||||
if [[ "$env_code" =~ ^0+$ ]] || [ "$env_code" = "444" ] || [ "$env_code" = "403" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: /.env blocked (connection closed)"
|
||||
((passed++)) || true
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: /.env NOT blocked ($env_code)"
|
||||
((failed++)) || true
|
||||
fi
|
||||
|
||||
# .git should be blocked
|
||||
git_code=$(curl -sS -k -o /dev/null -w "%{http_code}" --max-time 5 "$HOST/.git/config" 2>&1 | grep -oE '[0-9]{3}$' | tail -1 || echo "000")
|
||||
if [[ "$git_code" =~ ^0+$ ]] || [ "$git_code" = "444" ] || [ "$git_code" = "403" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: /.git blocked (connection closed)"
|
||||
((passed++)) || true
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: /.git NOT blocked ($git_code)"
|
||||
((failed++)) || true
|
||||
fi
|
||||
|
||||
# SQL injection attempt
|
||||
sql_code=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/?q=select+*+from+users" 2>/dev/null || echo "000")
|
||||
test_check "SQL injection blocked" "$sql_code" "403"
|
||||
|
||||
# wp-admin blocked
|
||||
wp_code=$(curl -sS -k -o /dev/null -w "%{http_code}" --max-time 5 "$HOST/wp-admin/" 2>&1 | grep -oE '[0-9]{3}$' | tail -1 || echo "000")
|
||||
if [[ "$wp_code" =~ ^0+$ ]] || [ "$wp_code" = "444" ] || [ "$wp_code" = "403" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: /wp-admin blocked (connection closed)"
|
||||
((passed++)) || true
|
||||
else
|
||||
echo -e "${RED}❌ FAIL${NC}: /wp-admin NOT blocked ($wp_code)"
|
||||
((failed++)) || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# === 6. Rate Limiting ===
|
||||
echo "=== 6. Rate Limiting ==="
|
||||
|
||||
echo -n "Sending 30 rapid requests to /ping... "
|
||||
got_429=false
|
||||
for i in $(seq 1 30); do
|
||||
code=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/ping" 2>/dev/null || echo "000")
|
||||
if [ "$code" = "429" ]; then
|
||||
got_429=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$got_429" = true ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: Rate limit (429) triggered"
|
||||
((passed++))
|
||||
else
|
||||
echo -e "${YELLOW}⚠ WARN${NC}: Rate limit (429) not triggered (may need more requests or higher rate)"
|
||||
((warnings++))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# === 7. Default Route ===
|
||||
echo "=== 7. Default Route Security ==="
|
||||
|
||||
# Unknown endpoint should not expose info
|
||||
unknown_code=$(curl -sS -k -o /dev/null -w "%{http_code}" "$HOST/unknown-endpoint-xyz" 2>/dev/null || echo "000")
|
||||
if [ "$unknown_code" = "404" ] || [ "$unknown_code" = "401" ] || [ "$unknown_code" = "403" ]; then
|
||||
echo -e "${GREEN}✅ PASS${NC}: Unknown endpoint returns safe code ($unknown_code)"
|
||||
((passed++))
|
||||
else
|
||||
echo -e "${YELLOW}⚠ WARN${NC}: Unknown endpoint returns $unknown_code"
|
||||
((warnings++))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# === Summary ===
|
||||
echo "========================================"
|
||||
echo " Security Regression Test Summary"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo -e " ${GREEN}Passed:${NC} $passed"
|
||||
echo -e " ${RED}Failed:${NC} $failed"
|
||||
echo -e " ${YELLOW}Warnings:${NC} $warnings"
|
||||
echo ""
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
echo -e "${RED}SECURITY REGRESSION DETECTED${NC}"
|
||||
exit 1
|
||||
elif [ "$warnings" -gt 0 ]; then
|
||||
echo -e "${YELLOW}Tests passed with warnings${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${GREEN}All security tests passed${NC}"
|
||||
exit 0
|
||||
fi
|
||||
180
ops/nginx/node1-api.conf
Normal file
180
ops/nginx/node1-api.conf
Normal file
@@ -0,0 +1,180 @@
|
||||
#
|
||||
# NODE1 API Gateway - Nginx Configuration
|
||||
# Version: 1.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Features:
|
||||
# - Rate limiting per IP (10 req/s, burst 20)
|
||||
# - Connection limiting (20 concurrent per IP)
|
||||
# - Security headers
|
||||
# - Upstream keepalive
|
||||
# - Heavy endpoint separate limits
|
||||
#
|
||||
|
||||
# === Rate Limit Zones ===
|
||||
# Standard API: 10 req/s per IP
|
||||
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
|
||||
# Heavy endpoints (RAG, image, search): 2 req/s per IP
|
||||
limit_req_zone $binary_remote_addr zone=heavy_per_ip:10m rate=2r/s;
|
||||
# Connection limit per IP
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
|
||||
|
||||
# === Upstreams ===
|
||||
upstream gateway_upstream {
|
||||
server 127.0.0.1:9300;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
upstream grafana_upstream {
|
||||
server 127.0.0.1:3030;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
upstream prometheus_upstream {
|
||||
server 127.0.0.1:9090;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
# === Main API Server ===
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.daarion.io _;
|
||||
|
||||
# Redirect to HTTPS (uncomment when SSL is configured)
|
||||
# return 301 https://$host$request_uri;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Rate limit status page (for debugging)
|
||||
location = /nginx-status {
|
||||
stub_status on;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Health check endpoint (no rate limit)
|
||||
location = /health {
|
||||
proxy_pass http://gateway_upstream/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
|
||||
# Heavy endpoints - stricter rate limit
|
||||
location ~ ^/(v1/rag|v1/image|v1/search|v1/embed) {
|
||||
limit_req zone=heavy_per_ip burst=5 nodelay;
|
||||
limit_conn conn_per_ip 10;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Longer timeouts for heavy operations
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 120s;
|
||||
proxy_read_timeout 120s;
|
||||
|
||||
client_max_body_size 50m;
|
||||
}
|
||||
|
||||
# Webhook endpoints - higher burst for Telegram
|
||||
location ~ ^/(webhook|telegram) {
|
||||
limit_req zone=api_per_ip burst=50 nodelay;
|
||||
limit_conn conn_per_ip 30;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# Default - standard rate limit
|
||||
location / {
|
||||
limit_req zone=api_per_ip burst=20 nodelay;
|
||||
limit_conn conn_per_ip 20;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# Rate limit exceeded - custom error
|
||||
error_page 429 = @rate_limited;
|
||||
location @rate_limited {
|
||||
default_type application/json;
|
||||
return 429 '{"error": "rate_limit_exceeded", "message": "Too many requests. Please slow down.", "retry_after": 1}';
|
||||
}
|
||||
}
|
||||
|
||||
# === Admin Panel (Internal Only) ===
|
||||
# Access via SSH tunnel: ssh -L 3030:localhost:3030 root@node1
|
||||
# Or via allowlisted IPs
|
||||
server {
|
||||
listen 127.0.0.1:8080;
|
||||
server_name localhost;
|
||||
|
||||
# Grafana
|
||||
location /grafana/ {
|
||||
proxy_pass http://grafana_upstream/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Prometheus
|
||||
location /prometheus/ {
|
||||
proxy_pass http://prometheus_upstream/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
# === HTTPS Server (uncomment after certbot) ===
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name api.daarion.io;
|
||||
#
|
||||
# ssl_certificate /etc/letsencrypt/live/api.daarion.io/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/api.daarion.io/privkey.pem;
|
||||
# ssl_session_timeout 1d;
|
||||
# ssl_session_cache shared:SSL:50m;
|
||||
# ssl_session_tickets off;
|
||||
#
|
||||
# # Modern SSL config
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
#
|
||||
# # HSTS
|
||||
# add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
#
|
||||
# # (same location blocks as HTTP server above)
|
||||
# include /etc/nginx/conf.d/node1-api-locations.conf;
|
||||
# }
|
||||
284
ops/nginx/node1-hardened-v3.conf
Normal file
284
ops/nginx/node1-hardened-v3.conf
Normal file
@@ -0,0 +1,284 @@
|
||||
#
|
||||
# NODE1 Hardened Nginx Configuration v3
|
||||
# Version: 3.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# v3 Features:
|
||||
# - /health protected (allowlist + token)
|
||||
# - Auth-gate for /v1/* endpoints
|
||||
# - Nginx metrics endpoint for Prometheus
|
||||
# - Enhanced WAF rules
|
||||
# - Fail2ban integration ready
|
||||
#
|
||||
|
||||
# === Rate Limit Zones ===
|
||||
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=heavy_per_ip:10m rate=2r/s;
|
||||
limit_req_zone $binary_remote_addr zone=webhook_per_ip:10m rate=50r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth_fail:10m rate=5r/s;
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
|
||||
|
||||
# === Auth token (change this!) ===
|
||||
# Generate: openssl rand -hex 32
|
||||
map $http_x_health_token $health_token_valid {
|
||||
default 0;
|
||||
"dg-health-2026-secret-change-me" 1;
|
||||
}
|
||||
|
||||
# API Key validation map
|
||||
map $http_authorization $api_key_valid {
|
||||
default 0;
|
||||
"~^Bearer\s+sk-[a-zA-Z0-9]{32,}$" 1;
|
||||
}
|
||||
|
||||
map $http_x_api_key $x_api_key_valid {
|
||||
default 0;
|
||||
"~^sk-[a-zA-Z0-9]{32,}$" 1;
|
||||
}
|
||||
|
||||
# === Logging format (no auth headers, fail2ban ready) ===
|
||||
log_format api_safe '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'rt=$request_time';
|
||||
|
||||
log_format fail2ban '$remote_addr - [$time_local] "$request" $status';
|
||||
|
||||
# === Upstream ===
|
||||
upstream gateway_upstream {
|
||||
server 127.0.0.1:9300;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
# === HTTP → HTTPS redirect ===
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name gateway.daarion.city api.daarion.io 144.76.224.179 _;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# === Main HTTPS Server ===
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name gateway.daarion.city api.daarion.io 144.76.224.179;
|
||||
|
||||
# === SSL Configuration ===
|
||||
ssl_certificate /etc/letsencrypt/live/gateway.daarion.city/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/gateway.daarion.city/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# === Security Headers ===
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' wss: https:;" always;
|
||||
|
||||
# === Logging ===
|
||||
access_log /var/log/nginx/api-access.log api_safe;
|
||||
access_log /var/log/nginx/fail2ban.log fail2ban;
|
||||
error_log /var/log/nginx/api-error.log warn;
|
||||
|
||||
# === WAF-lite: Block sensitive files ===
|
||||
location ~* \.(env|git|sql|bak|swp|old|backup|log|ini|conf|config|yml|yaml|json|xml|db|sqlite|pem|key)$ {
|
||||
access_log /var/log/nginx/waf-blocks.log fail2ban;
|
||||
return 444;
|
||||
}
|
||||
|
||||
location ~* ^/(\.git|\.svn|\.hg|\.env|wp-admin|wp-login|phpmyadmin|admin\.php|xmlrpc\.php|\.aws|\.docker) {
|
||||
access_log /var/log/nginx/waf-blocks.log fail2ban;
|
||||
return 444;
|
||||
}
|
||||
|
||||
# Block SQL injection attempts
|
||||
if ($query_string ~* "(union|select|insert|drop|delete|update|truncate|exec|script|alert|eval|base64)") {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# Block suspicious user agents
|
||||
if ($http_user_agent ~* (sqlmap|nikto|nmap|masscan|zgrab|python-requests/2\.[0-9]+\.[0-9]+$)) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
# === Nginx metrics (internal only) ===
|
||||
location = /nginx-status {
|
||||
stub_status on;
|
||||
allow 127.0.0.1;
|
||||
allow 10.42.0.0/16; # K8s pod network
|
||||
allow 10.43.0.0/16; # K8s service network
|
||||
deny all;
|
||||
}
|
||||
|
||||
# === Health check (protected) ===
|
||||
location = /health {
|
||||
# Allow localhost
|
||||
set $health_allowed 0;
|
||||
if ($remote_addr = "127.0.0.1") {
|
||||
set $health_allowed 1;
|
||||
}
|
||||
# Allow with valid token
|
||||
if ($health_token_valid = 1) {
|
||||
set $health_allowed 1;
|
||||
}
|
||||
# Allow internal networks (Prometheus, K8s)
|
||||
if ($remote_addr ~ "^(10\.(42|43)\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)") {
|
||||
set $health_allowed 1;
|
||||
}
|
||||
|
||||
if ($health_allowed = 0) {
|
||||
return 401;
|
||||
}
|
||||
|
||||
proxy_pass http://gateway_upstream/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
|
||||
# === Public health (limited info) ===
|
||||
location = /ping {
|
||||
default_type application/json;
|
||||
return 200 '{"status":"ok"}';
|
||||
}
|
||||
|
||||
# === Webhook endpoints (Telegram, etc.) - no auth ===
|
||||
location ~ ^/(webhook|telegram|bot[0-9]+) {
|
||||
limit_req zone=webhook_per_ip burst=100 nodelay;
|
||||
limit_conn conn_per_ip 50;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# === API v1 endpoints (require auth) ===
|
||||
location ~ ^/v1/ {
|
||||
# Check for valid API key
|
||||
set $auth_valid 0;
|
||||
if ($api_key_valid = 1) {
|
||||
set $auth_valid 1;
|
||||
}
|
||||
if ($x_api_key_valid = 1) {
|
||||
set $auth_valid 1;
|
||||
}
|
||||
# Allow internal networks (service-to-service)
|
||||
if ($remote_addr ~ "^(127\.0\.0\.1|10\.(42|43)\.|172\.(1[6-9]|2[0-9]|3[01])\.)") {
|
||||
set $auth_valid 1;
|
||||
}
|
||||
|
||||
if ($auth_valid = 0) {
|
||||
access_log /var/log/nginx/auth-fails.log fail2ban;
|
||||
return 401 '{"error":"unauthorized","message":"Valid API key required"}';
|
||||
}
|
||||
|
||||
limit_req zone=heavy_per_ip burst=5 nodelay;
|
||||
limit_conn conn_per_ip 10;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
# === Root / docs / public (rate limited) ===
|
||||
location / {
|
||||
limit_req zone=api_per_ip burst=20 nodelay;
|
||||
limit_conn conn_per_ip 20;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# === Error pages ===
|
||||
error_page 401 = @unauthorized;
|
||||
location @unauthorized {
|
||||
default_type application/json;
|
||||
return 401 '{"error":"unauthorized","message":"Authentication required"}';
|
||||
}
|
||||
|
||||
error_page 429 = @rate_limited;
|
||||
location @rate_limited {
|
||||
default_type application/json;
|
||||
return 429 '{"error":"rate_limit_exceeded","message":"Too many requests","retry_after":1}';
|
||||
}
|
||||
|
||||
error_page 403 = @forbidden;
|
||||
location @forbidden {
|
||||
default_type application/json;
|
||||
return 403 '{"error":"forbidden","message":"Access denied"}';
|
||||
}
|
||||
}
|
||||
|
||||
# === WebSocket upgrade mapping ===
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# === Admin Panel (localhost only) ===
|
||||
server {
|
||||
listen 127.0.0.1:8080;
|
||||
server_name localhost;
|
||||
|
||||
location /grafana/ {
|
||||
proxy_pass http://127.0.0.1:3030/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /prometheus/ {
|
||||
proxy_pass http://127.0.0.1:9090/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /nginx-metrics {
|
||||
stub_status on;
|
||||
}
|
||||
}
|
||||
224
ops/nginx/node1-hardened.conf
Normal file
224
ops/nginx/node1-hardened.conf
Normal file
@@ -0,0 +1,224 @@
|
||||
#
|
||||
# NODE1 Hardened Nginx Configuration
|
||||
# Version: 2.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Features:
|
||||
# - TLS 1.2/1.3 only with modern ciphers
|
||||
# - HSTS with preload
|
||||
# - Rate limiting (standard + heavy endpoints)
|
||||
# - WAF-lite rules (block scanners, sensitive files)
|
||||
# - Security headers (XSS, CSRF, clickjacking)
|
||||
# - Logging with privacy (no auth headers)
|
||||
#
|
||||
|
||||
# === Rate Limit Zones ===
|
||||
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=heavy_per_ip:10m rate=2r/s;
|
||||
limit_req_zone $binary_remote_addr zone=webhook_per_ip:10m rate=50r/s;
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
|
||||
|
||||
# === Logging format (no Authorization header) ===
|
||||
log_format api_safe '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'rt=$request_time uct="$upstream_connect_time" '
|
||||
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
|
||||
# === Upstream ===
|
||||
upstream gateway_upstream {
|
||||
server 127.0.0.1:9300;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
# === HTTP → HTTPS redirect ===
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name gateway.daarion.city api.daarion.io 144.76.224.179 _;
|
||||
|
||||
# Allow ACME challenge for certificate renewal
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
# Redirect everything else to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# === Main HTTPS Server ===
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name gateway.daarion.city api.daarion.io 144.76.224.179;
|
||||
|
||||
# === SSL Configuration ===
|
||||
ssl_certificate /etc/letsencrypt/live/gateway.daarion.city/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/gateway.daarion.city/privkey.pem;
|
||||
|
||||
# Modern SSL (TLS 1.2+ only)
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# SSL session
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# OCSP Stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
# === Security Headers ===
|
||||
# HSTS (2 years, with preload)
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
# Prevent clickjacking
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
# Prevent MIME sniffing
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
# XSS Protection
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
# Referrer policy
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
# Content Security Policy (adjust as needed)
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' wss: https:;" always;
|
||||
|
||||
# === Logging ===
|
||||
access_log /var/log/nginx/api-access.log api_safe;
|
||||
error_log /var/log/nginx/api-error.log warn;
|
||||
|
||||
# === WAF-lite: Block sensitive files ===
|
||||
location ~* \.(env|git|sql|bak|swp|old|backup|log|ini|conf|config|yml|yaml|json|xml|db|sqlite)$ {
|
||||
return 444; # Close connection without response
|
||||
}
|
||||
|
||||
# Block common attack paths
|
||||
location ~* ^/(\.git|\.svn|\.hg|\.env|wp-admin|wp-login|phpmyadmin|admin\.php|xmlrpc\.php) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
# Block suspicious query strings
|
||||
if ($query_string ~* "(union|select|insert|drop|delete|update|truncate|exec|script|alert)") {
|
||||
return 403;
|
||||
}
|
||||
|
||||
# === Rate limit status (internal only) ===
|
||||
location = /nginx-status {
|
||||
stub_status on;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# === Health check (no rate limit) ===
|
||||
location = /health {
|
||||
limit_req off;
|
||||
proxy_pass http://gateway_upstream/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
}
|
||||
|
||||
# === Webhook endpoints (higher burst for Telegram) ===
|
||||
location ~ ^/(webhook|telegram|bot) {
|
||||
limit_req zone=webhook_per_ip burst=100 nodelay;
|
||||
limit_conn conn_per_ip 50;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# === Heavy endpoints (stricter limit) ===
|
||||
location ~ ^/(v1/rag|v1/image|v1/search|v1/embed|v1/generate) {
|
||||
limit_req zone=heavy_per_ip burst=5 nodelay;
|
||||
limit_conn conn_per_ip 10;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Longer timeouts for heavy operations
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
# === Default API (standard rate limit) ===
|
||||
location / {
|
||||
limit_req zone=api_per_ip burst=20 nodelay;
|
||||
limit_conn conn_per_ip 20;
|
||||
|
||||
proxy_pass http://gateway_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
# === Error pages ===
|
||||
error_page 429 = @rate_limited;
|
||||
location @rate_limited {
|
||||
default_type application/json;
|
||||
return 429 '{"error": "rate_limit_exceeded", "message": "Too many requests", "retry_after": 1}';
|
||||
}
|
||||
|
||||
error_page 403 = @forbidden;
|
||||
location @forbidden {
|
||||
default_type application/json;
|
||||
return 403 '{"error": "forbidden", "message": "Access denied"}';
|
||||
}
|
||||
}
|
||||
|
||||
# === WebSocket upgrade mapping ===
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# === Admin Panel (localhost only, via SSH tunnel) ===
|
||||
server {
|
||||
listen 127.0.0.1:8080;
|
||||
server_name localhost;
|
||||
|
||||
location /grafana/ {
|
||||
proxy_pass http://127.0.0.1:3030/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /prometheus/ {
|
||||
proxy_pass http://127.0.0.1:9090/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
279
ops/secrets/api-keys.sh
Normal file
279
ops/secrets/api-keys.sh
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# NODE1 API Key Management
|
||||
# Version: 1.0
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Usage:
|
||||
# ./api-keys.sh create <name> [--admin]
|
||||
# ./api-keys.sh revoke <key_id>
|
||||
# ./api-keys.sh list
|
||||
# ./api-keys.sh rotate-health
|
||||
# ./api-keys.sh verify <key>
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Paths
|
||||
KEYS_DIR="/opt/microdao-daarion/secrets"
|
||||
KEYS_FILE="$KEYS_DIR/api-keys.conf"
|
||||
HEALTH_TOKEN_FILE="$KEYS_DIR/health-token.conf"
|
||||
NGINX_KEYS_MAP="$KEYS_DIR/nginx-api-keys.conf"
|
||||
LOG_FILE="/var/log/microdao/api-keys.log"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Initialize directories
|
||||
init_dirs() {
|
||||
mkdir -p "$KEYS_DIR"
|
||||
mkdir -p "$(dirname $LOG_FILE)"
|
||||
chmod 700 "$KEYS_DIR"
|
||||
touch "$KEYS_FILE" "$HEALTH_TOKEN_FILE" "$NGINX_KEYS_MAP"
|
||||
chmod 600 "$KEYS_FILE" "$HEALTH_TOKEN_FILE" "$NGINX_KEYS_MAP"
|
||||
}
|
||||
|
||||
# Log action (without exposing secrets)
|
||||
log_action() {
|
||||
local action="$1"
|
||||
local key_id="$2"
|
||||
local details="$3"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') | $action | key_id=$key_id | $details" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Generate random key
|
||||
generate_key() {
|
||||
openssl rand -hex 32
|
||||
}
|
||||
|
||||
# Generate key ID
|
||||
generate_key_id() {
|
||||
local name="$1"
|
||||
local timestamp=$(date +%Y%m%d%H%M%S)
|
||||
local random=$(openssl rand -hex 4)
|
||||
echo "kid_${name}_${timestamp}_${random}"
|
||||
}
|
||||
|
||||
# Create new API key
|
||||
create_key() {
|
||||
local name="$1"
|
||||
local is_admin="${2:-false}"
|
||||
|
||||
if [ -z "$name" ]; then
|
||||
echo -e "${RED}Error: Key name required${NC}"
|
||||
echo "Usage: $0 create <name> [--admin]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local key_id=$(generate_key_id "$name")
|
||||
local secret=$(generate_key)
|
||||
local api_key="sk-${secret}"
|
||||
local created_at=$(date -Iseconds)
|
||||
local scope="standard"
|
||||
[ "$is_admin" = "--admin" ] && scope="admin"
|
||||
|
||||
# Save to keys file (key_id|name|scope|created_at|hash)
|
||||
local key_hash=$(echo -n "$api_key" | sha256sum | cut -d' ' -f1)
|
||||
echo "${key_id}|${name}|${scope}|${created_at}|${key_hash}" >> "$KEYS_FILE"
|
||||
|
||||
# Update nginx map
|
||||
rebuild_nginx_map
|
||||
|
||||
log_action "CREATE" "$key_id" "name=$name scope=$scope"
|
||||
|
||||
echo -e "${GREEN}API Key Created${NC}"
|
||||
echo "================================"
|
||||
echo "Key ID: $key_id"
|
||||
echo "Name: $name"
|
||||
echo "Scope: $scope"
|
||||
echo "API Key: $api_key"
|
||||
echo "================================"
|
||||
echo -e "${YELLOW}WARNING: Save this key now. It cannot be retrieved later.${NC}"
|
||||
}
|
||||
|
||||
# Revoke API key
|
||||
revoke_key() {
|
||||
local key_id="$1"
|
||||
|
||||
if [ -z "$key_id" ]; then
|
||||
echo -e "${RED}Error: Key ID required${NC}"
|
||||
echo "Usage: $0 revoke <key_id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "^${key_id}|" "$KEYS_FILE" 2>/dev/null; then
|
||||
echo -e "${RED}Error: Key ID not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove from keys file
|
||||
sed -i "/^${key_id}|/d" "$KEYS_FILE"
|
||||
|
||||
# Rebuild nginx map
|
||||
rebuild_nginx_map
|
||||
|
||||
log_action "REVOKE" "$key_id" "revoked"
|
||||
|
||||
echo -e "${GREEN}Key revoked: $key_id${NC}"
|
||||
echo "Run 'nginx -s reload' to apply changes"
|
||||
}
|
||||
|
||||
# List all keys (without secrets)
|
||||
list_keys() {
|
||||
echo "API Keys"
|
||||
echo "========"
|
||||
echo ""
|
||||
printf "%-40s %-15s %-10s %s\n" "KEY_ID" "NAME" "SCOPE" "CREATED"
|
||||
printf "%-40s %-15s %-10s %s\n" "------" "----" "-----" "-------"
|
||||
|
||||
if [ -f "$KEYS_FILE" ] && [ -s "$KEYS_FILE" ]; then
|
||||
while IFS='|' read -r key_id name scope created_at hash; do
|
||||
printf "%-40s %-15s %-10s %s\n" "$key_id" "$name" "$scope" "${created_at:0:19}"
|
||||
done < "$KEYS_FILE"
|
||||
else
|
||||
echo "(no keys)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Total: $(wc -l < "$KEYS_FILE" 2>/dev/null || echo 0) keys"
|
||||
}
|
||||
|
||||
# Verify a key (returns key_id if valid)
|
||||
verify_key() {
|
||||
local api_key="$1"
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
echo -e "${RED}Error: API key required${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local key_hash=$(echo -n "$api_key" | sha256sum | cut -d' ' -f1)
|
||||
|
||||
while IFS='|' read -r key_id name scope created_at stored_hash; do
|
||||
if [ "$key_hash" = "$stored_hash" ]; then
|
||||
echo -e "${GREEN}Valid${NC}"
|
||||
echo "Key ID: $key_id"
|
||||
echo "Name: $name"
|
||||
echo "Scope: $scope"
|
||||
log_action "VERIFY" "$key_id" "valid"
|
||||
exit 0
|
||||
fi
|
||||
done < "$KEYS_FILE"
|
||||
|
||||
echo -e "${RED}Invalid key${NC}"
|
||||
log_action "VERIFY" "unknown" "invalid"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Rotate health token
|
||||
rotate_health_token() {
|
||||
local new_token=$(generate_key)
|
||||
local old_token=""
|
||||
|
||||
# Read old token if exists
|
||||
if [ -f "$HEALTH_TOKEN_FILE" ] && [ -s "$HEALTH_TOKEN_FILE" ]; then
|
||||
old_token=$(grep "^current=" "$HEALTH_TOKEN_FILE" | cut -d'=' -f2)
|
||||
fi
|
||||
|
||||
# Write new config (current + previous for grace period)
|
||||
cat > "$HEALTH_TOKEN_FILE" << EOF
|
||||
# Health Token Configuration
|
||||
# Generated: $(date -Iseconds)
|
||||
# Previous token valid for 24h after rotation
|
||||
current=$new_token
|
||||
previous=$old_token
|
||||
rotated_at=$(date -Iseconds)
|
||||
EOF
|
||||
|
||||
chmod 600 "$HEALTH_TOKEN_FILE"
|
||||
|
||||
# Update nginx include
|
||||
cat > "$KEYS_DIR/nginx-health-token.conf" << EOF
|
||||
# Auto-generated health token map
|
||||
# Do not edit manually
|
||||
map \$http_x_health_token \$health_token_valid {
|
||||
default 0;
|
||||
"$new_token" 1;
|
||||
EOF
|
||||
|
||||
# Add previous token if exists (grace period)
|
||||
if [ -n "$old_token" ]; then
|
||||
echo " \"$old_token\" 1; # previous (grace period)" >> "$KEYS_DIR/nginx-health-token.conf"
|
||||
fi
|
||||
|
||||
echo "}" >> "$KEYS_DIR/nginx-health-token.conf"
|
||||
chmod 600 "$KEYS_DIR/nginx-health-token.conf"
|
||||
|
||||
log_action "ROTATE_HEALTH" "-" "token rotated"
|
||||
|
||||
echo -e "${GREEN}Health Token Rotated${NC}"
|
||||
echo "================================"
|
||||
echo "New Token: $new_token"
|
||||
echo "================================"
|
||||
echo -e "${YELLOW}Update your monitoring systems with the new token.${NC}"
|
||||
echo "Previous token remains valid for grace period."
|
||||
echo "Run 'nginx -s reload' to apply changes"
|
||||
}
|
||||
|
||||
# Rebuild nginx API keys map
|
||||
rebuild_nginx_map() {
|
||||
cat > "$NGINX_KEYS_MAP" << 'HEADER'
|
||||
# Auto-generated API keys map
|
||||
# Do not edit manually - use api-keys.sh
|
||||
# Format: key hash -> "key_id:scope"
|
||||
|
||||
map $api_key_hash $api_key_info {
|
||||
default "";
|
||||
HEADER
|
||||
|
||||
while IFS='|' read -r key_id name scope created_at hash; do
|
||||
echo " \"$hash\" \"$key_id:$scope\";" >> "$NGINX_KEYS_MAP"
|
||||
done < "$KEYS_FILE"
|
||||
|
||||
echo "}" >> "$NGINX_KEYS_MAP"
|
||||
chmod 600 "$NGINX_KEYS_MAP"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
echo "NODE1 API Key Management"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 create <name> [--admin] Create new API key"
|
||||
echo " $0 revoke <key_id> Revoke API key"
|
||||
echo " $0 list List all keys"
|
||||
echo " $0 verify <api_key> Verify API key"
|
||||
echo " $0 rotate-health Rotate health check token"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 create monitoring"
|
||||
echo " $0 create admin-user --admin"
|
||||
echo " $0 revoke kid_monitoring_20260126_abc123"
|
||||
}
|
||||
|
||||
# Main
|
||||
init_dirs
|
||||
|
||||
case "${1:-}" in
|
||||
create)
|
||||
create_key "$2" "$3"
|
||||
;;
|
||||
revoke)
|
||||
revoke_key "$2"
|
||||
;;
|
||||
list)
|
||||
list_keys
|
||||
;;
|
||||
verify)
|
||||
verify_key "$2"
|
||||
;;
|
||||
rotate-health)
|
||||
rotate_health_token
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
228
ops/status.sh
Executable file
228
ops/status.sh
Executable file
@@ -0,0 +1,228 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# NODE1 Status Check Script
|
||||
# Version: 1.1
|
||||
# Last Updated: 2026-01-26
|
||||
#
|
||||
# Usage: ./ops/status.sh [--remote] [--verbose]
|
||||
# --remote Run on NODE1 via SSH
|
||||
# --verbose Show detailed output
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Config
|
||||
NODE1_HOST="144.76.224.179"
|
||||
NODE1_USER="root"
|
||||
|
||||
# Parse arguments
|
||||
REMOTE=false
|
||||
VERBOSE=false
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--remote) REMOTE=true ;;
|
||||
--verbose) VERBOSE=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Remote execution wrapper
|
||||
if [ "$REMOTE" = true ]; then
|
||||
echo "Connecting to NODE1 ($NODE1_HOST)..."
|
||||
|
||||
# Copy and execute on remote
|
||||
ssh -o StrictHostKeyChecking=accept-new ${NODE1_USER}@${NODE1_HOST} "bash -s" << 'REMOTE_SCRIPT'
|
||||
# Inline the status check for remote execution
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "========================================"
|
||||
echo " NODE1 Status Check (Remote)"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
echo "=== Health Endpoints ==="
|
||||
failed=0
|
||||
|
||||
for check in "Router:9102:/health" "Gateway:9300:/health" "Memory:8000:/health" \
|
||||
"RAG:9500:/health" "Swapper:8890:/health" "Qdrant:6333:/healthz" \
|
||||
"Vision:8001:/health" "Parser:8101:/health" "Prometheus:9090:/-/healthy" \
|
||||
"Grafana:3030:/api/health"; do
|
||||
name="${check%%:*}"
|
||||
rest="${check#*:}"
|
||||
port="${rest%%:*}"
|
||||
path="${rest#*:}"
|
||||
url="http://127.0.0.1:${port}${path}"
|
||||
|
||||
code=$(curl -sS -m 5 -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$code" = "200" ]; then
|
||||
echo -e "${GREEN}✅${NC} $name (${port}): ${GREEN}$code${NC}"
|
||||
else
|
||||
echo -e "${RED}❌${NC} $name (${port}): ${RED}$code${NC}"
|
||||
((failed++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== DNS Resolution ==="
|
||||
if docker exec dagi-memory-service-node1 python3 -c "import socket; socket.gethostbyname('dagi-qdrant-node1')" 2>/dev/null; then
|
||||
echo -e "${GREEN}✅${NC} DNS: dagi-qdrant-node1 resolves"
|
||||
else
|
||||
echo -e "${RED}❌${NC} DNS: dagi-qdrant-node1 does NOT resolve"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== System Resources ==="
|
||||
echo "Hostname: $(hostname)"
|
||||
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
|
||||
echo "Memory: $(free -h | awk '/^Mem:/ {print $3 "/" $2}')"
|
||||
echo "Disk: $(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 " used)"}')"
|
||||
|
||||
echo ""
|
||||
echo "=== Docker Containers (top 15) ==="
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | head -16
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
|
||||
REMOTE_SCRIPT
|
||||
|
||||
else
|
||||
# Local execution (for running on NODE1 directly)
|
||||
echo "========================================"
|
||||
echo " NODE1 Status Check (Local)"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
echo "=== Health Endpoints ==="
|
||||
failed=0
|
||||
|
||||
for check in "Router:9102:/health" "Gateway:9300:/health" "Memory:8000:/health" \
|
||||
"RAG:9500:/health" "Swapper:8890:/health" "Qdrant:6333:/healthz" \
|
||||
"Vision:8001:/health" "Parser:8101:/health" "Prometheus:9090:/-/healthy" \
|
||||
"Grafana:3030:/api/health"; do
|
||||
name="${check%%:*}"
|
||||
rest="${check#*:}"
|
||||
port="${rest%%:*}"
|
||||
path="${rest#*:}"
|
||||
url="http://127.0.0.1:${port}${path}"
|
||||
|
||||
code=$(curl -sS -m 5 -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$code" = "200" ]; then
|
||||
echo -e "${GREEN}✅${NC} $name (${port}): ${GREEN}$code${NC}"
|
||||
else
|
||||
echo -e "${RED}❌${NC} $name (${port}): ${RED}$code${NC}"
|
||||
((failed++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== DNS Resolution ==="
|
||||
if docker exec dagi-memory-service-node1 python3 -c "import socket; socket.gethostbyname('dagi-qdrant-node1')" 2>/dev/null; then
|
||||
echo -e "${GREEN}✅${NC} DNS: dagi-qdrant-node1 resolves"
|
||||
else
|
||||
echo -e "${RED}❌${NC} DNS: dagi-qdrant-node1 does NOT resolve"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== System Resources ==="
|
||||
echo "Hostname: $(hostname)"
|
||||
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
|
||||
echo "Memory: $(free -h | awk '/^Mem:/ {print $3 "/" $2}')"
|
||||
echo "Disk: $(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 " used)"}')"
|
||||
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
echo ""
|
||||
echo "=== Docker Containers ==="
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== SECURITY CHECKS ==="
|
||||
sec_ok=0
|
||||
sec_fail=0
|
||||
|
||||
# Check 1: Internal ports blocked by iptables
|
||||
echo -n "Internal ports blocked: "
|
||||
ipt_blocked=$(iptables -L DOCKER-USER -n 2>/dev/null | grep -cE "DROP.*dpt:(9300|9102|6333|9090|3030|8890|8000|9500|8001|8101)" || echo "0")
|
||||
if [ "$ipt_blocked" -ge 5 ]; then
|
||||
echo -e "${GREEN}OK${NC} (iptables: $ipt_blocked rules)"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} (iptables: $ipt_blocked rules)"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
# Check 2: HSTS header present
|
||||
echo -n "HSTS header: "
|
||||
hsts=$(curl -sS -k -I https://127.0.0.1/ 2>/dev/null | grep -i "strict-transport-security" | wc -l)
|
||||
if [ "$hsts" -gt 0 ]; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
# Check 3: WAF blocks .env
|
||||
echo -n "WAF blocks .env: "
|
||||
waf_code=$(curl -sS -k -o /dev/null -w "%{http_code}" --max-time 3 https://127.0.0.1/.env 2>&1 | tail -c 3)
|
||||
if [ "$waf_code" = "000" ] || [ "$waf_code" = "444" ] || [ "$waf_code" = "403" ] || [ -z "$waf_code" ]; then
|
||||
echo -e "${GREEN}OK${NC} (blocked)"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} ($waf_code)"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
# Check 4: iptables DOCKER-USER rules present
|
||||
echo -n "iptables DOCKER-USER: "
|
||||
ipt_rules=$(iptables -L DOCKER-USER -n 2>/dev/null | grep -c "DROP.*dpt:" || echo "0")
|
||||
if [ "$ipt_rules" -ge 5 ]; then
|
||||
echo -e "${GREEN}OK${NC} ($ipt_rules rules)"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} ($ipt_rules rules)"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
# Check 5: Nginx running
|
||||
echo -n "Nginx status: "
|
||||
if systemctl is-active nginx >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
# Check 6: HTTP→HTTPS redirect
|
||||
echo -n "HTTP→HTTPS redirect: "
|
||||
http_code=$(curl -sS -o /dev/null -w "%{http_code}" http://127.0.0.1/ 2>/dev/null || echo "000")
|
||||
if [ "$http_code" = "301" ]; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
((sec_ok++)) || true
|
||||
else
|
||||
echo -e "${YELLOW}WARN${NC} ($http_code)"
|
||||
((sec_fail++)) || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Security: $sec_ok passed, $sec_fail failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
fi
|
||||
Reference in New Issue
Block a user