cdrop — technical manual

Version 0.22.0

2026.11.05

This manual covers the purpose, architecture, API, configuration, access tokens, integration and deployment of cdrop.


1. Purpose

cdrop is a small HTTP service for authenticated publication of short-lived text artifacts with delivery over a short link and/or QR code and a limited number of fetches.

The service solves one problem:

Publish text with a secret token, get a short URL, serve that text at most N times within a TTL, then forget it.

cdrop is not:

It is transport, not a source of trust. The recipient must verify the signature, chain or origin of the artifact independently.

1.1. What can be published

Three artifact classes are accepted:

Only the format, the number of blocks and the total size are checked. Signatures, networks and certificate chains are not verified: a publish token guarantees only who published, and the web form relies on captcha and quotas. The recipient must verify the material independently.


2. Why cdrop exists

cnet artifacts are ordinary text, 1.3–4.5 KB. Passing them around is inconvenient:

cdrop provides:


3. Demand

3.1. Who uses it and why

3.2. Similar services and the difference

Class Examples cdrop difference
Pastebin PrivateBin, Hastebin no TTL policy or fetch limit in the short link
One-time secrets onetimesecret, Yopass, Bitwarden Send cdrop is not E2E and not for secrets
File sharing transfer.sh, file.io cdrop accepts only text/PEM and requires a token
Git storage Gist, S3 presigned long-lived, inconvenient for QR and phones
PKI tools step-ca, CFSSL issue certificates but do not deliver the artifact to a person

cdrop occupies a narrow niche: public artifacts, short lifetime, short link, mandatory publish authorization.

3.3. Honest assessment

This is not a mass-market SaaS and is unlikely to become one. Its value is an infrastructure tool and a by-product of cnet. The main demand is inside infrastructure and PKI teams. For secrets and private keys, use dedicated E2E tools.


4. Architecture

4.1. Layers

cmd/cdrop            CLI and server
  internal/config    YAML/JSON, environment variables
  publish            Validator contract, Item{Kind, Scope}, Quota
  store              SQLite/PostgreSQL, atomic fetch
  server             HTTP, limits, quotas, metrics, pages
  artifact           cnet/PEM/SSH format validator
  web                embedded HTML pages

4.2. Independence from cnet

The core (publish, store, server, web, artifact) does not import cnet/auth or cnet/uid. The invariant is checked by:

make -C cdrop check-core

There is no signature, trust or chain verification in the core. This allows the core to be reused in other projects and cdrop to evolve as a separate product.

4.3. What the service accepts

The service accepts artifacts of three classes: cnet, public PEM and SSH. One instance serves all classes; no separate policy setting is required. Classes are not mixed in one publication, but a chain of several PEM blocks is one publication of class pem. The service checks the format, the number of blocks and the total size; it does not verify signatures or certificate chains.


5. How the service works

5.1. Publish

  1. The client calls POST /api/v1/objects with Authorization: Bearer.
  2. The token, size and TTL are checked.
  3. The validator checks the body format:
    • cnet — armor correctness;
    • pem — allowed block types (chains are permitted), private keys forbidden;
    • ssh — key/certificate type, base64 and type match.
  4. Token quotas are checked.
  5. An ID is generated (12 characters of Crockford Base32, 60 bits).
  6. The body is stored with a TTL and a fetch counter.
  7. id, url, expires_at, remaining and sha256 are returned.

5.2. Fetch

5.3. Delete

DELETE /api/v1/objects/<ID> requires a token. Deletion is soft: the counter is set to zero and the object returns 410 until the cleanup cycle removes the row.

5.4. Cleanup

A background loop runs every 10 minutes, removes expired and soft-deleted rows and then trims the database to its size limit. Artifact bodies are not kept longer than the TTL plus one cleanup cycle.

5.5. Database size limit (drop head)

When the database may be untrusted or publishing is unbounded, the size is limited by configuration:

max-db-bytes: 1073741824   # 1 GiB; -1 - unlimited
max-objects: 0             # 0 - unlimited

