Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Remove abandoned temporary upload objects after 30 minutes.
|
||||
*/5 * * * * root find /var/www/contracts-upload/ -type f -mmin +30 -delete
|
||||
@@ -0,0 +1,16 @@
|
||||
# Temporary HTTP-only vhost for the initial Let's Encrypt HTTP-01 challenge.
|
||||
# Install as /etc/nginx/sites-enabled/contracts-upload.conf, issue the
|
||||
# certificate, then replace it with nginx-contracts-upload.conf.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name contracts-upload.sinefo.ru;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Dedicated WebDAV upload buffer for Contracts on VM213.
|
||||
# Install as /etc/nginx/sites-enabled/contracts-upload.conf.
|
||||
|
||||
# sites-enabled is included from Nginx's http context, where map is valid.
|
||||
map $http_origin $contracts_upload_cors_origin {
|
||||
default "";
|
||||
~^https://contractor\.pythonk8s\.dev\.nubes\.ru$ $http_origin;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name contracts-upload.sinefo.ru;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name contracts-upload.sinefo.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/contracts-upload.sinefo.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/contracts-upload.sinefo.ru/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# Match the app's 50 MiB per-file limit; uploads are raw PUT bodies.
|
||||
client_max_body_size 50m;
|
||||
|
||||
location /contracts-upload/ {
|
||||
alias /var/www/contracts-upload/;
|
||||
dav_methods PUT DELETE;
|
||||
create_full_put_path on;
|
||||
client_max_body_size 50m;
|
||||
add_header Access-Control-Allow-Origin $contracts_upload_cors_origin always;
|
||||
add_header Access-Control-Allow-Methods "PUT, GET, OPTIONS, DELETE" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||
add_header Access-Control-Max-Age 86400 always;
|
||||
if ($request_method = OPTIONS) { return 204; }
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -92,22 +92,6 @@ server {
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
# ── VM-буфер загрузки contracts (паттерн drhider) ──────────────
|
||||
# Браузер кладёт файл сюда PUT (мимо шлюза кластера ~64КБ), бэк тянет сам.
|
||||
# Файлы: /var/www/contracts-upload/ (нужно создать, chown www-data).
|
||||
# CORS: origin фронтенда = contractor.pythonk8s.dev.nubes.ru (ingress, подтверждено).
|
||||
location /contracts-upload/ {
|
||||
alias /var/www/contracts-upload/;
|
||||
dav_methods PUT DELETE;
|
||||
create_full_put_path on;
|
||||
client_max_body_size 1024m;
|
||||
add_header Access-Control-Allow-Origin https://contractor.pythonk8s.dev.nubes.ru always;
|
||||
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always;
|
||||
add_header Access-Control-Allow-Headers 'Content-Type' always;
|
||||
add_header Access-Control-Max-Age 86400 always;
|
||||
if ($request_method = OPTIONS) { return 204; }
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/contracts.kube5s.ru/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/contracts.kube5s.ru/privkey.pem; # managed by Certbot
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Инцидент: ошибки загрузки файлов через VM-буфер — 2026-10-30
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
Контрактор отправлял выбранные файлы на `https://contracts.kube5s.ru/contracts-upload/`.
|
||||
На момент проверки 2026-10-30 имя `contracts.kube5s.ru` возвращало `NXDOMAIN` у
|
||||
публичных DNS-резолверов `1.1.1.1` и `8.8.8.8`. Поэтому браузер не мог разрешить
|
||||
адрес назначения и показывал общую ошибку «Сетевая ошибка при отправке файла в
|
||||
буфер». Запрос не доходил до Nginx и WebDAV.
|
||||
|
||||
Форматы и содержимое выбранных документов не были причиной этого сбоя. Реальные
|
||||
файлы из пользовательской папки не отправлялись; проверки upload-инфраструктуры
|
||||
ограничились DNS, чтением конфигураций и безопасными HTTP `OPTIONS`-запросами.
|
||||
|
||||
## Подтверждённые факты
|
||||
|
||||
### Настройка Контрактора
|
||||
|
||||
- Развёрнутая страница Контрактора показывала версию `2.0.23`.
|
||||
- Загруженный браузером [`site/static/app.js`](../site/static/app.js) содержит
|
||||
`VM_UPLOAD_URL = 'https://contracts.kube5s.ru/contracts-upload/'`.
|
||||
- [`site/config.py`](../site/config.py) задаёт тот же домен для `VM_UPLOAD_URL`
|
||||
и `VM_UPLOAD_PREFIX`. Backend регистрирует `upload_refs` с этим
|
||||
`VM_UPLOAD_PREFIX` через [`site/routes/__init__.py`](../site/routes/__init__.py).
|
||||
- [`deploy/nginx-contracts.conf`](../deploy/nginx-contracts.conf) содержит
|
||||
WebDAV location `/contracts-upload/` с каталогом
|
||||
`/var/www/contracts-upload/` и CORS origin
|
||||
`https://contractor.pythonk8s.dev.nubes.ru`. Этот location находится в
|
||||
virtual host `contracts.kube5s.ru`.
|
||||
|
||||
### DNS и доступные буферы
|
||||
|
||||
- Запросы к `1.1.1.1` и `8.8.8.8` для `contracts.kube5s.ru A` вернули
|
||||
`NXDOMAIN`.
|
||||
- После первоначальной диагностики владелец DNS добавил
|
||||
`contracts-upload.sinefo.ru A -> 5.172.178.213`. Повторная проверка обоих
|
||||
публичных резолверов подтвердила эту запись.
|
||||
- При первой проверке до настройки Nginx новый hostname отдавал чужой TLS-
|
||||
сертификат. После установки HTTP challenge vhost сертификат Let's Encrypt
|
||||
выпущен для `contracts-upload.sinefo.ru`; срок действия до 2027-01-28.
|
||||
- Nginx virtual host для нового hostname теперь установлен на VM213, а
|
||||
`nginx -t` и reload прошли успешно.
|
||||
- `upload.sinefo.ru` разрешался в `5.172.178.213`, но
|
||||
`https://upload.sinefo.ru/contracts-upload/...` отвечал `404`.
|
||||
- На `upload.sinefo.ru/drhider-upload/` WebDAV-маршрут существует, но
|
||||
`Access-Control-Allow-Origin` возвращается только для DrHider origin
|
||||
`https://drhider.pythonk8s.dev.nubes.ru`, не для Контрактора.
|
||||
- На `obfuscator-upload.sinefo.ru/obfuscator-upload/` маршрут существует, но
|
||||
`Access-Control-Allow-Origin` возвращается только для Obfuscator origin
|
||||
`https://obfuscator.pythonk8s.services.ngcloud.ru`, не для Контрактора.
|
||||
- Запросы `OPTIONS` для origin Контрактора к обоим существующим маршрутам
|
||||
возвращали `204`, но без `Access-Control-Allow-Origin`. Такой ответ не
|
||||
разрешает браузерный запрос; кроме того, путь `/contracts-upload/` на этих
|
||||
хостах не настроен.
|
||||
- На VM213 (`5.172.178.213`) при проверке существовали каталог
|
||||
`/var/www/contracts-upload/` и старый Nginx location `/contracts-upload/`
|
||||
внутри virtual host `contracts.kube5s.ru`. Наличие каталога само по себе не
|
||||
делает его доступным по другому hostname.
|
||||
|
||||
## Почему прямое исправление URL недостаточно
|
||||
|
||||
Замена только домена в Контракторе не решит проблему:
|
||||
|
||||
1. `/contracts-upload/` на `upload.sinefo.ru` сейчас не настроен и отвечает
|
||||
`404`.
|
||||
2. Существующий `/drhider-upload/` предназначен для DrHider и не разрешает
|
||||
origin Контрактора в CORS.
|
||||
3. Буфер Obfuscator имеет отдельные hostname и путь, отдельную конфигурацию и
|
||||
allowlist origin. Он не является готовым Contracts endpoint.
|
||||
4. URL на фронтенде и доверенный префикс backend должны совпадать. Иначе браузер
|
||||
может загрузить объект, но `upload_refs` отклонит URL при SSRF-проверке либо
|
||||
backend будет пытаться забрать объект с другого адреса.
|
||||
5. Backend `VM_UPLOAD_PREFIX` может быть переопределён переменной окружения и
|
||||
должен совпадать с URL буфера. URL браузерной загрузки задаётся константой
|
||||
`VM_UPLOAD_URL` в `site/static/app.js`; переменная окружения с таким именем
|
||||
сама по себе статический JavaScript не меняет.
|
||||
|
||||
## Отличие от Obfuscator
|
||||
|
||||
Актуальный Obfuscator использует согласованную отдельную конфигурацию:
|
||||
|
||||
- hostname `obfuscator-upload.sinefo.ru`;
|
||||
- путь `/obfuscator-upload/`;
|
||||
- отдельный каталог `/var/www/obfuscator-upload/`;
|
||||
- CORS allowlist для `https://obfuscator.pythonk8s.services.ngcloud.ru`;
|
||||
- один и тот же URL-префикс на фронтенде и backend.
|
||||
|
||||
Это описано в [`OBFUSCATOR/README.md`](../../../drhider/OBFUSCATOR/README.md) и
|
||||
настроено в
|
||||
[`obfuscator-upload.conf`](../../../drhider/OBFUSCATOR/deploy/vm213/nginx/obfuscator-upload.conf).
|
||||
Контрактор должен получить аналогично согласованный endpoint, а не использовать
|
||||
Obfuscator/DrHider путь без изменения и проверки его конфигурации.
|
||||
|
||||
## План исправления
|
||||
|
||||
Первоначальный план обновлён после предоставления DNS-записи
|
||||
`contracts-upload.sinefo.ru`. Код и source Nginx изменены локально; VM213 и
|
||||
развёрнутое приложение ещё не обновлялись.
|
||||
|
||||
1. **Hostname и DNS — выполнено.** Выбран `contracts-upload.sinefo.ru`;
|
||||
публичные DNS `1.1.1.1` и `8.8.8.8` возвращают `5.172.178.213`.
|
||||
2. **Настроить VM213 — выполнено.** Проверен отдельный каталог
|
||||
`/var/www/contracts-upload/` и права пользователя Nginx. Для первоначального
|
||||
выпуска сертификата установить
|
||||
[`deploy/nginx-contracts-upload-acme.conf`](../deploy/nginx-contracts-upload-acme.conf)
|
||||
как `/etc/nginx/sites-enabled/contracts-upload.conf`, выполнить `nginx -t`
|
||||
и только при успехе reload. Первая попытка Certbot завершилась временным
|
||||
CAA DNS timeout; повторная попытка выпустила сертификат. После этого
|
||||
временный конфиг заменён полным
|
||||
[`deploy/nginx-contracts-upload.conf`](../deploy/nginx-contracts-upload.conf),
|
||||
снова выполнить `nginx -t` и reload. Полный vhost задаёт отдельный WebDAV
|
||||
путь, `PUT`/`GET`/`DELETE`, `OPTIONS`, ограничение тела 50 MiB и CORS allowlist
|
||||
через `map` для точного origin `https://contractor.pythonk8s.dev.nubes.ru`.
|
||||
Предел `50m` соответствует backend-лимиту 50 MiB; не копировать 1024 MiB из
|
||||
Obfuscator, поскольку Contracts не принимает файлы такого размера. Установить
|
||||
[`deploy/contracts-upload-cleanup.cron`](../deploy/contracts-upload-cleanup.cron)
|
||||
как `/etc/cron.d/contracts-upload-cleanup`, чтобы удалять оставшиеся объекты
|
||||
старше 30 минут. Cron установлен в `/etc/cron.d/contracts-upload-cleanup`.
|
||||
Не менять маршруты DrHider или Obfuscator и не объединять их каталоги с Contracts.
|
||||
3. **Код приложения — выполнено локально, deploy ожидает выполнения.**
|
||||
Frontend `VM_UPLOAD_URL` и backend `VM_UPLOAD_PREFIX` default приведены к
|
||||
`https://contracts-upload.sinefo.ru/contracts-upload/`; обновлены
|
||||
frontend/backend defaults, пример конфига и default URL нагрузочного теста.
|
||||
Версия приложения поднята до `2.0.24`. Проверить переменные окружения
|
||||
развёртывания: `VM_UPLOAD_PREFIX` не должен переопределять новый endpoint
|
||||
старым значением.
|
||||
4. **Синхронизировать конфигурацию VM — выполнено.** Источник Nginx вынесен в
|
||||
отдельные bootstrap- и TLS-конфиги; старый upload location удалён из
|
||||
[`deploy/nginx-contracts.conf`](../deploy/nginx-contracts.conf). Конфигурация
|
||||
TLS-vhost и cron установлена на VM213; Nginx config test/reload успешны.
|
||||
Деплой приложения Контрактора — отдельная операция, ещё не выполнена.
|
||||
5. **Проверить endpoint до обработки документов — выполнено.**
|
||||
- DNS разрешается в `5.172.178.213`; TLS сертификат валиден.
|
||||
- `OPTIONS` с origin Контрактора отвечает `204` и содержит точный
|
||||
`Access-Control-Allow-Origin`, разрешённые методы и заголовки. С
|
||||
посторонним origin `Access-Control-Allow-Origin` не отправляется.
|
||||
- Синтетический объект прошёл `PUT` (`201`) → `GET` (байты совпали) →
|
||||
`DELETE` (`204`). Объект удалён.
|
||||
6. **Проверить путь через приложение безопасно.** В тестовой сессии/батче
|
||||
загрузить синтетический небольшой файл, убедиться в успешном
|
||||
`POST /api/upload_refs` и очистить созданную тестовую запись/объект. Не
|
||||
использовать пользовательские документы для проверки, пока синтетический
|
||||
тест не пройден.
|
||||
7. **Только после успешной проверки** проверить обычную загрузку и последующие
|
||||
этапы обработки выбранных документов.
|
||||
|
||||
## Статус
|
||||
|
||||
- Причина исходной сетевой ошибки подтверждена: используемый hostname
|
||||
`contracts.kube5s.ru` вернул `NXDOMAIN`.
|
||||
- DNS, TLS, Nginx vhost, CORS и прямой синтетический WebDAV цикл проверены
|
||||
успешно.
|
||||
- Причина установлена до этапов HTTP/WebDAV, CORS и обработки документов.
|
||||
- URL/defaults, версия приложения и source Nginx обновлены локально.
|
||||
- Конфигурация буфера и cron уже установлены на VM213.
|
||||
- Изменения приложения ещё не запушены и не redeployed; end-to-end загрузка
|
||||
через UI Контрактора и обработка документов ещё не проверены.
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
"""Конфигурация приложения — все настройки в одном месте."""
|
||||
import os
|
||||
|
||||
VERSION = "2.0.23"
|
||||
VERSION = "2.0.24"
|
||||
|
||||
LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
|
||||
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
||||
@@ -14,9 +14,9 @@ CONVERT_SERVICE_URL = os.getenv("CONVERT_SERVICE_URL", "http://containerk8s.df36
|
||||
# ── VM-буфер загрузки (паттерн drhider) ──────────────────────────────
|
||||
# Браузер кладёт файл на ВМ через WebDAV (мимо шлюза кластера ~64КБ),
|
||||
# бэк сам тянет его исходящим GET (egress без лимита).
|
||||
VM_UPLOAD_URL = os.getenv("VM_UPLOAD_URL", "https://contracts.kube5s.ru/contracts-upload/")
|
||||
VM_UPLOAD_URL = os.getenv("VM_UPLOAD_URL", "https://contracts-upload.sinefo.ru/contracts-upload/")
|
||||
# SSRF-защита: тянуть можно ТОЛЬКО с этого префикса.
|
||||
VM_UPLOAD_PREFIX = os.getenv("VM_UPLOAD_PREFIX", "https://contracts.kube5s.ru/contracts-upload/")
|
||||
VM_UPLOAD_PREFIX = os.getenv("VM_UPLOAD_PREFIX", "https://contracts-upload.sinefo.ru/contracts-upload/")
|
||||
# Лимит на один файл (совпадает с фронтом).
|
||||
VM_UPLOAD_MAX_BYTES = int(os.getenv("VM_UPLOAD_MAX_BYTES", str(50 * 1024 * 1024)))
|
||||
# Ретраи pull с ВМ (разовые DNS/сетевые сбои не роняют загрузку).
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
var VM_API = '';
|
||||
// ВМ-буфер загрузки (паттерн drhider): браузер кладёт файл сюда (WebDAV, мимо шлюза),
|
||||
// бэк сам тянет его по /api/upload_refs. Origin должен быть в CORS на nginx ВМ.
|
||||
var VM_UPLOAD_URL = 'https://contracts.kube5s.ru/contracts-upload/';
|
||||
var VM_UPLOAD_URL = 'https://contracts-upload.sinefo.ru/contracts-upload/';
|
||||
// SITE_URL удалён (Фаза 4) — не использовался
|
||||
|
||||
// state.files — теперь в state.js (Фаза 0: state + render)
|
||||
|
||||
@@ -26,7 +26,7 @@ import httpx
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="HTTP-стресс contracts-flask")
|
||||
ap.add_argument("--url", default="http://127.0.0.1:5000")
|
||||
ap.add_argument("--vm-url", default="https://contracts.kube5s.ru/contracts-upload/", help="ВМ WebDAV-буфер (PUT файла)")
|
||||
ap.add_argument("--vm-url", default="https://contracts-upload.sinefo.ru/contracts-upload/", help="ВМ WebDAV-буфер (PUT файла)")
|
||||
ap.add_argument("--n", type=int, default=1000, help="число запросов")
|
||||
ap.add_argument("--threads", type=int, default=32, help="параллельных потоков")
|
||||
ap.add_argument("--size", type=int, default=100000, help="размер файла в байтах (если не задан --file)")
|
||||
|
||||
@@ -154,3 +154,20 @@ store_result(res.session, result_zip) # если нужно отдать
|
||||
|
||||
- Слой 3 — логика приложения (обработка файлов из сессии, SSE-прогресс) у каждого своя.
|
||||
- CORS на ВМ-буфере — если домен приложения другой, правится nginx на ВМ, а не код модуля.
|
||||
|
||||
## Инцидент Contracts VM-буфера (2026-10-30)
|
||||
|
||||
Контрактор использовал `contracts.kube5s.ru/contracts-upload/`, но DNS для
|
||||
`contracts.kube5s.ru` возвращал `NXDOMAIN`. DNS-запись для отдельного hostname
|
||||
`contracts-upload.sinefo.ru` теперь добавлена и проверена на публичных
|
||||
резолверах. Переключение только на существующий
|
||||
DrHider или Obfuscator endpoint недостаточно: у них другие пути и CORS allowlist,
|
||||
и тестовый путь `/contracts-upload/` на этих хостах отвечал `404`. Новый
|
||||
hostname теперь настроен на VM213 с отдельным TLS-сертификатом, Nginx vhost и
|
||||
cron-очисткой временных файлов. Проверены HTTPS, CORS для Контрактора и прямой
|
||||
синтетический цикл PUT/GET/DELETE.
|
||||
|
||||
Код приложения и source Nginx настроены локально; Nginx-конфиг буфера уже
|
||||
установлен на VM213, но само приложение пока не redeployed. Причина, результаты
|
||||
DNS/CORS/TLS-проверок и дальнейший план описаны в
|
||||
[отчёте об инциденте](../docs/upload-buffer-incident-2026-10-30.md).
|
||||
|
||||
@@ -8,4 +8,4 @@ PULL_RETRIES = 3
|
||||
PULL_RETRY_DELAY = 2 # секунды между попытками
|
||||
|
||||
# Доверенный префикс ВМ-буфера — валидация URL при pull (защита от SSRF)
|
||||
VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"
|
||||
VM_UPLOAD_PREFIX = "https://contracts-upload.sinefo.ru/contracts-upload/"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"vmUploadUrl": "https://contracts.kube5s.ru/drhider-upload/",
|
||||
"vmUploadUrl": "https://contracts-upload.sinefo.ru/contracts-upload/",
|
||||
"allowedExt": [".pdf", ".doc", ".docx", ".txt", ".md"],
|
||||
"maxFileBytes": 52428800,
|
||||
"maxSessionBytes": 524288000,
|
||||
"apiPrefix": "/api",
|
||||
"vmUploadPrefix": "https://contracts.kube5s.ru/drhider-upload/",
|
||||
"vmUploadPrefix": "https://contracts-upload.sinefo.ru/contracts-upload/",
|
||||
"pullRetries": 3,
|
||||
"pullRetryDelay": 2,
|
||||
"pullTimeout": 120,
|
||||
|
||||
Reference in New Issue
Block a user