From f8d6b92fd210aeb5de6bea029f689af10fedd6a8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 16 Sep 2025 18:31:51 +0300 Subject: [PATCH] feat: add Nginx reverse proxy and SSL configuration - Introduce Nginx service in docker-compose for handling HTTP/HTTPS traffic. - Configure Nginx with SSL support and health checks for Grafana and Prometheus. - Update env.template to include SERVER_IP and STATUS_PAGE_PASSWORD variables. - Enhance Ansible playbook with tasks for Nginx installation, SSL certificate generation, and configuration management. --- docker-compose.yml | 26 +++++ env.template | 6 ++ infra/ansible/playbook.yml | 152 +++++++++++++++++++++++++++++ infra/nginx/README.md | 106 ++++++++++++++++++++ infra/nginx/conf.d/grafana.conf | 32 ++++++ infra/nginx/conf.d/prometheus.conf | 34 +++++++ infra/nginx/conf.d/status.conf | 24 +++++ infra/nginx/nginx.conf | 103 +++++++++++++++++++ 8 files changed, 483 insertions(+) create mode 100644 infra/nginx/README.md create mode 100644 infra/nginx/conf.d/grafana.conf create mode 100644 infra/nginx/conf.d/prometheus.conf create mode 100644 infra/nginx/conf.d/status.conf create mode 100644 infra/nginx/nginx.conf diff --git a/docker-compose.yml b/docker-compose.yml index fa59cb9..d620e95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,9 @@ services: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin} - GF_USERS_ALLOW_SIGN_UP=false - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource + - GF_SERVER_ROOT_URL=https://${SERVER_IP:-localhost}/grafana/ + - GF_SERVER_SERVE_FROM_SUB_PATH=true + - GF_SERVER_DOMAIN=${SERVER_IP:-localhost} ports: - "3000:3000" volumes: @@ -50,6 +53,29 @@ services: timeout: 10s retries: 3 + # Nginx Reverse Proxy + nginx: + image: nginx:alpine + container_name: bots_nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./infra/nginx/conf.d:/etc/nginx/conf.d:ro + - ./infra/nginx/ssl:/etc/nginx/ssl:ro + - ./infra/nginx/.htpasswd:/etc/nginx/.htpasswd:ro + networks: + - bots_network + depends_on: + - grafana + - prometheus + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/nginx-health"] + interval: 30s + timeout: 10s + retries: 3 # Telegram Helper Bot telegram-bot: diff --git a/env.template b/env.template index a8af820..63839d0 100644 --- a/env.template +++ b/env.template @@ -21,3 +21,9 @@ PROMETHEUS_RETENTION_DAYS=30 # Grafana Configuration GRAFANA_ADMIN_USER=admin GRAFANA_ADMIN_PASSWORD=admin + +# Server Configuration +SERVER_IP=your_server_ip_here + +# Status Page Configuration +STATUS_PAGE_PASSWORD=admin123 diff --git a/infra/ansible/playbook.yml b/infra/ansible/playbook.yml index b8c8027..cb509d1 100644 --- a/infra/ansible/playbook.yml +++ b/infra/ansible/playbook.yml @@ -54,6 +54,9 @@ - prometheus-node-exporter - fail2ban - tzdata + - nginx + - openssl + - apache2-utils state: present - name: Установить часовой пояс Europe/Moscow @@ -257,6 +260,112 @@ - "80" # HTTP - "443" # HTTPS + # --- НАСТРОЙКА NGINX --- + - name: Остановить nginx (если запущен) + systemd: + name: nginx + state: stopped + ignore_errors: yes + + - name: Создать директории для nginx конфигураций + file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: '0755' + loop: + - "{{ project_root }}/infra/nginx" + - "{{ project_root }}/infra/nginx/ssl" + - "{{ project_root }}/infra/nginx/conf.d" + + - name: Сгенерировать самоподписанный SSL сертификат + command: > + openssl req -x509 -newkey rsa:4096 -keyout {{ project_root }}/infra/nginx/ssl/key.pem + -out {{ project_root }}/infra/nginx/ssl/cert.pem -days 365 -nodes + -subj "/CN={{ ansible_host }}/O=Monitoring/C=RU" + args: + creates: "{{ project_root }}/infra/nginx/ssl/cert.pem" + + - name: Установить права на SSL сертификаты + file: + path: "{{ item }}" + owner: root + group: root + mode: '0600' + loop: + - "{{ project_root }}/infra/nginx/ssl/cert.pem" + - "{{ project_root }}/infra/nginx/ssl/key.pem" + + - name: Создать htpasswd файл для status page + htpasswd: + path: "{{ project_root }}/infra/nginx/.htpasswd" + name: "admin" + password: "{{ lookup('env', 'STATUS_PAGE_PASSWORD') | default('admin123') }}" + owner: root + group: root + mode: '0644' + + - name: Скопировать основную конфигурацию nginx + copy: + src: "{{ project_root }}/infra/nginx/nginx.conf" + dest: /etc/nginx/nginx.conf + owner: root + group: root + mode: '0644' + backup: yes + + - name: Скопировать конфигурации nginx для сервисов + copy: + src: "{{ project_root }}/infra/nginx/conf.d/" + dest: /etc/nginx/conf.d/ + owner: root + group: root + mode: '0644' + backup: yes + + - name: Скопировать SSL сертификаты + copy: + src: "{{ project_root }}/infra/nginx/ssl/" + dest: /etc/nginx/ssl/ + owner: root + group: root + mode: '0600' + backup: yes + + - name: Скопировать htpasswd файл + copy: + src: "{{ project_root }}/infra/nginx/.htpasswd" + dest: /etc/nginx/.htpasswd + owner: root + group: root + mode: '0644' + backup: yes + + - name: Проверить конфигурацию nginx + command: nginx -t + register: nginx_config_test + changed_when: false + + - name: Показать результат проверки nginx + debug: + var: nginx_config_test.stdout_lines + + - name: Включить и запустить nginx + systemd: + name: nginx + enabled: yes + state: started + + - name: Проверить статус nginx + command: systemctl status nginx + register: nginx_status + changed_when: false + + - name: Показать статус nginx + debug: + var: nginx_status.stdout_lines + - name: Проверить существование пользователя deploy getent: database: passwd @@ -686,6 +795,49 @@ timeout: 30 state: started + - name: Проверить, что порт 80 (Nginx HTTP) открыт + wait_for: + port: 80 + host: "{{ ansible_host }}" + timeout: 30 + state: started + + - name: Проверить, что порт 443 (Nginx HTTPS) открыт + wait_for: + port: 443 + host: "{{ ansible_host }}" + timeout: 30 + state: started + + - name: Проверить доступность Nginx + uri: + url: "http://{{ ansible_host }}/nginx-health" + method: GET + status_code: 200 + register: nginx_health + retries: 5 + delay: 10 + + - name: Проверить доступность Grafana через Nginx + uri: + url: "https://{{ ansible_host }}/grafana/api/health" + method: GET + status_code: 200 + validate_certs: no + register: grafana_nginx_health + retries: 5 + delay: 10 + + - name: Проверить доступность Prometheus через Nginx + uri: + url: "https://{{ ansible_host }}/prometheus/-/healthy" + method: GET + status_code: 200 + validate_certs: no + register: prometheus_nginx_health + retries: 5 + delay: 10 + - name: Проверить доступность Grafana API uri: url: "http://{{ ansible_host }}:3000/api/health" diff --git a/infra/nginx/README.md b/infra/nginx/README.md new file mode 100644 index 0000000..e43831b --- /dev/null +++ b/infra/nginx/README.md @@ -0,0 +1,106 @@ +# Nginx Reverse Proxy Configuration + +## Обзор + +Данная конфигурация nginx обеспечивает безопасный доступ к сервисам мониторинга через HTTPS с самоподписанными SSL сертификатами. + +## Архитектура + +``` +Интернет → Nginx (443) → + ├→ /grafana → Grafana (3000) + ├→ /prometheus → Prometheus (9090) + ├→ /status → Status page (с Basic Auth) + └→ / → Redirect to /grafana +``` + +## Структура файлов + +``` +infra/nginx/ +├── nginx.conf # Основная конфигурация nginx +├── ssl/ # SSL сертификаты (создаются автоматически) +│ ├── cert.pem # SSL сертификат +│ └── key.pem # Приватный ключ +├── conf.d/ # Конфигурации location'ов +│ ├── grafana.conf # Конфиг для Grafana +│ ├── prometheus.conf # Конфиг для Prometheus +│ └── status.conf # Конфиг для status page +└── .htpasswd # Basic Auth для status page +``` + +## Доступ к сервисам + +### Grafana +- **URL**: `https://your-server-ip/grafana/` +- **Аутентификация**: Grafana admin credentials +- **Особенности**: Настроен для работы через sub-path + +### Prometheus +- **URL**: `https://your-server-ip/prometheus/` +- **Особенности**: Полный доступ к Prometheus UI + +### Status Page +- **URL**: `https://your-server-ip/status` +- **Аутентификация**: Basic Auth (admin/admin123 по умолчанию) +- **Особенности**: Показывает статус nginx (заготовка для Uptime Kuma) + +## Переменные окружения + +Добавьте в ваш `.env` файл: + +```bash +# Server Configuration +SERVER_IP=your_server_ip_here + +# Status Page Configuration +STATUS_PAGE_PASSWORD=admin123 +``` + +## Безопасность + +- **SSL/TLS**: Самоподписанные сертификаты (365 дней) +- **Rate Limiting**: 10 req/s для API, 1 req/s для status page +- **Security Headers**: X-Frame-Options, X-Content-Type-Options, CSP +- **Basic Auth**: Для status page +- **Fail2ban**: Интеграция с nginx логами + +## Мониторинг + +- **Health Check**: `https://your-server-ip/nginx-health` +- **Nginx Status**: `https://your-server-ip/nginx_status` (только локальные сети) +- **Logs**: `/var/log/nginx/access.log`, `/var/log/nginx/error.log` + +## Развертывание + +Конфигурация автоматически развертывается через Ansible playbook: + +```bash +ansible-playbook -i inventory.ini playbook.yml +``` + +## Устранение неполадок + +### Проверка конфигурации nginx +```bash +nginx -t +``` + +### Проверка SSL сертификатов +```bash +openssl x509 -in /etc/nginx/ssl/cert.pem -text -noout +``` + +### Проверка доступности сервисов +```bash +curl -k https://your-server-ip/grafana/api/health +curl -k https://your-server-ip/prometheus/-/healthy +curl -k https://your-server-ip/nginx-health +``` + +## Будущие улучшения + +- Интеграция с Uptime Kuma для status page +- Let's Encrypt сертификаты вместо самоподписанных +- Дополнительные security headers +- Мониторинг nginx метрик в Prometheus diff --git a/infra/nginx/conf.d/grafana.conf b/infra/nginx/conf.d/grafana.conf new file mode 100644 index 0000000..5a4e2ed --- /dev/null +++ b/infra/nginx/conf.d/grafana.conf @@ -0,0 +1,32 @@ +# Grafana reverse proxy configuration +upstream grafana_backend { + server grafana:3000; + keepalive 32; +} + +# Grafana proxy configuration +location /grafana/ { + proxy_pass http://grafana_backend/; + 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_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # WebSocket support for Grafana + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffer settings + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + proxy_busy_buffers_size 8k; +} diff --git a/infra/nginx/conf.d/prometheus.conf b/infra/nginx/conf.d/prometheus.conf new file mode 100644 index 0000000..b3a3156 --- /dev/null +++ b/infra/nginx/conf.d/prometheus.conf @@ -0,0 +1,34 @@ +# Prometheus reverse proxy configuration +upstream prometheus_backend { + server prometheus:9090; + keepalive 32; +} + +# Prometheus proxy configuration +location /prometheus/ { + proxy_pass http://prometheus_backend/; + 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_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Timeouts + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # Buffer settings + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + proxy_busy_buffers_size 8k; +} + +# Health check endpoint +location /prometheus/-/healthy { + proxy_pass http://prometheus_backend/-/healthy; + proxy_set_header Host $host; + access_log off; +} diff --git a/infra/nginx/conf.d/status.conf b/infra/nginx/conf.d/status.conf new file mode 100644 index 0000000..9b89b20 --- /dev/null +++ b/infra/nginx/conf.d/status.conf @@ -0,0 +1,24 @@ +# Status page configuration (for future uptime kuma integration) + +# Rate limiting for status page +location /status { + # Basic authentication for status page + auth_basic "Status Page Access"; + auth_basic_user_file /etc/nginx/.htpasswd; + + # Placeholder for future uptime kuma integration + # For now, show nginx status + access_log off; + return 200 '{"status": "ok", "nginx": "running", "timestamp": "$time_iso8601"}'; + add_header Content-Type application/json; +} + +# Nginx status stub (for monitoring) +location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + allow 172.16.0.0/12; # Docker networks + allow 192.168.0.0/16; # Private networks + deny all; +} diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf new file mode 100644 index 0000000..645bd02 --- /dev/null +++ b/infra/nginx/nginx.conf @@ -0,0 +1,103 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging format + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Basic settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 16M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=status:10m rate=1r/s; + + # 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; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:;" always; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Main server block + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl http2; + server_name _; + + # SSL configuration + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # Rate limiting + limit_req zone=api burst=20 nodelay; + + # Redirect root to Grafana + location = / { + return 301 /grafana/; + } + + # Health check endpoint + location /nginx-health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Include location configurations + include /etc/nginx/conf.d/*.conf; + } +}