cdrop — техническое руководство

Версия 0.22.0

2026.11.05

Руководство описывает назначение, архитектуру, API, конфигурацию, токены доступа, интеграцию и развёртывание cdrop.


1. Назначение сервиса

cdrop — небольшой HTTP-сервис для аутентифицированной публикации короткоживущих текстовых артефактов с выдачей по короткой ссылке и/или QR-коду и ограниченным числом получений.

Сервис решает одну задачу:

Опубликовать текст по секретному токену, получить короткий URL, отдать этот текст не более N раз за TTL, затем забыть.

cdrop не является:

Это транспорт, а не источник доверия. Получатель обязан сам проверить подпись, цепочку или происхождение артефакта.

1.1. Что публикуется

Принимаются артефакты трёх классов:

Проверяются только формат, число блоков и общий размер. Подписи, сеть и цепочки сертификатов сервис не проверяет: при публикации по токену гарантией является токен, при публикации через форму — только captcha и квоты. Подлинность материала получатель проверяет сам.


2. Зачем понадобился cdrop

Артефакты cnet — обычный текст размером 1.3–4.5 КБ. Передавать их неудобно:

cdrop даёт:


3. Востребованность

3.1. Кто и зачем использует

3.2. Похожие сервисы и отличие

Класс Примеры Отличие cdrop
Pastebin PrivateBin, Hastebin нет TTL-политики и лимита получений в короткой ссылке
Одноразовые секреты onetimesecret, Yopass, Bitwarden Send cdrop не E2E и не для секретов
Файлообмен transfer.sh, file.io cdrop принимает только поддерживаемый текст (cnet/PEM/SSH) и требует токен
Гит-хранилища Gist, S3 presigned долгоживущие, неудобны для QR и телефона
PKI-инструменты step-ca, CFSSL выпускают, но не передают артефакт человеку

cdrop занимает узкую нишу: публичные артефакты, короткий срок, короткая ссылка, обязательная авторизация публикации.

3.3. Честная оценка

Это не массовый SaaS и вряд ли им станет. Ценность — инфраструктурный инструмент и побочный продукт cnet. Основной спрос — внутри инфраструктурных и PKI-команд. Для секретов и приватных ключей следует использовать специализированные E2E-инструменты.


4. Архитектура

4.1. Слои

cmd/cdrop            CLI и сервер
  internal/config    YAML/JSON, переменные окружения
  publish            контракт Validator, Item{Kind, Scope}, Quota
  store              SQLite/PostgreSQL, атомарное получение
  server             HTTP, лимиты, квоты, метрики, страницы
  artifact           валидатор cnet/PEM/SSH по формату
  web                встроенные HTML-страницы

4.2. Независимость от cnet

Ядро (publish, store, server, web, artifact) не импортирует cnet/auth и cnet/uid. Инвариант проверяется:

make -C cdrop check-core

Проверки подписи, доверия и цепочки в ядре нет. Это позволяет использовать ядро в других проектах и развивать cdrop как отдельный продукт.

4.3. Что принимает сервис

Сервис принимает артефакты трёх классов: cnet, публичные PEM и SSH. Один экземпляр обслуживает все классы; отдельная настройка политики не требуется. Классы в одной публикации не смешиваются, но цепочка из нескольких PEM-блоков — это одна публикация класса pem. Сервис проверяет формат, число блоков и общий размер; подписи и цепочки сертификатов не проверяются.


5. Как работает сервис

5.1. Публикация

  1. Клиент вызывает POST /api/v1/objects с Authorization: Bearer.
  2. Проверяется токен, размер, TTL.
  3. Валидатор проверяет формат тела:
    • cnet — корректность armor;
    • pem — допустимые типы блоков (цепочки разрешены), запрет приватных ключей;
    • ssh — типы ключей/сертификатов, base64 и соответствие типа.
  4. Проверяются квоты токена.
  5. Генерируется ID (12 символов Crockford Base32, 60 бит).
  6. Тело сохраняется с TTL и остатком получений.
  7. Возвращаются id, url, expires_at, remaining, sha256.

5.2. Получение

5.3. Удаление

DELETE /api/v1/objects/<ID> требует токен. Удаление мягкое: остаток обнуляется, срок истекает, объект отдаёт 410 до цикла очистки, затем строка удаляется.

5.4. Очистка

Фоновый цикл раз в 10 минут удаляет истёкшие и мягко удалённые записи, затем укладывает БД в предел объёма. Тела артефактов не хранятся дольше TTL плюс цикл очистки.

5.5. Предел объёма БД (drop head)

Если БД может быть недоверенной или публикация идёт бесконечно, объём ограничивается конфигом:

max-db-bytes: 1073741824   # 1 ГиБ; -1 - без предела
max-objects: 0             # 0 - без предела

