diff --git a/.gitea/workflows/deploy-nginx.yml b/.gitea/workflows/deploy-nginx.yml new file mode 100644 index 0000000..2026b81 --- /dev/null +++ b/.gitea/workflows/deploy-nginx.yml @@ -0,0 +1,80 @@ +name: Deploy Nginx + +# Единственный публичный вход (443/80) на VPS с данными — system nginx +# (НЕ Docker, чтобы просто использовать certbot и его автопродление, без +# возни с сертификатами внутри контейнера). Только ручной запуск: nginx +# на этом сервере меняется редко, а сертификат/.htpasswd — одноразовые +# secrets-на-сервере, трогать их каждым push'ом смысла нет (тот же принцип, +# что и генерация сертификата Postgres в deploy.yml — один раз, не на +# каждый прогон). +# +# Порядок первого запуска: +# 1. Запустить эту джобу (поднимет http-конфиг + certbot + сам nginx). +# 2. Один раз руками на сервере: +# sudo apt-get install -y certbot +# sudo certbot certonly --webroot -w /var/www/certbot \ +# -d grafana.netrunner-vpn.com -d vault.netrunner-vpn.com +# sudo htpasswd -c /etc/nginx/.htpasswd # спросит пароль +# (htpasswd — из пакета apache2-utils, если его нет: apt-get install -y apache2-utils) +# 3. Запустить эту джобу ещё раз — увидит, что сертификат появился, +# включит netrunner-ssl.conf и перезагрузит nginx. +# +# Обновление одного из паролей позже — та же команда htpasswd без -c +# (создать) добавит/перезапишет юзера, не трогая остальных; -D удаляет. +on: + workflow_dispatch: + +jobs: + deploy: + name: Deploy nginx config to Data VPS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Sync nginx configs to VPS + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ vars.DATA_VPS_IP }} + username: root + key: ${{ secrets.DATA_SSH_PRIVATE_KEY }} + source: "nginx/netrunner-http.conf,nginx/netrunner-ssl.conf" + target: "/root/netrunner-data" + strip_components: 0 + + - name: Install/enable nginx via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ vars.DATA_VPS_IP }} + username: root + key: ${{ secrets.DATA_SSH_PRIVATE_KEY }} + script: | + set -e + + if ! command -v nginx >/dev/null 2>&1; then + echo "📦 Устанавливаю nginx..." + apt-get update + apt-get install -y nginx + fi + + mkdir -p /var/www/certbot + rm -f /etc/nginx/sites-enabled/default + + cp /root/netrunner-data/nginx/netrunner-http.conf /etc/nginx/sites-available/netrunner-http.conf + cp /root/netrunner-data/nginx/netrunner-ssl.conf /etc/nginx/sites-available/netrunner-ssl.conf + ln -sf /etc/nginx/sites-available/netrunner-http.conf /etc/nginx/sites-enabled/netrunner-http.conf + + CERT=/etc/letsencrypt/live/grafana.netrunner-vpn.com/fullchain.pem + if [ -f "$CERT" ]; then + ln -sf /etc/nginx/sites-available/netrunner-ssl.conf /etc/nginx/sites-enabled/netrunner-ssl.conf + echo "🔒 Сертификат найден — HTTPS-конфиг включён." + else + rm -f /etc/nginx/sites-enabled/netrunner-ssl.conf + echo "⚠️ Сертификата ещё нет ($CERT) — HTTPS-конфиг НЕ включён." + echo " Выпустите его руками (см. заголовок deploy-nginx.yml) и перезапустите эту джобу." + fi + + nginx -t + systemctl enable --now nginx + systemctl reload nginx + + echo "✅ nginx synchronized." diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f363b88..e854f68 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -54,6 +54,12 @@ jobs: echo "🔑 Генерирую самоподписанный TLS-сертификат для Postgres (первый деплой)..." openssl req -new -x509 -days 3650 -nodes -subj "/CN=netrunner-db" \ -out postgres/certs/server.crt -keyout postgres/certs/server.key + # postgres:16-alpine работает под UID/GID 70 (не root) — файл, + # созданный openssl от root с правами 600, контейнеру не прочитать + # ("Permission denied" при старте). Владельца нужно менять именно + # на 70:70 (проверено: `docker run --rm postgres:16-alpine id postgres`), + # не 999 — это UID из Debian-образов, у alpine он другой. + chown 70:70 postgres/certs/server.key chmod 600 postgres/certs/server.key fi diff --git a/.github/workflows/deploy-nginx.yml b/.github/workflows/deploy-nginx.yml new file mode 100644 index 0000000..2026b81 --- /dev/null +++ b/.github/workflows/deploy-nginx.yml @@ -0,0 +1,80 @@ +name: Deploy Nginx + +# Единственный публичный вход (443/80) на VPS с данными — system nginx +# (НЕ Docker, чтобы просто использовать certbot и его автопродление, без +# возни с сертификатами внутри контейнера). Только ручной запуск: nginx +# на этом сервере меняется редко, а сертификат/.htpasswd — одноразовые +# secrets-на-сервере, трогать их каждым push'ом смысла нет (тот же принцип, +# что и генерация сертификата Postgres в deploy.yml — один раз, не на +# каждый прогон). +# +# Порядок первого запуска: +# 1. Запустить эту джобу (поднимет http-конфиг + certbot + сам nginx). +# 2. Один раз руками на сервере: +# sudo apt-get install -y certbot +# sudo certbot certonly --webroot -w /var/www/certbot \ +# -d grafana.netrunner-vpn.com -d vault.netrunner-vpn.com +# sudo htpasswd -c /etc/nginx/.htpasswd # спросит пароль +# (htpasswd — из пакета apache2-utils, если его нет: apt-get install -y apache2-utils) +# 3. Запустить эту джобу ещё раз — увидит, что сертификат появился, +# включит netrunner-ssl.conf и перезагрузит nginx. +# +# Обновление одного из паролей позже — та же команда htpasswd без -c +# (создать) добавит/перезапишет юзера, не трогая остальных; -D удаляет. +on: + workflow_dispatch: + +jobs: + deploy: + name: Deploy nginx config to Data VPS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Sync nginx configs to VPS + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ vars.DATA_VPS_IP }} + username: root + key: ${{ secrets.DATA_SSH_PRIVATE_KEY }} + source: "nginx/netrunner-http.conf,nginx/netrunner-ssl.conf" + target: "/root/netrunner-data" + strip_components: 0 + + - name: Install/enable nginx via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ vars.DATA_VPS_IP }} + username: root + key: ${{ secrets.DATA_SSH_PRIVATE_KEY }} + script: | + set -e + + if ! command -v nginx >/dev/null 2>&1; then + echo "📦 Устанавливаю nginx..." + apt-get update + apt-get install -y nginx + fi + + mkdir -p /var/www/certbot + rm -f /etc/nginx/sites-enabled/default + + cp /root/netrunner-data/nginx/netrunner-http.conf /etc/nginx/sites-available/netrunner-http.conf + cp /root/netrunner-data/nginx/netrunner-ssl.conf /etc/nginx/sites-available/netrunner-ssl.conf + ln -sf /etc/nginx/sites-available/netrunner-http.conf /etc/nginx/sites-enabled/netrunner-http.conf + + CERT=/etc/letsencrypt/live/grafana.netrunner-vpn.com/fullchain.pem + if [ -f "$CERT" ]; then + ln -sf /etc/nginx/sites-available/netrunner-ssl.conf /etc/nginx/sites-enabled/netrunner-ssl.conf + echo "🔒 Сертификат найден — HTTPS-конфиг включён." + else + rm -f /etc/nginx/sites-enabled/netrunner-ssl.conf + echo "⚠️ Сертификата ещё нет ($CERT) — HTTPS-конфиг НЕ включён." + echo " Выпустите его руками (см. заголовок deploy-nginx.yml) и перезапустите эту джобу." + fi + + nginx -t + systemctl enable --now nginx + systemctl reload nginx + + echo "✅ nginx synchronized." diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f363b88..e854f68 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,6 +54,12 @@ jobs: echo "🔑 Генерирую самоподписанный TLS-сертификат для Postgres (первый деплой)..." openssl req -new -x509 -days 3650 -nodes -subj "/CN=netrunner-db" \ -out postgres/certs/server.crt -keyout postgres/certs/server.key + # postgres:16-alpine работает под UID/GID 70 (не root) — файл, + # созданный openssl от root с правами 600, контейнеру не прочитать + # ("Permission denied" при старте). Владельца нужно менять именно + # на 70:70 (проверено: `docker run --rm postgres:16-alpine id postgres`), + # не 999 — это UID из Debian-образов, у alpine он другой. + chown 70:70 postgres/certs/server.key chmod 600 postgres/certs/server.key fi diff --git a/docker-compose.observability.yml b/docker-compose.observability.yml index d0f1df1..ef26920 100644 --- a/docker-compose.observability.yml +++ b/docker-compose.observability.yml @@ -15,8 +15,10 @@ # LOKI_ALLOWED_CIDR ниже, заглушка 0.0.0.0/0 — заменить по мере аренды # серверов, тот же паттерн что postgres/pg_hba.conf в соседнем стеке). # - Prometheus/Grafana UI НЕ публикуются напрямую (127.0.0.1-only) — доступ -# к Grafana только через Caddy с auth-гейтом (см. фазу 7 плана -# мониторинга), Prometheus UI вообще не нужен наружу. +# к Grafana только через system nginx на этом же VPS (см. ../nginx/, +# TLS + HTTP Basic Auth) — единственный публичный вход, отдельный от +# этого compose-стека (nginx не в Docker, деплоится отдельным workflow +# deploy-nginx.yml). Prometheus UI вообще не нужен наружу. # # Список нод для scrape (observability/targets/nodes.json) генерится и # заливается сюда отдельным шагом деплоя netrunner-proxy — см. @@ -92,25 +94,6 @@ services: networks: - netrunner_observability - # Единственный публичный вход в Grafana — гейтит доступ через существующую - # cookie-сессию netrunner-backend (Owner/Moderator), см. observability/Caddyfile. - # Порт Grafana выше (3030) остаётся 127.0.0.1-only — снаружи только через это. - caddy: - image: caddy:2-alpine - container_name: netrunner-observability-caddy - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./observability/Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - depends_on: - - grafana - networks: - - netrunner_observability - networks: netrunner_observability: driver: bridge @@ -119,5 +102,3 @@ volumes: prometheus_data: loki_data: grafana_data: - caddy_data: - caddy_config: diff --git a/nginx/netrunner-http.conf b/nginx/netrunner-http.conf new file mode 100644 index 0000000..6409a4d --- /dev/null +++ b/nginx/netrunner-http.conf @@ -0,0 +1,18 @@ +# Часть 1/2 — включается всегда, с самого первого деплоя (см. +# deploy-nginx.yml). Обслуживает ACME-challenge для certbot и редиректит +# всё остальное на HTTPS. netrunner-ssl.conf (сами сервисы) деплой +# включает отдельно, только когда сертификат уже существует — иначе nginx +# откажется стартовать (ssl_certificate на несуществующий файл). +server { + listen 80; + listen [::]:80; + server_name grafana.netrunner-vpn.com vault.netrunner-vpn.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} diff --git a/nginx/netrunner-ssl.conf b/nginx/netrunner-ssl.conf new file mode 100644 index 0000000..40cf65e --- /dev/null +++ b/nginx/netrunner-ssl.conf @@ -0,0 +1,91 @@ +# Часть 2/2 — деплой (deploy-nginx.yml) включает этот файл ТОЛЬКО когда на +# сервере уже есть сертификат /etc/letsencrypt/live/grafana.netrunner-vpn.com/ +# (иначе nginx -t упадёт: ssl_certificate на несуществующий файл). До первого +# `certbot certonly` этот файл лежит на диске, но не подключён — только +# netrunner-http.conf, обслуживающий ACME-challenge. +# +# Обе площадки закрыты HTTP Basic Auth (доп. пароль ДО того, как запрос +# вообще дойдёт до Grafana/Vaultwarden) — см. deploy-nginx.yml про +# .htpasswd. Для Vaultwarden basic auth намеренно НЕ накрывает /api, +# /identity, /notifications — мобильные и десктоп-клиенты Bitwarden, а +# также браузерное расширение, не умеют проходить Basic Auth на этих +# путях (только сама веб-морда на "/" людьми открывается руками), +# закрыть их означало бы сломать синхронизацию всем клиентам. + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name grafana.netrunner-vpn.com; + + # Сертификат лежит в папке ПЕРВОГО домена из -d при выпуске (grafana...), + # даже несмотря на то что SAN-сертификат покрывает оба домена — так у + # certbot всегда, это не опечатка. + ssl_certificate /etc/letsencrypt/live/grafana.netrunner-vpn.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/grafana.netrunner-vpn.com/privkey.pem; + + auth_basic "Restricted"; + auth_basic_user_file /etc/nginx/.htpasswd; + + location / { + proxy_pass http://127.0.0.1:3030; + 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; + # Grafana Live (websocket) — используется для live-обновления панелей. + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name vault.netrunner-vpn.com; + + ssl_certificate /etc/letsencrypt/live/grafana.netrunner-vpn.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/grafana.netrunner-vpn.com/privkey.pem; + + client_max_body_size 525M; # вложения Vaultwarden + + # Веб-морда ("/") — единственное место, которое реально открывают руками + # в браузере, поэтому единственное место с доп. паролем. + location / { + auth_basic "Restricted"; + auth_basic_user_file /etc/nginx/.htpasswd; + + proxy_pass http://127.0.0.1:8081; + 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; + } + + # API/логин/синхронизация — сюда ходят мобильные приложения, десктоп-клиент + # и браузерное расширение напрямую, без Basic Auth (клиенты Bitwarden его + # на этих путях не поддерживают — добавить сюда auth_basic значило бы + # сломать синхронизацию всем, кроме веб-морды). + location ~ ^/(api|identity)/ { + proxy_pass http://127.0.0.1:8081; + 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; + } + + # Websocket-уведомления о синхронизации между устройствами — тоже без + # Basic Auth (по той же причине) и с апгрейдом соединения. + location /notifications/hub { + proxy_pass http://127.0.0.1:8081; + 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_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/observability/Caddyfile b/observability/Caddyfile deleted file mode 100644 index 20962c9..0000000 --- a/observability/Caddyfile +++ /dev/null @@ -1,28 +0,0 @@ -# Гейтит доступ к Grafana через уже существующую cookie-сессию -# netrunner-backend (владелец/модератор) — Grafana сама наружу НЕ торчит -# (см. docker-compose.observability.yml, порт 3030 только на 127.0.0.1), -# только через этот Caddy. -# -# Как это работает: cookie сессии backend'а (nrxp_token) ставится с -# Domain=.netrunner-vpn.com (см. COOKIE_DOMAIN в netrunner-backend/.env) — -# значит браузер сам приложит её и к запросам на grafana.netrunner-vpn.com -# (тот же родительский домен, другой сабдомен — это same-site, не -# cross-site, для куки без атрибута Partitioned браузер это разрешает даже -# из iframe). forward_auth здесь пересылает эту cookie на реальный бэкенд -# (уже на другом VPS) и либо пускает дальше в Grafana (200), либо блокирует -# (401) — сам Caddy решения не принимает, только проксирует запрос-проверку. -# -# ЗАМЕНИТЬ ПЕРЕД ПЕРВЫМ ЗАПУСКОМ: -# - CHANGE_ME_GRAFANA_DOMAIN — сабдомен для Grafana (например -# grafana.netrunner-vpn.com), DNS A-запись на IP этого VPS. -# - CHANGE_ME_BACKEND_PUBLIC_URL — публичный URL бэкенда (например -# https://account.netrunner-vpn.com), КОГДА backend будет арендован и -# задеплоен — до этого auth-гейт нерабочий (пускать некому проверять). -CHANGE_ME_GRAFANA_DOMAIN { - forward_auth CHANGE_ME_BACKEND_PUBLIC_URL { - uri /api/v1/admin/observability/check - copy_headers Cookie - } - - reverse_proxy grafana:3000 -}