Before each insert, in the same transaction, the service reads COUNT(*) and SUM(size). If the new row does not fit, the oldest publications (created_at, then id) are deleted in batches of 100 until there is room. This protects the database from being filled by unbounded publishing or a compromised account. If the artifact alone exceeds the limit, the service returns 507 Insufficient Storage. In PostgreSQL eviction is serialized with an advisory lock. size exists since migration 6 and is the plaintext artifact size (at-rest encryption adds a small overhead).

5.6. Pages


6. HTTP API

Base: https://<host>.

6.1. Publish

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 is optional and works only with a publish token: it asks to issue a limited grant together with the publication. kind must belong to the artifact scope (request for cnet, pem for PEM, ssh-key/ssh-cert for SSH); uses is 1..grants.max-uses.

The body is cnet armor, public PEM blocks (a chain is allowed) or SSH lines. When a grant is requested, the service embeds the full link https://<host>/p/<token> into the artifact (armor/PEM header X-CDROP-GRANT, SSH comment # X-CDROP-GRANT: ...).

Response 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 is present only when a grant was requested or the request was made with a grant token (then url is empty and remaining is the remaining number of grant publications).

Error codes:

Code Reason
400 not an artifact, empty body, invalid X-CDROP-TTL or grant
401 token missing/invalid/expired/disabled
403 scope not allowed, wrong kind, nested grant
410 grant expired or exhausted
413 body exceeds the limit
422 private key, unsupported block, mixed classes, more than 16 blocks
429 rate limit or quota (quota returns Retry-After: 3600)
507 database size limit, no room could be freed

Responses 400/422/403 are deliberately generic; details go only to the log.

6.1.1. Limited grant

A grant is a capability link: whoever receives it may publish the specified artifact type N times without a publish token. The grant:

6.2. Fetch

GET /o/<ID>
GET /o/<ID>.txt
Code Meaning
200 artifact body
404 unknown ID
410 expired, exhausted or deleted

Page headers: Cache-Control: no-store, X-Robots-Tag: noindex, Referrer-Policy: no-referrer, Content-Security-Policy (script nonce).

6.3. Metadata

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
}

Metadata contains the network and fingerprint, so it requires a token. Without a token — 401; foreign scope — 403.

6.4. Delete

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

204 — deleted; 401 — token; 403 — scope; 404 — no such ID; 410 — already expired/exhausted/deleted.

6.5. Publish web form

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> is the grant publication form: no captcha and no token field, it accepts only the grant artifact type and shows the remaining publications. On success it returns a link, QR and the remainder; when exhausted — 410 Gone.

/cdrop is the entry point: when the form is enabled (web-publish: true) it serves the publish form, otherwise the API reference. The reference is always available at /cdrop/api; the form links to it as «API reference». Russian variants are /ru/cdrop and /ru/cdrop/api.

GET /publish serves an HTML form: artifact, TTL (1h/24h/168h) and captcha. When grants are enabled the form also has an “issue a grant” checkbox; the number of publications is taken from grants.uses (3 by default), and the type is chosen by scope: request for cnet, pem for PEM, ssh-key/ssh-cert for SSH. POST verifies the captcha (own arithmetic, in-memory, 10-minute TTL, bound to IP, single-use) and publishes the artifact without a publish token. A limit of 10/min and an IP quota apply (20 publishes, 1 MiB, 50 active by default).

The form text is the same for all artifacts: it lists cnet, public PEM (including chains) and SSH; the placeholder is -----BEGIN CNET INVITATION-----.

The result is HTML with a link, a Copy button and a QR code (PNG data URI). If a grant was requested, the result also shows the full grant link, a Copy button and a QR code; the link is embedded into the artifact as well. The form accepts all supported artifacts; signatures and chains are not verified, so quotas, abuse handling and takedown are especially important. The captcha is only a speed bump, not protection against targeted bots.

6.6. Administrator panel

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

Login uses the /admin/login form (or /ru/admin/login): enter a valid publish token (not revoked, not disabled, not expired). The session is kept in an HttpOnly/Secure/SameSite=Strict cookie for 30 minutes (the “Remember me for 30 days” checkbox — 30 days); Basic and Bearer are still accepted for curl. The panel is available over TLS only. The cookie holds a random session ID, not the publish token; sessions are stored in state-dir/admin_sessions.json (0600) and survive a service restart. Browsers are redirected to the form; API clients get 401 and WWW-Authenticate.

