Documentation menu

Secrets and keys

Secrets and keys

What is encrypted, what is write-only, what is redacted before it reaches a log, and the exact rotation procedure for each credential the platform holds.

Verified against the checkout built for v2.2.4 · updated 20 Oct 2026

On this page

A forwarding platform necessarily holds the most sensitive credentials a user has: a Telegram session, a bot token, a model provider key. The design rule is narrow and applied consistently — a secret goes in, and it does not come back out. Not to a client, not to a log, not to an error message.

The inventory

Every credential the platform handles
SecretWhere it livesNotesWho can read it back
Bot token
ATF_BOT_TOKEN
Environment, per deploymentSent only to Telegram. Never returned by any route.nobody — no route returns it
JWT signing secret
ATF_JWT_SECRET
EnvironmentSigns the 7-day tokens. Rotating it invalidates every session at once.nobody — no route returns it
Database password
ATF_DB_PASSWORD
EnvironmentUsed inside the compose network; the database is not published by default.nobody — no route returns it
Redis password
ATF_REDIS_PASSWORD
EnvironmentProtects the shared rate-limit and lease state.nobody — no route returns it
Encryption key
ATF_ENCRYPTION_KEY
Environment, operator-suppliedAES-256-GCM for session strings, API keys and backup bundles.nobody — no route returns it
Tenant AI keys
per configuration
Database, encryptedWrite-only through the API; the response never carries the key back.nobody — no route returns it
Account passwords
per account
Database, hashedPBKDF2-SHA256 with a per-row salt and stored iteration count.nobody — no route returns it
Telegram session strings
per session
Database, encryptedA live session string is a credential; it is never returned in a list.nobody — no route returns it

Encryption at rest

Cryptographic primitives and what each one guarantees
DataPrimitiveProperty
Session strings, tenant API keysAES-256-GCMAuthenticated: a modified ciphertext fails to decrypt rather than decrypting into something else.
Account passwordsPBKDF2-SHA256, 240 000 iterations, per-row saltVerified in constant time; the iteration count is stored per row so it can be raised without invalidating old hashes.
Configuration backupsAES-256-GCM bundle, key derived with PBKDF2-SHA256 at 600 000 iterationsPortable between hosts: export on one, import on the next. The bundle uses a higher iteration count than a password because its passphrase protects everything at once.
Session backupsEncrypted archiveWritten only after the export succeeded; never in plaintext, not even transiently on disk.
A correct environment check — no values
{
  "checks": [
    { "key": "ATF_BOT_TOKEN", "set": true, "required": true },
    { "key": "ATF_ENCRYPTION_KEY", "set": true, "required": true },
    { "key": "ATF_REDIS_PASSWORD", "set": false, "required": false }
  ],
  "missing_required": [],
  "missing_recommended": ["ATF_REDIS_PASSWORD"],
  "ok": true
}

Write-only by contract

Four surfaces accept a secret and none of them read one back. The asymmetry is deliberate: a client that needs to know whether a key works calls the probe, and a client that needs to change one sends a new value.

Every write-only secret surface
SurfaceYou sendYou get back
POST /api/ai-configsThe provider base URL, model and API key.The configuration with the key masked or omitted.
POST /api/ai-configs/check-keyA provider, endpoint and key to test.Whether the model answered, in what latency, and a redacted detail — never the key.
POST /api/bot-tokensA bot token.The registration and its bot identity. The token is validated with Telegram first, then stored.
POST /api/client/credentialsPersonal client credentials.Acceptance. There is no read route.
POST /api/admin/settingsInstallation settings, including secrets.The settings with secret fields masked.

Redaction in logs

Logs are where credentials leak by accident, so redaction is a single module used by every call site rather than a rule each author is trusted to remember:

What a log line may contain
InputWhat is writtenWhy that shape
Phone numbers in log lines+989121234567 → +9891****4567Country prefix and last four digits survive, so two accounts stay distinguishable without being identifiable.
Telegram usernames@someone_long → so**********First two characters only — enough to match a report, not enough to identify a person.
Anything that is not a phone number in a phone field[REDACTED]Unstructured data in an identity field is not assumed benign.
Provider credentials in a probe result[REDACTED], truncated to 300 charactersWhatever the provider echoes back about a bad key is never repeated verbatim.
Redis addresses and passwordsredis://[REDACTED]@host:portThe status endpoint reports reachability without publishing the credential that made it reachable.

The access trail

Each request is recorded with the verified actor and the correlation id — the id comes from the validated token, so a spoofed header cannot attribute an action to somebody else.

Refusals are evidence

A refused key or a rejected token is logged as a security-relevant warning. The access logger sits outside the API-key gate precisely so a bad key is not invisible.

Rotation

Rotation, and what each one interrupts
CredentialHow to rotateBlast radius
Bot tokenIssue a new token with BotFather, update ATF_BOT_TOKEN, restart the bot service.The bot goes offline until it restarts; queued deliveries resume.
JWT secretSet the new value and restart the API.Every session ends at once — all users sign in again. Acceptable for a suspected compromise, disruptive otherwise.
Encryption keyRe-encrypt before switching: export, import under the new key, then rotate the environment.Doing this in the wrong order makes existing ciphertext unreadable. There is no recovery path.
Tenant AI keyPUT /api/ai-configs/{id} with the new key.Only that tenant's AI calls.
Account passwordPATCH /api/auth/change-password with the old password.The account; a new login is required after expiry.
Database passwordChange it in the database and in ATF_DB_PASSWORD, then restart the API and core together.Writes fail between the two restarts — do it in a maintenance window.

What is not protected

  • !Transport inside the compose network. Containers speak plain gRPC and HTTP to each other. TLS is expected at the reverse proxy, and the network boundary is the container network.
  • !A stolen bot token is a stolen bot. Validation on registration proves the token works; it cannot prove who holds it later. Revoke with BotFather, then remove the registration.
  • !A Telegram session file is a logged-in client. That is why it is encrypted at rest and never returned by a list route — but on a running host, the decrypted material exists in memory for as long as the session does.
  • !The operator's own host. Root on the machine that runs the stack can read the environment and the database. No application-level control changes that.