Перед вставкой в той же транзакции считается COUNT(*) и SUM(size). Если новая запись не помещается, самые старые публикации (created_at, затем id) удаляются пачками по 100 до освобождения места. Это защищает СУБД от переполнения при бесконечной публикации и взломе. Если артефакт больше предела, сервис возвращает 507 Insufficient Storage. В PostgreSQL вытеснение сериализуется advisory lock. size ведётся с миграции 6 и равен размеру открытого артефакта (шифрование at-rest добавляет небольшую служебную нагрузку).

5.6. Страницы


6. HTTP API

База: https://<host>.

6.1. Публикация

POST /api/v1/objects
Authorization: Bearer <publish-token|grant-token>
Content-Type: text/plain; charset=utf-8
X-CDROP-TTL: 24h
X-CDROP-GRANT: request=3

X-CDROP-GRANT необязателен и работает только с publish-токеном: он просит выдать вместе с публикацией ограниченный грант. kind должен принадлежать области артефакта (request для cnet, pem для PEM, ssh-key/ssh-cert для SSH); uses — от 1 до grants.max-uses.

Тело — armor cnet, публичные PEM-блоки (возможна цепочка) или строки SSH. Если грант запрошен, сервис встраивает в артефакт полную ссылку https://<host>/p/<token> (armor/PEM — заголовком X-CDROP-GRANT, SSH — комментарием # X-CDROP-GRANT: ...).

Ответ 201 Created:

{
  "id": "7Q2M9K4TABCD",
  "url": "https://drop.example/o/7Q2M9K4TABCD",
  "expires_at": "2026-11-05T12:00:00Z",
  "remaining": 3,
  "sha256": "9f2c...",
  "grant": {
    "kind": "request",
    "url": "https://drop.example/p/cdg_...",
    "remaining": 3,
    "expires_at": "2026-11-05T12:00:00Z"
  }
}

Поле grant присутствует, только если грант запрошен или запрос выполнен токеном гранта (тогда url пуст, а remaining — остаток публикаций по гранту).

Коды ошибок:

Код Причина
400 не артефакт, пустое тело, неверный X-CDROP-TTL или грант
401 токен отсутствует/неверен/истёк/отключён
403 токену не разрешена область, тип не тот, вложенный грант
410 грант истёк или исчерпан
413 тело больше предела
422 приватный ключ, недопустимый блок, смешение классов, больше 16 блоков
429 лимит или квота (для квоты Retry-After: 3600)
507 предел объёма БД, освободить место не удалось

Ответы 400/422/403 обезличены; подробности — только в журнале.

6.1.1. Ограниченный грант

Грант — capability-ссылка: кто её получил, может N раз опубликовать артефакт указанного типа без publish-токена. Грант:

6.2. Получение

GET /o/<ID>
GET /o/<ID>.txt
Код Значение
200 тело артефакта
404 неизвестный ID
410 истёк, исчерпан или удалён

Заголовки страниц: Cache-Control: no-store, X-Robots-Tag: noindex, Referrer-Policy: no-referrer, Content-Security-Policy (script nonce).

6.3. Метаданные

GET /api/v1/objects/<ID>
Authorization: Bearer <publish-token>
{
  "id": "7Q2M9K4TABCD",
  "kind": "invite",
  "scope": "cnet",
  "size": 1302,
  "sha256": "9f2c...",
  "created_at": "2026-11-04T12:00:00Z",
  "expires_at": "2026-11-05T12:00:00Z",
  "remaining": 2
}

kind — invite, request, passport, pem, ssh-key, ssh-cert или cnet; scope — cnet, pem или ssh. Метаданные содержат тип и отпечаток, поэтому требуют токен. Без токена — 401; чужая область — 403.

6.4. Удаление

DELETE /api/v1/objects/<ID>
Authorization: Bearer <publish-token>

204 — удалено; 401 — токен; 403 — область; 404 — нет ID; 410 — уже истёк/исчерпан/удалён.

6.5. Веб-форма публикации

GET  /cdrop
GET  /cdrop/api
GET  /publish
POST /publish
GET  /ru/publish
POST /ru/publish
GET  /p/<token>
POST /p/<token>
GET  /ru/p/<token>
POST /ru/p/<token>

/p/<token> — форма публикации по гранту: без captcha и без поля токена, принимает только тип артефакта гранта, показывает остаток публикаций. При успехе — ссылка, QR и остаток; при исчерпании — 410 Gone.

/cdrop — точка входа: при включённой форме (web-publish: true) отдаёт форму публикации, иначе справку API. Справка всегда доступна по /cdrop/api; в форме есть ссылка «API reference». Русские варианты — /ru/cdrop и /ru/cdrop/api.

GET /publish отдаёт HTML-форму: artifact, TTL (1h/24h/168h) и captcha. Если гранты включены, в форме есть чек-бокс «выдать грант»; число публикаций берётся из grants.uses (по умолчанию 3), тип выбирается по области: request для cnet, pem для PEM, ssh-key/ssh-cert для SSH. POST проверяет captcha (собственная арифметика, in-memory, TTL 10 минут, привязка к IP, одноразовая) и публикует артефакт без publish-токена. Действуют лимит 10/мин и квота на IP (по умолчанию 20 публикаций, 1 МиБ, 50 активных).