The table shows every objects row, including expired and exhausted ones, without bodies: ID, kind, scope, size, creation, expiry, remaining, fetched, token-id, publisher IP, source (api/web), User-Agent and status. Sorting is done by column headers, including IP and source; pages of 25/50/100.

The IP is the TCP peer address; behind a reverse proxy it is the proxy address. IP and User-Agent are stored only in the publication row and are deleted with the artifact; they are not written to the log. The User-Agent is truncated to 200 characters and control characters are replaced with spaces.

Clicking an ID opens the artifact card: body, IP, source, client and other metadata. Viewing in the panel (or with an admin session/publish token) does not decrement the fetch counter; a regular recipient following the short link does.

Deletion is hard: selected rows or all rows disappear from the database and free space. Forms use CSRF double-submit cookies; delete all requires confirmation. The «Sign out» button in the header ends the session (POST /admin/logout).

6.7. Service endpoints


6.8. Binary file store

Since 0.24.0 cdrop can publish binary distributions (APK, ZIP, TGZ and other files) as well as text artifacts. The feature lives in a separate binstore module and is enabled by the service mode.

6.8.1. Modes

The mode field selects the service:

In pki mode the /admin/store and /s/<token> routes do not exist and no bin_* tables are created. In store mode PKI/CNET publishing is disabled and /admin redirects to /admin/store.

6.8.2. Sign-in and upload

The store is available only to the administrator, using the same token as the /admin panel (login form, Basic or Bearer). There is no anonymous upload.

  1. Sign in at /admin and click “Store” (or open /admin/store).
  2. Click “Upload file”.
  3. Enter a description and a TTL in days, then choose a file. 0 means keep forever, 365 means one year.
  4. During the upload the form shows percent, transferred bytes and speed; the Cancel button aborts it. The file is streamed straight to the store, without an intermediate copy in /tmp.
  5. The object page then shows the name, description, size, TTL, upload and expiry dates, SHA-256 and status.

The object table supports sorting by upload order, name, size, TTL and description, with 25/50/100 pagination. Deleting an object removes its file and all its links.

On the object page enter a note (who the link is for) and the maximum number of downloads. 0 means unlimited — intended for very close relatives or trusted colleagues.

The service returns a short link https://<base-url>/s/<token> and a page with a QR code. The link table shows the note, object, maximum, actual downloads, remaining count, creation date and status. A link stops working after the maximum is reached (410); a HEAD request does not consume a download. Individual links can be deleted.

6.8.4. TTL, capacity and cleanup

Expired objects are removed by the cleanup loop every 10 minutes, together with their files and links. Unlike PKI publications, old files are not evicted automatically: when capacity is exhausted an upload gets 507. On a small host set store.max-total-bytes well below the free disk space.

Files live in <state-dir>/store/objects/<xx>/<id> with mode 0600; metadata lives in the bin_objects and bin_links tables of the same database. A PostgreSQL backup (pg_dump) preserves metadata but not file bodies: include the store directory in a file-level backup.

6.8.5. Examples (Bearer token)

# upload
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

# create a link to an object
curl -fsS -H "Authorization: Bearer $CDROP_TOKEN" \
  -d object_id=<ID> -d note="for Ivan" -d max=1 \
  https://drop.example/admin/store/link

# download via the short link
curl -fLO https://drop.example/s/<token>

The description and ttl fields may come before or after the file. The csrf field for cookie/Basic auth must come before the file; a Bearer token needs no CSRF. With X-Requested-With: XMLHttpRequest the service returns 201 {id,url} instead of a redirect — this is what the form’s progress bar uses.

7. Configuration

Format — YAML or JSON. Path: -config or CDROP_CONFIG. Fields use kebab-case (base-url, max-size, publish-tokens, web-quota, token-env), as in cnet. Old snake_case keys (base_url, max_size, …) are still read; if both are present, kebab-case wins.

Example:

listen: ":80"                    # HTTP: redirect and ACME
listen-tls: ":443"               # HTTPS; empty - TLS off
tls-cert: "/etc/cdrop/tls/fullchain.pem"  # full chain
tls-key: "/etc/cdrop/tls/privkey.pem"     # private key
challenge-dir: "/var/www/certbot"         # ACME webroot; empty - off

base-url: "https://drop.example"  # public URL without trailing "/"base-url: "https://drop.example"  # public URL without trailing "/"
# canonical-url: "https://drop.example"  # security.txt; empty - base-url
state-dir: "/var/lib/cdrop"      # state, mode 0700
db: "postgres"                   # sqlite | postgres
dsn: "host=/var/run/postgresql dbname=cdrop user=cdrop sslmode=disable"

max-size: 16384                  # bytes; max 65536
max-db-bytes: 1073741824         # 1 GiB; -1 - unlimited
max-objects: 0                   # 0 - unlimited rows
default-ttl: "24h"               # default lifetime
max-ttl: "168h"                  # maximum 168h
fetches: 3                       # 1..3

grants:                          # limited publication grants
  enabled: true                  # false - X-CDROP-GRANT -> 403
  uses: 3                        # default publications per grant
  max-uses: 10                   # maximum that can be requested

# publish-token: "..."          # shared token; prefer CDROP_TOKEN
# metrics-token: "..."          # /metrics; prefer CDROP_METRICS_TOKEN
encrypt-bodies: false           # AES-256-GCM; key in CDROP_DATA_KEY
web-publish: false              # /publish form with captcha
web-quota:                      # form quotas per IP over 24 hours
  publishes-per-day: 20
  bytes-per-day: 1048576
  active-objects: 50

rate-limit:
  publish: 10                    # publish/metadata/delete per minute
  get: 60                        # fetch per minute

quotas:                          # per token over rolling 24 hours
  publishes-per-day: 500         # 0 - unlimited
  bytes-per-day: 8388608
  active-objects: 200

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

store:                           # modes store and pki+store
  # dir: "/var/lib/cdrop/store"  # empty - <state-dir>/store
  max-size: 1073741824           # per file; 8 GiB ceiling
  max-total-bytes: 0             # total; 0 - unlimited
  max-objects: 0                 # 0 - unlimited
  default-ttl-days: 0            # 0 - forever
  max-ttl-days: 3650             # maximum TTL in days

publish-tokens:
  - token-env: "CDROP_TOKEN_REGISTRAR"  # env var with the token
    scopes: ["cnet"]                    # cnet | pem | ssh
    expires: "2027-01-01T00:00:00Z"     # RFC3339; empty - no expiry
    disabled: false                     # disable without removing env
    quota:                              # overrides global quotas
      publishes-per-day: 50
      bytes-per-day: 1048576
      active-objects: 20

# abuse-contact: "mailto:abuse@example.org"  # security.txt; empty - 404

log:
  level: "info"                  # flood|trace|debug|info|notice|warn|error|crit
  pipe: "stderr"                 # stderr or file
  format: "tint"                 # tint | logfmt | json
  file: "/var/log/cdrop/cdrop.log"  # rotating file
  file-mode: "0600"              # file mode
  color-off: true                # no ANSI colour
  rotate:
    enable: true                 # lumberjack rotation
    max-size: 50                 # MiB before rotation
    max-age: 14                  # days to keep
    max-backups: 5               # old copies
    compress: true               # gzip old copies
    local-time: true             # filenames in local time

7.1. Environment variables

Variable Purpose
CDROP_CONFIG config path
CDROP_TOKEN shared publish token (any scope)
CDROP_METRICS_TOKEN access token for /metrics
CDROP_TOKEN_<NAME> values for publish-tokens[].token-env

Tokens are never written to YAML. For systemd use an EnvironmentFile with mode 0600.

7.2. cnet artifacts

Armored CNET INVITATION, CNET REQUEST, CNET PASS and other blocks with the CNET prefix are accepted by armor format. Signature, validity, network and nested invitations are not checked. kind is invite/request/passport for the three known types and cnet otherwise; scope is cnet. One cnet block per publication.

7.3. Public PEM and SSH

Allowed PEM blocks:

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

A certificate chain is several CERTIFICATE blocks in one publication; the service checks only the format and total size, not chain validity.

