Files
wrapped/README.md
T
inecs f35144ed28
devops-tools/wrapped/wrapped-deploy/pipeline/head This commit looks good
Исправлен VERSION до 0.1.3; убран авто-bump из make push.
Упрощён экран «Токен выдан»: чеклист/trust убраны, Share и Download QR — иконки под QR.
2026-07-29 14:15:07 +03:00

26 KiB
Raw Blame History

Wrapped

Анонимный сервис одноразовой передачи зашифрованных данных (текст, изображения, файлы). Шифрование выполняется в браузере (Web Crypto), на сервере хранится только ciphertext. После первого успешного открытия пакет удаляется из хранилища.

Возможности

  • zero-knowledge шифрование на клиенте (AES-GCM)
  • одноразовая расшифровка — ciphertext удаляется с сервера после открытия
  • опциональный пароль: Argon2-проверка до выдачи ciphertext, лимит попыток (по умолчанию 3), пустой пароль попытку не тратит
  • подсветка синтаксиса (highlight.js), ручной выбор языка
  • после расшифровки: прокрутка текста по вертикали и горизонтали, копирование в буфер обмена
  • вложения: drag-and-drop, выбор файлов, вставка скриншота из буфера
  • спиннер с логотипом при создании и расшифровке
  • RU / EN, светлая и тёмная тема
  • CAPTCHA: Cloudflare Turnstile и/или hCaptcha
  • админка: статистика, настройки, audit (пагинация + очистка), danger purge
  • Docker Compose и Helm; multi-arch образ (amd64/arm64) на Docker Hub: inecs/wrapped и Harbor hub.antropoff.ru/devops-tools/wrapped (Jenkins CI)

Ссылка для получателя: /w/<id>#<key> или токен wrapped_v1.<id>.<key>. Ключ шифрования в URL-фрагменте (#...) на сервер не уходит.

Что нового

Кратко по недавним доработкам UI, админки и релиза:

Область Что появилось
Расшифровка Прокрутка превью текста по вертикали и горизонтали; кнопка Копировать рядом со скачиванием
Пароль Argon2-проверка до выдачи ciphertext; лимит попыток; пустой пароль попытку не тратит; структурированные ошибки в UI
Админка Стартовая Статистика (MinIO, wraps, audit); очистка audit с подтверждением; пагинация журнала
Релиз Авто-SemVer (VERSION); Jenkins CI+CD: multi-arch → Harbor/Hub; Helm upgrade в k3s

Подробности — в разделах ниже (безопасность, админка, сборка образа).

Скриншоты

Интерфейс на русском и английском, светлая и тёмная тема.

Создание wrap

Светлая тема Тёмная тема
Создание — светлая Создание — тёмная

Создание с текстом и файлами

Текст с подсветкой синтаксиса, вложения, TTL и опциональный пароль.

Светлая тема Тёмная тема
С контентом — светлая С контентом — тёмная

Токен выдан

После создания — ссылка /w/<id>#<key> и wrapped_v1-токен. Ключ только во фрагменте URL.

Светлая тема Тёмная тема
Токен выдан — светлая Токен выдан — тёмная

Расшифровка

Одноразовое открытие: ciphertext удаляется на сервере, превью — только в текущей сессии браузера. Текст с подсветкой прокручивается по обеим осям; рядом — кнопки копирования и скачивания.

Светлая тема Тёмная тема
Расшифровка — светлая Расшифровка — тёмная

Быстрый старт (разработка)

Нужны Docker и Docker Compose.

make env          # создаёт .env из .env.example
make up           # app + Postgres + MinIO + nginx
Сервис URL
Приложение http://localhost:8000
Админка (статистика) http://localhost:8000/admin
MinIO Console http://localhost:9001 (wrappedminio / wrappedminio123)
Postgres (опционально с хоста) 127.0.0.1:5433
make logs         # логи app
make down         # остановить
make clean        # остановить и удалить volumes
make migrate      # alembic upgrade head (в контейнере app)

Локальный docker-compose.yml собирает образ из Dockerfile и монтирует код с --reload — режим разработки. Для сервера используйте готовый образ с Docker Hub (см. ниже).