Тексты формы единые: описание перечисляет cnet, публичные PEM (включая цепочки) и SSH; placeholder — -----BEGIN CNET INVITATION-----.

Результат — HTML со ссылкой, кнопкой Copy и QR-кодом (PNG data URI). Если грант запрошен, дополнительно показываются полная ссылка на грант, Copy и QR; ссылка также встроена в сам артефакт. Форма принимает все поддерживаемые артефакты; подписи и цепочки не проверяются, поэтому особенно важны квоты, abuse и takedown. Captcha — только speed bump, не защита от целевых ботов.

6.6. Панель администратора

GET  /admin
GET  /ru/admin
POST /admin/delete
POST /admin/delete-all

Вход — форма /admin/login (или /ru/admin/login): вводится действующий publish-токен (не отозванный, не disabled, не истёкший). Сессия хранится в HttpOnly/Secure/SameSite=Strict cookie 30 минут (галочка «Запомнить меня на 30 дней» — 30 суток); для curl поддержаны Basic и Bearer. Панель доступна только по TLS. В cookie хранится случайный ID сессии, а не publish-токен; сессии сохраняются в state-dir/admin_sessions.json (0600) и переживают рестарт сервиса. Без входа браузер перенаправляется на форму, API получает 401 и WWW-Authenticate.

Таблица показывает все строки objects, включая истёкшие и исчерпанные, без тел: ID, тип, область, размер, создание, срок, остаток, число получений, token-id, IP публикатора, источник (api/web), User-Agent и статус. Сортировка — по заголовкам, включая IP и источник; страницы по 25/50/100.

IP — адрес TCP-соединения; за реверс-прокси это адрес прокси. IP и User-Agent хранятся только в строке публикации, не пишутся в журнал и удаляются вместе с артефактом. User-Agent обрезается до 200 символов, управляющие символы заменяются пробелами.

Клик по ID открывает карточку артефакта: тело, IP, источник, клиент и остальные метаданные. Просмотр в панели (а также с админ-сессией или publish-токеном) не уменьшает остаток получений; обычный получатель по короткой ссылке уменьшает.

Удаление жёсткое: выбранные строки или все сразу исчезают из БД и освобождают место. Формы защищены CSRF (double-submit cookie), для удаления всех нужно подтверждение. Выход — кнопка «Выйти» в шапке (POST /admin/logout).

6.7. Служебные


6.8. Хранилище бинарных файлов (store)

Начиная с 0.24.0 cdrop умеет публиковать не только текстовые артефакты, но и дистрибутивы: APK, ZIP, TGZ и любые другие файлы. Это отдельный модуль binstore; он включается режимом сервиса.

6.8.1. Режимы

Режим задаётся полем mode:

В режиме pki маршруты /admin/store и /s/<token> отсутствуют, а таблицы bin_* не создаются. В режиме store отключены публикация PKI/CNET и /admin перенаправляет в /admin/store.

6.8.2. Вход и загрузка

Хранилище доступно только администратору по тому же токену, что и панель /admin (форма входа, Basic или Bearer). Анонимной загрузки нет.

  1. Войдите в /admin и нажмите «Хранилище» (или сразу откройте /admin/store).
  2. Нажмите «Загрузить файл».
  3. Задайте описание и TTL в днях, затем выберите файл. 0 — хранить вечно, 365 — один год.
  4. Во время загрузки показываются проценты, объём и скорость; кнопка «Отмена» прерывает передачу. Файл пишется потоково, без промежуточной копии в /tmp.
  5. После загрузки откроется карточка объекта: имя, описание, размер, TTL, даты загрузки и истечения, SHA-256, статус.

Таблица объектов поддерживает сортировку по порядку загрузки, имени, размеру, TTL и описанию, а также пагинацию 25/50/100. Удаление объекта удаляет и файл, и все его ссылки.

6.8.3. Короткие ссылки и QR

В карточке объекта задайте заметку (для кого ссылка) и максимум скачиваний. 0 означает неограниченное число скачиваний — режим для очень близких родственников или доверенных коллег.

Сервис выдаёт короткую ссылку https://<base-url>/s/<token> и страницу с QR-кодом. В таблице ссылок видны заметка, объект, максимум, фактическое число скачиваний, остаток, дата и статус. Ссылка перестаёт действовать после исчерпания максимума (410); HEAD-запрос скачивание не расходует. Отдельную ссылку можно удалить.

6.8.4. TTL, объём и очистка

Истёкшие объекты удаляются циклом очистки каждые 10 минут вместе с файлами и ссылками. В отличие от PKI-публикаций, старые файлы не вытесняются автоматически: при исчерпании объёма загрузка получает 507. На узле с небольшим диском задавайте store.max-total-bytes заметно меньше свободного места.