Private keys and their containers are forbidden: RSA PRIVATE KEY (PKCS#1), PRIVATE KEY (PKCS#8), ENCRYPTED PRIVATE KEY, EC PRIVATE KEY, DSA PRIVATE KEY, OPENSSH PRIVATE KEY, as well as arbitrary text. Binary PKCS#12/PFX is not accepted.

SSH: authorized_keys lines of the form <type> <base64> [comment]. Allowed types are ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256/384/521, sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com and their -cert-v01@openssh.com certificate forms. GOST 2012 public keys ssh-gost2012-256 and ssh-gost2012-512 are accepted as well (they have no certificate forms). Lines starting with # are comments. Keys and certificates cannot be mixed.

At most 16 blocks or SSH lines per publication; the total body size is limited by max-size (16 KiB default, 64 KiB maximum).

7.4. Limits

Parameter Default Range
Size 16 KiB up to 64 KiB
TTL 24 h 1 h – 168 h
Fetches 3 1–3
Publishes/deletes 10/min per IP configurable
Fetches 60/min per IP configurable
Cleanup 10 min fixed

8. Access tokens

8.1. Principle

A token is an opaque bearer secret. It is not a JWT, is not signed and is not derived from a password. The service compares it in constant time and does not store it in the database.

The token grants the right to:

Fetching by short link is public by possession of the ID.

8.2. Generation

Recommended:

openssl rand -hex 32

That is 32 random bytes (256 bits) in hex, 64 characters. The service imposes no minimum format beyond a non-empty string, but 256 random bits is a reasonable minimum.

The scripts/install.sh installer:

8.3. Scopes, quotas, expiry

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

Quotas are counted over a rolling 24 hours by objects.token_id. The quota check and the insert run in one transaction; PostgreSQL also takes an advisory lock on token_id, so concurrent publishes never exceed the limit.

8.4. Audit identifier

The service computes:

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

It appears in the publish/delete log and in objects.token_id. The token itself is never written to the log or database. A token-id lets you investigate an incident without revealing the secret.

8.5. Rotation

  1. Generate a new token.
  2. Add a second publish-tokens entry (or temporarily use the shared CDROP_TOKEN).
  3. Restart the service (env is read at startup).
  4. Switch systems to the new token.
  5. Remove the old token from env/config and restart.

Emergency revocation uses the revoked_tokens table (migration 4), checked on every publish/meta/delete:

cdrop tokenid -token "$TOKEN"          # non-secret token-id
cdrop revoke -config cdrop.yaml <id>   # revoke immediately, no restart
cdrop revoke -config cdrop.yaml -list
cdrop revoke -config cdrop.yaml -delete <id>

disabled: true in the config remains a static option (requires a restart).

For abuse/takedown:

cdrop takedown -config cdrop.yaml <ID>           # object -> 410
cdrop takedown -config cdrop.yaml -token-id <id> # token -> 401
cdrop takedown -config cdrop.yaml -list

The default reason is abuse (-reason); records are stored in the takedowns table. The policy is in ABUSE.md. The contact is set by abuse-contact and published in /.well-known/security.txt.

8.6. Limited publication grants

A grant is a link to the same service that lets the recipient publish the next artifact without a publish token:

https://drop.example/p/cdg_<random token>

Request a grant when publishing:

X-CDROP-GRANT: request=3

The format is <kind> or <kind>=<uses>. The kind must belong to the artifact scope:

Parent scope Allowed kind
cnet invite, request, passport, cnet (usually request)
pem pem
ssh ssh-key, ssh-cert

uses is 1..grants.max-uses; without =N the grants.uses default is used. Grants are not tied to cnet and work for PEM/SSH.

The service embeds the full link (with base-url) into the published artifact:

The header is outside the signed cnet bytes and does not break the signature. Unknown PEM headers are ignored by standard parsers; SSH comments are standard.

Grant properties:

Without X-CDROP-GRANT the service behaves exactly as before: old clients and the API keep working.


9. Binding to system secrets: a cnet registrar example

Goal: a cnet registrar publishes Invitations and Passports to cdrop without access to other scopes.

9.1. Step 1. Token on the cdrop side

On the cdrop server:

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

In the cdrop config:

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

Restart:

systemctl restart cdrop

Verify that the token is recognized:

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

A 201 response means the token and scope are configured. 401 — token not found/expired; 403 — scope not allowed; 422 — the artifact failed validation.

9.2. Step 2. Passing the token to the registrar

A token is as sensitive as a key. Storage options:

Never: git, YAML in a repository, argv (visible in ps), logs.

9.3. Step 3. Publishing from the registrar

The CLI takes the token from CDROP_TOKEN or -token:

# Invitation
cnet pki invite new --out invite.txt ...
CDROP_TOKEN="$CDROP_TOKEN_REGISTRAR" \
  cdrop publish -config registrar-cdrop.yaml -file invite.txt

# Passport
CDROP_TOKEN="$CDROP_TOKEN_REGISTRAR" \
  cdrop publish -config registrar-cdrop.yaml -file pass.txt -qr

registrar-cdrop.yaml contains only the address:

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

The token is not stored there.

An alternative is a separate env file for the 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. Step 4. Recipient

cdrop get -config subscriber-cdrop.yaml <ID>
# or open https://drop.example/o/<ID>

The recipient verifies the artifact with standard cnet tools:

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

cdrop does not replace this verification.

9.5. Step 5. Audit and revocation

In the cdrop log:

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

The token-id shows which system performed the publish. To revoke:

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

and restart. Or remove the line from the env file.


10. Integration

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 prints a QR code to the console; -qr-out/-out write a PNG.

10.2. cnet pki

Currently:

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

Planned: cnet pki invite new --publish and passport show --publish, so publishing is a single command.

10.3. Android

The cnet issuer may gain a “share an invitation via cdrop” mode. This requires the INTERNET permission and explicit cdrop enablement; when disabled there are no network calls.

10.4. Other systems

Any system with a token can publish over 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

The response contains grant.url - the full link for the recipient. To publish with the grant:

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

For a scoped token, use it in Authorization; the scope must match the artifact scope.


11. Deployment

11.1. Requirements

11.2. Installer

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

install.env is never committed to git.

Main fields:

SSH_HOST="drop.example"
DOMAIN="drop.example"
BASE_URL="https://drop.example"
DB="postgres"
TLS_MODE="letsencrypt"     # letsencrypt|files|off
TOKEN_FILE=""              # empty - generate
REUSE_TOKEN="1"

What the script does:

  1. builds the executable (CGO_ENABLED=0, -trimpath -s -w);
  2. prepares config, env, unit, hook, cron and backup;
  3. runs a preflight check before stopping the service;
  4. copies files over ssh/scp;
  5. creates the user, directories and the PostgreSQL role/database;
  6. issues a certificate (standalone on first start);
  7. starts the service and checks /healthz;
  8. rolls the executable back from cdrop.prev on failure.

11.3. TLS

Renewal: /etc/cron.d/cdrop-certbot twice a day; the deploy hook copies the certificate and runs systemctl reload cdrop (SIGHUP, no downtime).

11.4. PostgreSQL

The installer creates the role and database through 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

The schema and migrations are created at startup: the schema_migrations table stores versions 1–6 (base schema, network → scope, token_id, revoked_tokens). The rename migration is irreversible. The installer makes a pg_dump into /var/backups/cdrop/pre-upgrade-*.dump before an upgrade.

11.5. Post-install checks

curl -I http://drop.example/          # 301 to https
curl https://drop.example/healthz     # ok
curl --resolve drop.example:443:127.0.0.1 \
  https://drop.example/metrics        # locally
cdrop check -config /etc/cdrop/cdrop.yaml
# -db requires peer-auth: run as the cdrop user
su - cdrop -s /bin/sh -c \
  'cdrop check -db -config /etc/cdrop/cdrop.yaml'
systemctl status cdrop

11.6. Upgrade and rollback

A repeated install.sh is idempotent: config/unit are updated, the token is preserved and the executable is replaced. On a failed health check, cdrop.prev is restored automatically.

The installer keeps cdrop.prev, cdrop.yaml.prev and cdrop.service.prev; on a failed health check all three are restored. Manual rollback:

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. Backup and restore

Every day /usr/local/sbin/cdrop-backup makes a pg_dump into /var/backups/cdrop and keeps it for 7 days. Once a week /usr/local/sbin/cdrop-backup-verify restores the latest dump into a temporary database, checks the objects table and drops it.

Restore:

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

11.8. Releases

make -C cdrop dist

Builds linux/amd64 and linux/arm64:

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

The archive contains the executable, config.example.yaml, deploy/, the installer, healthcheck, SPEC, README, the manual (Markdown RU/EN, PDF and self-contained HTML), OpenAPI and BUILD.txt (version/git/built_at/os/arch). Verify with sha256sum -c.

Manual sources are cdrop-manual-ru.md and cdrop-manual-en.md; make -C cdrop manual builds the PDF and self-contained HTML pages that are embedded in the executable and served at /cdrop/manual and /ru/cdrop/manual. The HTML is also installed into /usr/share/doc/cdrop (root) or ~/.local/share/doc/cdrop (user/portable). pandoc is not required on the VPS.

11.9. Non-root installation and TUI

A 3×3 matrix: installation mode × TLS.

Mode systemd Paths Ports TLS
root system /etc/cdrop, /var/lib/cdrop 80/443 letsencrypt/files/off
user systemd --user + linger ~/.config/cdrop, ~/.local/state/cdrop 8080/8443 files/off
portable none ~/.cdrop 8080/8443 files/off

11.10. Removal

There is no automatic uninstall. The order is:

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. Operations

12.1. Log

By default the log is written to a rotating file (lumberjack) and also to stderr/journald. Paths:

Defaults: 50 MiB, 5 copies, 14 days, gzip, 0600. This protects the VPS from running out of space. color-off is enabled.

The log contains:

Artifact bodies and tokens are never logged.

12.2. Metrics

/metrics (loopback or CDROP_METRICS_TOKEN):

Every HTTP request is limited to 10 seconds, including database operations. Responses include X-Request-ID (incoming or generated), which is also written to the log. PostgreSQL uses a pool of 10/5 with a 30-minute lifetime and 5-minute idle time.

12.3. Healthcheck and alerts

/usr/local/sbin/cdrop-healthcheck runs from cron every 5 minutes:

Prometheus rules — deploy/alerts.yml: down, not ready, 5xx, 429, quota, certificate < 14 and < 3 days.

Behind a reverse proxy the loopback check is useless: requests with X-Forwarded-For/Forwarded are not considered loopback. Set CDROP_METRICS_TOKEN or a separate listener.

12.4. Resources

The service is undemanding: one process, SQLite/PostgreSQL, static pages. The main consumers are the database and TLS. Under growing load, watch the PostgreSQL pool and the token_id/created_at indexes.


13. Security


14. API contract

cdrop/openapi.yaml describes all public endpoints, the PublishResponse, Metadata and Error schemas, the ID parameter and the Bearer token. YAML correctness is checked by the server/openapi_test.go test.

15. Tests and quality

make -C cdrop check       # vet + check-core + test
make -C cdrop cover       # coverage
make -C cdrop fuzz        # short fuzz runs

Coverage (0.22.0):

Package Coverage
cmd/cdrop 73%
store 82%
artifact 95%
internal/config 91%
server 88%
secret 66%

Integration suite: make -C cdrop test-integration (install, bwrap-E2E, user-E2E, portable-E2E, go test ./...).

Installer generation is checked by make -C cdrop test-install (dry-run, config, remote.sh, token permissions). The full E2E in bwrap is make -C cdrop test-e2e: file layout, repeated installation, token preservation and rollback on a failed restart (systemd and PostgreSQL are replaced with stubs).

PostgreSQL tests are enabled with CDROP_TEST_POSTGRES_DSN. Fuzz targets: the PEM validator and NormalizeID.


16. Limitations and roadmap

Limitations:

Near-term directions:


17. Appendices

Sections 17.2 and 17.3 are control lists: [x] marks a completed check. They are checklists, not technological charts: operations, modes and time norms are not described here.

17.1. Minimal config

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. Token control list

17.3. Deployment control list