При старте контейнер app сам выполняет alembic upgrade head (миграции, в т.ч. лимит попыток пароля).


Запуск на сервере (Docker Hub + Compose)

Образ: inecs/wrapped. На сервере не нужно собирать — достаточно docker compose pull / up.

1. Подготовка

mkdir -p /opt/wrapped && cd /opt/wrapped

Создайте .env (обязательно смените секреты):

# Приложение
APP_NAME=Wrapped
APP_ENV=production
APP_SECRET_KEY=замените-на-длинную-случайную-строку
APP_BASE_URL=https://wrapped.example.com
LOG_LEVEL=INFO
DOCS_ENABLED=false

# Админ
ADMIN_USERNAME=admin
ADMIN_PASSWORD=замените-пароль

# БД и S3 задаются в compose (см. ниже). Для внешнего Postgres/MinIO
# переопределите DATABASE_URL и S3_* здесь или в environment сервиса app.

# CAPTCHA (опционально; site keys также в админке)
TURNSTILE_SECRET_KEY=
HCAPTCHA_SECRET_KEY=

# Доверие к прокси для реального IP клиента
TRUSTED_PROXIES=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

APP_PORT=8000

2. docker-compose.yml для продакшена

services:
  proxy:
    image: nginx:1.27-alpine
    container_name: wrapped-proxy
    restart: unless-stopped
    ports:
      - "${APP_PORT:-8000}:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

  app:
    image: inecs/wrapped:0.1.0
    container_name: wrapped-app
    restart: unless-stopped
    expose:
      - "8000"
    env_file:
      - .env
    environment:
      DATABASE_URL: postgresql+asyncpg://wrapped:CHANGE_DB_PASSWORD@postgres:5432/wrapped
      S3_ENDPOINT_URL: http://minio:9000
      S3_ACCESS_KEY: CHANGE_MINIO_USER
      S3_SECRET_KEY: CHANGE_MINIO_PASSWORD
      S3_BUCKET: wrapped
      S3_USE_SSL: "false"
      S3_CREATE_BUCKET: "true"
      APP_BASE_URL: ${APP_BASE_URL:-http://localhost:8000}
      APP_ENV: production
      DOCS_ENABLED: "false"
      TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started
      minio-init:
        condition: service_completed_successfully

  postgres:
    image: postgres:16-alpine
    container_name: wrapped-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: wrapped
      POSTGRES_PASSWORD: CHANGE_DB_PASSWORD
      POSTGRES_DB: wrapped
    volumes:
      - wrapped_pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U wrapped -d wrapped"]
      interval: 5s
      timeout: 5s
      retries: 10

  minio:
    image: minio/minio:latest
    container_name: wrapped-minio
    restart: unless-stopped
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: CHANGE_MINIO_USER
      MINIO_ROOT_PASSWORD: CHANGE_MINIO_PASSWORD
    volumes:
      - wrapped_minio_data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 5

  minio-init:
    image: minio/mc:latest
    container_name: wrapped-minio-init
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      /bin/sh -c "
      mc alias set local http://minio:9000 CHANGE_MINIO_USER CHANGE_MINIO_PASSWORD &&
      mc mb --ignore-existing local/wrapped &&
      exit 0
      "

volumes:
  wrapped_pg_data:
  wrapped_minio_data:

Рядом — nginx.conf (пример также в репозитории: deploy/nginx.conf):

server {
    listen 80;
    server_name _;

    client_max_body_size 64m;

    location / {
        proxy_pass http://app:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_read_timeout 120s;
    }
}

3. Запуск и обновление

docker compose pull
docker compose up -d
curl -fsS http://127.0.0.1:8000/health
# новая версия образа, например 0.2.0
docker compose pull app
docker compose up -d app

HTTPS лучше завернуть снаружи (Caddy, Traefik, nginx на хосте, Cloudflare Tunnel). В APP_BASE_URL укажите публичный https://....

Внешний Postgres и S3/MinIO

Уберите сервисы postgres, minio, minio-init и задайте:

Переменная Пример
DATABASE_URL postgresql+asyncpg://user:pass@db-host:5432/wrapped
S3_ENDPOINT_URL https://s3.example.com
S3_ACCESS_KEY / S3_SECRET_KEY ключи
S3_BUCKET wrapped
S3_USE_SSL true
S3_CREATE_BUCKET false (бакет уже есть)
S3_REGION us-east-1

Минимальный compose — только app (+ опционально proxy):

services:
  app:
    image: inecs/wrapped:0.1.0
    restart: unless-stopped
    ports:
      - "8000:8000"
    env_file:
      - .env
    environment:
      APP_ENV: production
      DOCS_ENABLED: "false"

Переменные окружения

Переменная Описание По умолчанию
APP_NAME Имя сервиса Wrapped
APP_ENV development / production development
APP_SECRET_KEY Секрет сессий — смените
APP_BASE_URL Публичный URL http://localhost:8000
DOCS_ENABLED /docs, /redoc, /openapi.json в prod — false
LOG_LEVEL Уровень логов INFO
ADMIN_USERNAME Логин админки admin
ADMIN_PASSWORD Пароль админки — смените
DATABASE_URL Postgres (asyncpg)
S3_ENDPOINT_URL Endpoint MinIO/S3
S3_ACCESS_KEY / S3_SECRET_KEY Ключи S3
S3_BUCKET Имя бакета wrapped
S3_REGION Регион us-east-1
S3_USE_SSL TLS к S3 false
S3_CREATE_BUCKET Создавать бакет при старте true
TURNSTILE_SECRET_KEY Секрет Turnstile пусто
HCAPTCHA_SECRET_KEY Секрет hCaptcha пусто
TRUSTED_PROXIES IP/CIDR прокси для X-Forwarded-* loopback + RFC1918

Лимиты загрузки, TTL, MIME, CAPTCHA, режим пароля и число попыток пароля задаются в админке (таблица app_settings), не через env. Полный шаблон env: .env.example.


Модель безопасности

  • Zero-knowledge. Шифрование AES-GCM в браузере. Сервер видит только ciphertext и метаданные (TTL, MIME, размер).
  • Одноразовая расшифровка. После успешного открытия объект удаляется из S3/MinIO, статус → consumed. Превью остаётся только в сессии браузера.
  • Пароль. При создании с паролем сервер сохраняет Argon2-хеш и проверяет его до выдачи ciphertext.
    • пустой пароль → password_required, попытка не списывается;
    • неверный пароль → bad_password, UI показывает оставшиеся попытки;
    • лимит: password_max_attempts в админке (по умолчанию 3); при исчерпании wrap уничтожается (password_locked);
    • в client_only пароль также участвует в шифровании на клиенте.
  • CAPTCHA. Включается в админке; секреты — через env, site keys — в UI.
  • IP в audit. Реальный IP — только из заголовков от TRUSTED_PROXIES.

Ключ в #fragment не уходит на сервер в запросе страницы.

UX (v0.1.3+)

  • После создания: QR на share-link, иконки скачать QR (PNG) и Web Share (если есть navigator.share) под QR; отдельные кнопки Copy для ссылки / токена / пароля; пароль не советуется слать в той же переписке.
  • Create: счётчик размера ≈ used / max и предупреждение near-limit (сверх лимита — create.tooLarge).
  • Unwrap: Enter в поле пароля отправляет форму; после password_required / bad_password — focus+select; при ≥2 элементах — скачать всё (zip через JSZip); trust-строка на результате; спокойный экран «ссылка недоступна» для already used / expired (anti-enumeration); отдельные состояния для password_locked, rate limit, CAPTCHA, ошибки расшифровки.
  • Картинки после unwrap: inline-превью и lightbox (тап/клик).
  • Тема при первом визите следует prefers-color-scheme (пока нет выбора в localStorage); лёгкий haptic после успешного Copy.
  • Минимальный PWA: manifest.webmanifest (без Service Worker для API).
  • UI строки EN/RU через i18n.js.
  • Статика (/static/...) отдаётся с ?v=версия.mtime (cache-bust после деплоя).

Админка

Логин: /admin/login. После входа — Статистика (/admin/stats). На мобиле меню — через гамбургер.

Раздел URL Что делает
Статистика /admin/stats MinIO pending ciphertext; успешные unwrap vs сожжения паролем; pending / uploads; таблица статусов (count/size/items); with password; 24h/7d creates; audit create/unwrap; разбивка fail-причин unwrap (all-time и 24h)
Настройки /admin/settings Limits (upload, TTL, retention audit), rate limits, MIME allowlist (в т.ч. YAML / application/octet-stream для kubeconfig и неизвестных типов), пароль, CAPTCHA
Аудит /admin/audit Фильтры, пагинация (10/25/50/100), номера страниц, кнопка Очистить (с подтверждением; пишется событие admin.audit_clear)
Опасная зона /admin/danger Полная очистка wraps и объектов в MinIO (PURGE)

JSON Admin API: HTTP Basic (ADMIN_USERNAME / ADMIN_PASSWORD), тег OpenAPI Admin.


API

Тот же ZK-протокол, что в UI: шифруете локально, затем:

Метод Путь Назначение
GET /api/v1/settings Публичные лимиты, CAPTCHA, режим пароля, password_max_attempts
POST /api/v1/wraps Загрузить ciphertext + метаданные
POST /api/v1/wraps/{id}/unwrap Одноразово получить ciphertext
GET /health Healthcheck

Серверного «зашифруй за меня» нет.

Ошибки пароля при unwrap — 403:

detail.code Когда
password_required пароль не введён (попытка не списывается)
bad_password неверный пароль (attempts_remaining / attempts_max)
password_locked попытки исчерпаны, wrap уничтожен

Пример:

{
  "detail": {
    "code": "bad_password",
    "attempts_remaining": 2,
    "attempts_max": 3
  }
}

При DOCS_ENABLED=true — Swagger /docs и OpenAPI /openapi.json.


Сборка образа и git

Docker Hub (make release)

Сборка multi-arch (linux/amd64 + linux/arm64) через Docker Buildx и push манифеста на Docker Hub. Пушится только полный тег $(RELEASE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) (по умолчанию inecs/wrapped:…) — короткий тег вроде wrapped:0.1.0 при --push ушёл бы в docker.io/library/… и получил бы отказ в авторизации.

docker login   # пользователь inecs

make release IMAGE_TAG=0.1.0              # buildx amd64+arm64 + push inecs/wrapped:0.1.0
make release IMAGE_TAG=0.1.0 PUSH=0       # только сборка в buildx cache (без push)
make release RELEASE_PLATFORMS=linux/arm64  # одна платформа

Нужны Docker Buildx и (для cross-build) QEMU/эмуляция — на Docker Desktop обычно уже есть. Первый запуск создаёт builder wrapped-release (make buildx-setup внутри release).

Эквивалент:

docker buildx create --name wrapped-release --driver docker-container --use  # один раз
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t inecs/wrapped:0.1.0 \
  --push .

После успешного push Makefile печатает краткий imagetools inspect (платформы в манифесте). Образ при старте: alembic upgrade head + uvicorn :8000. Healthcheck: GET /health.

Jenkins CI (Harbor + Docker Hub)

После push в ветку main job со Script Path Jenkinsfile собирает multi-arch образ (linux/amd64 + linux/arm64) и пушит в оба реестра:

Реестр Образ
Harbor hub.antropoff.ru/devops-tools/wrapped
Docker Hub inecs/wrapped

Публичные теги на каждый реестр: :<semver> (из VERSION в коммите) и :latest. Short-sha больше не пушится. Промежуточные :<semver>-amd64 / -arm64 остаются только в Harbor для сборки multi-arch manifest.

Версионирование

Источник правды — файл VERSION (SemVer). Синхронизируется в pyproject.toml и Helm (Chart.yaml, values.yaml image.tag).

Где Поведение
UI (модалка «?») бейдж vX.Y.Z
make bump-patch / bump-minor ручной bump (по желанию)
make push commit/push без auto-bump
Jenkins (Jenkinsfile) читает VERSION из коммита без доп. bump → те же теги в образе и на кластере

Что в VERSION запушили — то и уйдёт в Harbor/Hub/деплой (и в футер модалки внутри образа).

Credentials (Global, как в job’ах):

ID Тип Назначение
ssh-gitea-key SSH Username with private key SCM checkout
gitea-jenkins-token Secret text (Gitea PAT) SCM/API Gitea. Для commit status из Multibranch нужен scope write:repository (сейчас при read-only в логе: Could not send notifications / 403/405). Checkout достаточно read:repository
harbor-devops-tools-push-pull-access Username/password Harbor devops-tools (robot)
docker-hub Username/password Docker Hub inecs
k3s-kubeconfig Secret file Helm/kubectl deploy в K3S

Agent: label docker (pod template с container docker / DinD).

Jenkins CD (Helm → k3s)

Как в аддоне DevOpsTools/K3S addons/wrapped: релиз wrapped, namespace wrapped, образ по умолчанию inecs/wrapped:0.1.0, pullPolicy: Always.

Первый install — только с хоста/CI ansible:

make addon-wrapped   # в репозитории K3S / k3s-ansible

Дальше обновление образа — отдельный Jenkins job, Script Path = Jenkinsfile.deploy:

helm upgrade wrapped ./helm/wrapped \
  --namespace wrapped \
  --reuse-values \
  --set image.repository=… \
  --set image.tag=… \
  --set image.pullPolicy=Always \
  --atomic --wait --timeout 600s
kubectl -n wrapped rollout status deployment/wrapped

--reuse-values сохраняет Ingress, секреты, Postgres/MinIO с кластера (то, что зашил addon).

Параметр job Умолч. Смысл
IMAGE_REPOSITORY inecs/wrapped как wrapped_image_repository
IMAGE_TAG из VERSION / CI SemVer образа (Always подтянет новый)
IMAGE_PULL_POLICY Always как в addon

Credential: k3s-kubeconfig (Secret file) — kubeconfig к K3S.

Рекомендуемая связка job’ов уже в Jenkinsfile: после успешного Build image (DinD) стадия Trigger deploy с wait: false и agent none — под Builder освобождается, затем отдельно стартует Deploy. Так на кластере не нужны два DinD одновременно.

Ручной запуск Deploy: Build with Parameters (пустой IMAGE_TAGcat VERSION).

Git (make push)

Как в proxmox_api_simulator:

make push
  1. git add .
  2. ввод сообщения коммита (можно несколько строк)
  3. Ctrl-D — конец ввода
  4. git push origin HEAD

Kubernetes (Helm)

helm upgrade --install wrapped ./helm/wrapped \
  -f my-values.yaml
image:
  repository: inecs/wrapped
  tag: "0.1.0"
  pullPolicy: IfNotPresent

app:
  env: production
  baseUrl: "https://wrapped.example.com"
  secretKey: "..."
  adminUsername: admin
  adminPassword: "..."
  docsEnabled: false
  trustedProxies: "*"   # если до пода достучаться может только Ingress

external:
  databaseUrl: "postgresql+asyncpg://..."
  s3:
    endpointUrl: "https://..."
    accessKey: "..."
    secretKey: "..."
    bucket: wrapped
    createBucket: false

Упаковка чарта: make helm-package IMAGE_TAG=0.1.0.


Make-цели

Цель Описание
make env .env из .env.example
make up / make down / make clean Dev-стек
make logs / make ps / make restart Логи, статус, рестарт app
make migrate alembic upgrade head
make revision m="msg" Новая alembic-ревизия
make release Multi-arch образ (amd64+arm64) → Docker Hub (inecs/wrapped) + Helm package
make buildx-setup Создать/выбрать Buildx builder wrapped-release (вызывается из release)
make push git add → многострочный commit (Ctrl-D) → push
make helm-lint / make helm-package Helm

Требования

  • Python 3.12+ (без Docker)
  • PostgreSQL 16+
  • S3-совместимое хранилище (MinIO и т.п.)
  • Docker Compose v2
  • для make release: Docker Buildx (+ QEMU для cross-build arm64 на amd64 и наоборот)

Лицензия

Proprietary / на ваше усмотрение.