Файлы лежат в <state-dir>/store/objects/<xx>/<id> с правами 0600, метаданные — в таблицах bin_objects и bin_links той же БД. Бэкап PostgreSQL (pg_dump) сохраняет метаданные, но не тела файлов: каталог store нужно включить в файловый бэкап отдельно.

6.8.5. Примеры (Bearer-токен)

# загрузка
curl -fsS -H "Authorization: Bearer $CDROP_TOKEN" \
  -F file=@release.apk \
  -F description="Release 1.0" \
  -F ttl=365 \
  https://drop.example/admin/store/upload

# создание ссылки на объект
curl -fsS -H "Authorization: Bearer $CDROP_TOKEN" \
  -d object_id=<ID> -d note="for Ivan" -d max=1 \
  https://drop.example/admin/store/link

# скачивание по короткой ссылке
curl -fLO https://drop.example/s/<token>

Поля description и ttl могут идти до или после файла. Поле csrf для входа по cookie/Basic обязано идти до файла; Bearer-токен CSRF не требует. При X-Requested-With: XMLHttpRequest сервис отвечает 201 {id,url} вместо редиректа — так работает прогресс-бар в форме.

7. Конфигурация

Формат — YAML или JSON. Путь: -config или CDROP_CONFIG. Поля — kebab-case (base-url, max-size, publish-tokens, web-quota, token-env), как в cnet. Старые snake_case ключи (base_url, max_size, …) читаются без изменений; если заданы оба варианта, побеждает kebab-case.

Пример:

listen: ":80"                    # HTTP: редирект и ACME
listen-tls: ":443"               # HTTPS; пусто - TLS выключен
tls-cert: "/etc/cdrop/tls/fullchain.pem"  # полная цепочка
tls-key: "/etc/cdrop/tls/privkey.pem"     # закрытый ключ
challenge-dir: "/var/www/certbot"         # ACME webroot; пусто - нет

base-url: "https://drop.example"  # публичный адрес без "/"base-url: "https://drop.example"  # публичный адрес без "/"
# canonical-url: "https://drop.example"  # security.txt; пусто - base-url
state-dir: "/var/lib/cdrop"      # состояние, права 0700
db: "postgres"                   # sqlite | postgres
dsn: "host=/var/run/postgresql dbname=cdrop user=cdrop sslmode=disable"

max-size: 16384                  # байт; максимум 65536
max-db-bytes: 1073741824         # 1 ГиБ; -1 - без предела
max-objects: 0                   # 0 - без предела по числу строк
default-ttl: "24h"               # срок по умолчанию
max-ttl: "168h"                  # максимум 168h
fetches: 3                       # 1..3

grants:                          # ограниченные гранты публикации
  enabled: true                  # false - заголовок X-CDROP-GRANT -> 403
  uses: 3                        # публикаций по гранту по умолчанию
  max-uses: 10                   # максимум, который можно запросить

# publish-token: "..."          # общий токен; лучше CDROP_TOKEN
# metrics-token: "..."          # /metrics; лучше CDROP_METRICS_TOKEN
encrypt-bodies: false           # AES-256-GCM; ключ CDROP_DATA_KEY
web-publish: false              # форма /publish с captcha
web-quota:                      # квоты формы на IP за 24 часа
  publishes-per-day: 20
  bytes-per-day: 1048576
  active-objects: 50

rate-limit:
  publish: 10                    # публикация/метаданные/удаление в минуту
  get: 60                        # получение в минуту

quotas:                          # на токен за скользящие 24 часа
  publishes-per-day: 500         # 0 - без ограничения
  bytes-per-day: 8388608
  active-objects: 200

mode: "pki"                      # pki | store | pki+store

store:                           # режимы store и pki+store
  # dir: "/var/lib/cdrop/store"  # пусто - <state-dir>/store
  max-size: 1073741824           # один файл; предел 8 ГиБ
  max-total-bytes: 0             # сумма; 0 - без предела
  max-objects: 0                 # 0 - без предела
  default-ttl-days: 0            # 0 - вечно
  max-ttl-days: 3650             # максимум TTL в днях

publish-tokens:
  - token-env: "CDROP_TOKEN_REGISTRAR"  # имя переменной с токеном
    scopes: ["cnet"]                    # cnet | pem | ssh
    expires: "2027-01-01T00:00:00Z"     # RFC3339; пусто - бессрочно
    disabled: false                     # отключить без удаления из env
    quota:                              # переопределение quotas
      publishes-per-day: 50
      bytes-per-day: 1048576
      active-objects: 20

# abuse-contact: "mailto:abuse@example.org"  # security.txt; пусто - 404

log:
  level: "info"                  # flood|trace|debug|info|notice|warn|error|crit
  pipe: "stderr"                 # stderr или файл
  format: "tint"                 # tint | logfmt | json
  file: "/var/log/cdrop/cdrop.log"  # файл с ротацией
  file-mode: "0600"              # права файла
  color-off: true                # без ANSI-подкраски
  rotate:
    enable: true                 # ротация lumberjack
    max-size: 50                 # МБ до ротации
    max-age: 14                  # дней хранить
    max-backups: 5               # старых копий
    compress: true               # gzip старых копий
    local-time: true             # имена по локальному времени

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

