Documentation menu
Getting started
New in v2.0
Feature deep dives
Reference
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
| Secret | Where it lives | Notes | Who can read it back |
|---|---|---|---|
Bot token | Environment, per deployment | Sent only to Telegram. Never returned by any route. | nobody — no route returns it |
JWT signing secret | Environment | Signs the 7-day tokens. Rotating it invalidates every session at once. | nobody — no route returns it |
Database password | Environment | Used inside the compose network; the database is not published by default. | nobody — no route returns it |
Redis password | Environment | Protects the shared rate-limit and lease state. | nobody — no route returns it |
Encryption key | Environment, operator-supplied | AES-256-GCM for session strings, API keys and backup bundles. | nobody — no route returns it |
Tenant AI keysper configuration | Database, encrypted | Write-only through the API; the response never carries the key back. | nobody — no route returns it |
Account passwordsper account | Database, hashed | PBKDF2-SHA256 with a per-row salt and stored iteration count. | nobody — no route returns it |
Telegram session stringsper session | Database, encrypted | A live session string is a credential; it is never returned in a list. | nobody — no route returns it |
Encryption at rest
| Data | Primitive | Property |
|---|---|---|
| Session strings, tenant API keys | AES-256-GCM | Authenticated: a modified ciphertext fails to decrypt rather than decrypting into something else. |
| Account passwords | PBKDF2-SHA256, 240 000 iterations, per-row salt | Verified in constant time; the iteration count is stored per row so it can be raised without invalidating old hashes. |
| Configuration backups | AES-256-GCM bundle, key derived with PBKDF2-SHA256 at 600 000 iterations | Portable 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 backups | Encrypted archive | Written only after the export succeeded; never in plaintext, not even transiently on disk. |
{
"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.
| Surface | You send | You get back |
|---|---|---|
POST /api/ai-configs | The provider base URL, model and API key. | The configuration with the key masked or omitted. |
POST /api/ai-configs/check-key | A provider, endpoint and key to test. | Whether the model answered, in what latency, and a redacted detail — never the key. |
POST /api/bot-tokens | A bot token. | The registration and its bot identity. The token is validated with Telegram first, then stored. |
POST /api/client/credentials | Personal client credentials. | Acceptance. There is no read route. |
POST /api/admin/settings | Installation 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:
| Input | What is written | Why that shape |
|---|---|---|
| Phone numbers in log lines | +989121234567 → +9891****4567 | Country 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 characters | Whatever the provider echoes back about a bad key is never repeated verbatim. |
| Redis addresses and passwords | redis://[REDACTED]@host:port | The 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
| Credential | How to rotate | Blast radius |
|---|---|---|
| Bot token | Issue a new token with BotFather, update ATF_BOT_TOKEN, restart the bot service. | The bot goes offline until it restarts; queued deliveries resume. |
| JWT secret | Set 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 key | Re-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 key | PUT /api/ai-configs/{id} with the new key. | Only that tenant's AI calls. |
| Account password | PATCH /api/auth/change-password with the old password. | The account; a new login is required after expiry. |
| Database password | Change 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.