Переменная Назначение
CDROP_CONFIG путь к конфигу
CDROP_TOKEN общий publish-токен (любая область)
CDROP_METRICS_TOKEN токен доступа к /metrics
CDROP_TOKEN_<NAME> значения для publish-tokens[].token-env

Токены в YAML не пишутся. Для systemd — EnvironmentFile 0600.

7.2. Артефакты cnet

Armored-блоки CNET INVITATION, CNET REQUEST, CNET PASS и другие блоки с префиксом CNET принимаются по формату armor. Подпись, срок, сеть и вложенные приглашения не проверяются. kind для трёх известных типов — invite/request/passport, для остальных — cnet; scope — cnet. В одной публикации допускается один блок cnet.

7.3. Публичные PEM и SSH

Допустимые PEM-блоки:

CERTIFICATE, TRUSTED CERTIFICATE, PUBLIC KEY, RSA PUBLIC KEY, CERTIFICATE REQUEST, NEW CERTIFICATE REQUEST, X509 CRL, PKCS7.

Цепочка сертификатов — это несколько блоков CERTIFICATE в одной публикации; сервис не проверяет её целостность, только формат и общий размер.

Запрещены приватные ключи и их контейнеры: RSA PRIVATE KEY (PKCS#1), PRIVATE KEY (PKCS#8), ENCRYPTED PRIVATE KEY, EC PRIVATE KEY, DSA PRIVATE KEY, OPENSSH PRIVATE KEY, а также произвольный текст. Бинарные PKCS#12/PFX не принимаются.

SSH: строки authorized_keys вида <type> <base64> [comment]. Допустимы ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256/384/521, sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com и их сертификатные формы -cert-v01@openssh.com. Дополнительно принимаются публичные ключи ГОСТ 2012 ssh-gost2012-256 и ssh-gost2012-512 (сертификатных форм у них нет). Строки # ... — комментарии. Смешивать ключи и сертификаты нельзя.

Не более 16 блоков или строк SSH на публикацию; общий размер тела ограничен max-size (16 КиБ по умолчанию, максимум 64 КиБ).

7.4. Пределы

Параметр По умолчанию Диапазон
Размер 16 КиБ до 64 КиБ
TTL 24 ч 1 ч – 168 ч
Получений 3 1–3
Публикаций/удалений 10/мин на IP настраивается
Получений 60/мин на IP настраивается
Очистка 10 мин фиксировано

8. Токены доступа

8.1. Принцип

Токен — непрозрачный bearer-секрет. Он не является JWT, не подписывается и не выводится из пароля. Сервис сравнивает его constant-time и не хранит в БД.

Токен даёт право:

Получение по короткой ссылке — публичное по владению ID.

8.2. Формирование

Рекомендуемый способ:

openssl rand -hex 32

Это 32 случайных байта (256 бит) в hex, 64 символа. Минимальных требований к формату сервис не накладывает, кроме непустой строки, но случайные 256 бит — разумный минимум.

Установщик scripts/install.sh:

8.3. Области, квоты, срок

publish-tokens:
  - token-env: "CDROP_TOKEN_REGISTRAR"
    scopes: ["cnet"]
    expires: "2027-01-01T00:00:00Z"
    disabled: false
    quota:
      publishes-per-day: 50
      bytes-per-day: 1048576
      active-objects: 20

Квоты считаются за скользящие 24 часа по objects.token_id. Проверка квоты и вставка выполняются в одной транзакции; в PostgreSQL дополнительно берётся advisory lock по token_id, поэтому параллельные публикации не превышают лимит.

8.4. Идентификатор для аудита

Сервис вычисляет:

token-id = sha256(token)[:16]

Он попадает в журнал публикации/удаления и в objects.token_id. Сам токен в журнал и БД не пишется. По token-id можно разобрать инцидент, не раскрывая секрет.

8.5. Ротация

  1. Сгенерировать новый токен.
  2. Добавить второй publish-tokens (или временно использовать общий CDROP_TOKEN).
  3. Перезапустить сервис (env читается при старте).
  4. Переключить системы на новый токен.
  5. Удалить старый токен из env/конфига и перезапустить.

Для аварийного отзыва используется таблица revoked_tokens (миграция 4), проверяемая на каждом publish/meta/delete:

cdrop tokenid -token "$TOKEN"          # несекретный token-id
cdrop revoke -config cdrop.yaml <id>   # отозвать сразу, без рестарта
cdrop revoke -config cdrop.yaml -list
cdrop revoke -config cdrop.yaml -delete <id>

disabled: true в конфиге остаётся статическим вариантом (нужен рестарт).

Для abuse/takedown:

cdrop takedown -config cdrop.yaml <ID>           # объект -> 410
cdrop takedown -config cdrop.yaml -token-id <id> # токен -> 401
cdrop takedown -config cdrop.yaml -list

Причина по умолчанию abuse (-reason), записи хранятся в таблице takedowns. Политика — ABUSE.md. Контакт задаётся abuse-contact и публикуется в /.well-known/security.txt.

8.6. Ограниченные гранты публикации

Грант — это ссылка на этот же сервис, которая позволяет получателю опубликовать следующий артефакт без publish-токена:

https://drop.example/p/cdg_<случайный токен>

Грант запрашивается при публикации заголовком:

X-CDROP-GRANT: request=3

Формат — <kind> или <kind>=<uses>. Тип обязан принадлежать области артефакта:

Область родителя Допустимые kind
cnet invite, request, passport, cnet (обычно request)
pem pem
ssh ssh-key, ssh-cert

uses — от 1 до grants.max-uses; без =N берётся grants.uses. Грант не привязан к cnet и работает для PEM/SSH.

Сервис встраивает полную ссылку (с base-url) в опубликованный артефакт:

Заголовок находится вне подписанных байтов cnet-артефакта и не ломает подпись. Для PEM неизвестные заголовки игнорируются стандартными парсерами; для SSH комментарии стандартны.

Свойства гранта:

Без заголовка X-CDROP-GRANT поведение сервиса не меняется: старые клиенты и API работают как прежде.


9. Привязка к секретам систем: пример регистратора cnet

Задача: регистратор cnet должен публиковать Приглашения и Паспорта в cdrop, не имея доступа к чужим областям.

9.1. Шаг 1. Токен на стороне cdrop

На сервере cdrop:

umask 077
TOKEN=$(openssl rand -hex 32)
printf 'CDROP_TOKEN_REGISTRAR=%s\n' "$TOKEN" >> /etc/cdrop/cdrop.env

В конфиг cdrop:

publish-tokens:
  - token-env: "CDROP_TOKEN_REGISTRAR"
    scopes: ["cnet"]
    quota:
      publishes-per-day: 100
      bytes-per-day: 4194304
      active-objects: 50

Перезапуск:

systemctl restart cdrop

Проверка, что токен распознан:

set -a; . /etc/cdrop/cdrop.env; set +a
curl -X POST https://drop.example/api/v1/objects \
  -H "Authorization: Bearer $CDROP_TOKEN_REGISTRAR" \
  -H "X-CDROP-TTL: 1h" \
  --data-binary @test-invite.txt

Если ответ 201 — токен и область настроены. 401 — токен не найден/истёк; 403 — область не разрешена; 422 — артефакт отклонён по формату (приватный ключ, недопустимый блок, смешение классов).

9.2. Шаг 2. Передача токена регистратору

Токен — такой же секрет, как ключ. Варианты хранения:

Нельзя: git, YAML в репозитории, argv (виден в ps), логи.

9.3. Шаг 3. Публикация из регистратора

CLI берёт токен из CDROP_TOKEN или -token:

# Приглашение
cnet pki invite new --out invite.txt ...
CDROP_TOKEN="$CDROP_TOKEN_REGISTRAR" \
  cdrop publish -config registrar-cdrop.yaml -file invite.txt

# Паспорт
CDROP_TOKEN="$CDROP_TOKEN_REGISTRAR" \
  cdrop publish -config registrar-cdrop.yaml -file pass.txt -qr

registrar-cdrop.yaml содержит только адрес:

base-url: "https://drop.example"

Токен в нём не хранится.

Альтернатива — отдельный env для CLI:

install -m 0600 /dev/null /etc/cnet/cdrop.env
printf 'CDROP_TOKEN=%s\n' "$REGISTRAR_TOKEN" > /etc/cnet/cdrop.env
set -a; . /etc/cnet/cdrop.env; set +a
cdrop publish -config registrar-cdrop.yaml -file invite.txt

9.4. Шаг 4. Получатель

cdrop get -config subscriber-cdrop.yaml <ID>
# или открыть https://drop.example/o/<ID>

Получатель проверяет артефакт штатными средствами cnet:

cnet pki invite show --in invite.txt
cnet pki passport verify --in pass.txt --root root.crt ...

cdrop не заменяет эту проверку.

9.5. Шаг 5. Аудит и отзыв

В журнале cdrop:

published id=... kind=invite scope=cnet token=<token-id>

По token-id видно, какой системой сделана публикация. Для отзыва:

publish-tokens:
  - token-env: "CDROP_TOKEN_REGISTRAR"
    disabled: true

и рестарт. Либо удалить строку из env.


10. Интеграция

10.1. CLI

cdrop serve   -config /etc/cdrop/cdrop.yaml
cdrop check   -config /etc/cdrop/cdrop.yaml [-db]
cdrop publish -config cdrop.yaml -file invite.txt [-ttl 24h] [-qr] [-qr-out png]
cdrop publish -config cdrop.yaml -file invite.txt -grant request=3
cdrop get     -config cdrop.yaml <ID|URL>
cdrop status  -config cdrop.yaml <ID|URL>
cdrop qr      -config cdrop.yaml [-out png] <ID|URL>
cdrop version

-qr печатает QR в консоль, -qr-out/-out пишут PNG.

10.2. cnet pki

Сейчас:

cnet pki invite new ... --out invite.txt
cdrop publish ... -file invite.txt

В планах — cnet pki invite new --publish и passport show --publish, чтобы публикация была одной командой.

10.3. Android

Издатель cnet может получить режим «поделиться приглашением через cdrop». Требуется разрешение INTERNET и явное включение cdrop; при выключенном режиме сетевых вызовов нет.

10.4. Другие системы

Любая система с токеном может публиковать через HTTP:

curl -X POST https://drop.example/api/v1/objects \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-CDROP-TTL: 12h" \
  -H "X-CDROP-GRANT: request=3" \
  --data-binary @artifact.txt

Ответ содержит grant.url — полную ссылку для получателя. Для публикации по гранту:

curl -X POST https://drop.example/api/v1/objects \
  -H "Authorization: Bearer cdg_..." \
  --data-binary @request.txt

Для scoped-токена укажите его в Authorization; область токена должна совпадать с scope артефакта (cnet, pem, ssh).


11. Развёртывание

11.1. Требования

11.2. Установщик

cd cdrop
cp scripts/install.env.example scripts/install.env
$EDITOR scripts/install.env
./scripts/install.sh scripts/install.env

install.env в git не попадает.

Основные поля:

SSH_HOST="drop.example"
DOMAIN="drop.example"
BASE_URL="https://drop.example"
DB="postgres"
TLS_MODE="letsencrypt"     # letsencrypt|files|off
TOKEN_FILE=""              # пусто - сгенерировать
REUSE_TOKEN="1"

Что делает скрипт:

  1. собирает исполняемый файл (CGO_ENABLED=0, -trimpath -s -w);
  2. готовит конфиг, env, unit, hook, cron, бэкап;
  3. проверяет окружение (preflight) до остановки сервиса;
  4. копирует файлы по ssh/scp;
  5. создаёт пользователя, каталоги, роль/БД PostgreSQL;
  6. выпускает сертификат (standalone при первом запуске);
  7. запускает сервис и проверяет /healthz;
  8. при неудаче откатывает исполняемый файл из cdrop.prev.

11.3. TLS

Продление: /etc/cron.d/cdrop-certbot дважды в сутки, deploy-hook копирует сертификат и делает systemctl reload cdrop (SIGHUP, без простоя).

11.4. PostgreSQL

Установщик создаёт роль и БД через peer-auth:

su - postgres -c 'createuser --login cdrop'
su - postgres -c 'createdb -O cdrop cdrop'

DSN:

host=/var/run/postgresql dbname=cdrop user=cdrop sslmode=disable

Схема и миграции создаются при старте: таблица schema_migrations хранит версии 1–6 (базовая схема, network → scope, token_id, revoked_tokens). Миграция переименования необратима. Установщик сам делает pg_dump в /var/backups/cdrop/pre-upgrade-*.dump перед обновлением.

11.5. Проверка после установки

curl -I http://drop.example/          # 301 на https
curl https://drop.example/healthz     # ok
curl --resolve drop.example:443:127.0.0.1 \
  https://drop.example/metrics        # локально
cdrop check -config /etc/cdrop/cdrop.yaml
# -db требует peer-auth: запускать от пользователя cdrop
su - cdrop -s /bin/sh -c \
  'cdrop check -db -config /etc/cdrop/cdrop.yaml' 
systemctl status cdrop

11.6. Обновление и откат

Повторный install.sh идемпотентен: конфиг/unit обновляются, токен сохраняется, исполняемый файл заменяется. При неудачном health-check автоматически возвращается cdrop.prev.

Установщик сохраняет cdrop.prev, cdrop.yaml.prev и cdrop.service.prev; при неудачном health-check возвращаются все три. Ручной откат:

install -m 0755 /usr/local/bin/cdrop.prev /usr/local/bin/cdrop
install -m 0644 /etc/cdrop/cdrop.yaml.prev /etc/cdrop/cdrop.yaml
install -m 0644 /etc/systemd/system/cdrop.service.prev \
  /etc/systemd/system/cdrop.service
systemctl daemon-reload
systemctl restart cdrop

11.7. Бэкап и восстановление

Ежедневно /usr/local/sbin/cdrop-backup делает pg_dump в /var/backups/cdrop, хранит 7 суток. Раз в неделю /usr/local/sbin/cdrop-backup-verify восстанавливает последний дамп во временную БД, проверяет таблицу objects и удаляет её.

Восстановление:

su - cdrop -s /bin/sh -c \
  'pg_restore --clean --no-owner -d cdrop /var/backups/cdrop/<файл>'
systemctl restart cdrop

11.8. Релизы

make -C cdrop dist

Собираются linux/amd64 и linux/arm64:

dist/cdrop-<version>-linux-amd64.tar.gz
dist/cdrop-<version>-linux-arm64.tar.gz
dist/*.sha256

В архив входят исполняемый файл, config.example.yaml, deploy/, установщик, healthcheck, SPEC, README, руководство (Markdown RU/EN, PDF и self-contained HTML), OpenAPI и BUILD.txt (version/git/built_at/os/arch). Проверка: sha256sum -c.

Исходники руководства — cdrop-manual-ru.md и cdrop-manual-en.md; make -C cdrop manual собирает PDF и HTML. HTML встраивается в исполняемый файл и доступен по /cdrop/manual и /ru/cdrop/manual; pandoc на VPS не требуется.

11.9. Non-root установка и TUI

Матрица 3×3: режим установки × TLS.

Режим systemd Пути Порты TLS
root системный /etc/cdrop, /var/lib/cdrop 80/443 letsencrypt/files/off
user systemd --user + linger ~/.config/cdrop, ~/.local/state/cdrop 8080/8443 files/off
portable нет ~/.cdrop 8080/8443 files/off

11.10. Удаление

Автоматического uninstall нет. Порядок:

systemctl disable --now cdrop
rm -f /etc/systemd/system/cdrop.service
rm -f /etc/cron.d/cdrop-certbot /etc/cron.d/cdrop-backup
rm -f /usr/local/bin/cdrop /usr/local/bin/cdrop.prev
rm -f /usr/local/sbin/cdrop-cert-hook /usr/local/sbin/cdrop-backup
rm -rf /etc/cdrop /var/lib/cdrop
userdel cdrop
su - postgres -c 'dropdb cdrop'
su - postgres -c 'dropuser cdrop'
systemctl daemon-reload

12. Эксплуатация

12.1. Журнал

По умолчанию журнал пишется в файл с ротацией (lumberjack), а также в stderr/journald. Пути:

Параметры по умолчанию: 50 МБ, 5 копий, 14 дней, gzip, 0600. Это защищает VPS от переполнения. color-off включён.

В журнал пишутся:

Тела артефактов и токены не логируются.

12.2. Метрики

/metrics (loopback или CDROP_METRICS_TOKEN):

Каждый HTTP-запрос ограничен 10 секундами, включая операции с БД. В ответах возвращается X-Request-ID (входящий или сгенерированный), он же пишется в журнал. PostgreSQL использует пул 10/5 с lifetime 30 минут и idle 5 минут.

12.3. Healthcheck и алерты

/usr/local/sbin/cdrop-healthcheck запускается cron каждые 5 минут:

Правила Prometheus — deploy/alerts.yml: down, not ready, 5xx, 429, квота, сертификат < 14 и < 3 дней.

За реверс-прокси loopback-проверка бесполезна: запросы с X-Forwarded-For/Forwarded не считаются loopback. Задайте CDROP_METRICS_TOKEN или отдельный listener.

12.4. Ресурсы

Сервис нетребователен: один процесс, SQLite/PostgreSQL, статические страницы. Основные потребители — БД и TLS. При росте нагрузки следует смотреть на пул PostgreSQL и индексы token_id/created_at.


13. Безопасность


14. API-контракт

cdrop/openapi.yaml описывает все публичные эндпоинты, схемы PublishResponse, Metadata, Error, параметр ID и Bearer-токен. Корректность YAML проверяется тестом server/openapi_test.go.

15. Тесты и качество

make -C cdrop check       # vet + check-core + test
make -C cdrop cover       # покрытие
make -C cdrop fuzz        # короткие fuzz-прогоны

Покрытие (0.22.0):

Пакет Покрытие
cmd/cdrop 73%
store 82%
artifact 95%
internal/config 91%
server 88%
secret 66%

Интеграционный набор: make -C cdrop test-integration (install, bwrap-E2E, user-E2E, portable-E2E, go test ./...).

Генерация установщика проверяется make -C cdrop test-install (dry-run, конфиг, remote.sh, права токена). Полный E2E в bwrap — make -C cdrop test-e2e: раскладка файлов, повторная установка, сохранение токена и rollback при сбое restart (systemd/PostgreSQL подменяются заглушками).

PostgreSQL-тесты включаются переменной CDROP_TEST_POSTGRES_DSN. Fuzz: валидатор artifact и NormalizeID.


16. Ограничения и развитие

Ограничения:

Ближайшие направления:


17. Приложения

Разделы 17.2 и 17.3 оформлены как контрольные листы: отметка [x] означает выполненную проверку. Это перечни контрольных проверок, а не технологические карты: операции, режимы и нормы времени здесь не описываются.

17.1. Минимальный конфиг

listen: ":8080"
base-url: "http://127.0.0.1:8080"
state-dir: "./state"
db: "sqlite"
publish-token: "dev-token"
fetches: 3
log:
  level: "info"
  pipe: "stderr"
  format: "logfmt"

17.2. Контрольный лист токена

17.3. Контрольный лист развёртывания