KMS key security
KMS key security
Summary
Cornice stores venue credentials in the database as encrypted blobs and decrypts them in memory at runtime. The encryption key is supplied through the server environment as cornice_kms_key. This is a pragmatic “local envelope encryption” model suitable for a low‑volume prototype with trusted operators. It is not equivalent to a managed cloud KMS.
Threat model and limitations
- A server operator with access to both the database and
cornice_kms_keycan decrypt secrets. This is an accepted risk for the current phase. - Secrets are never logged and are not written to disk except for keys that require a file path (see below), which are stored in
/tmpwith0600permissions. - If the master key is rotated, previously stored secrets must be re‑encrypted.
Data model
Secrets are stored in arb_account_secrets:
user_id(foreign key to users)venue(e.g.,polymarket,kalshi)secret_key(e.g.,POLY_PRIVATE_KEY,KALSHI_API_KEY)encrypted_value(Fernet ciphertext)label,active
The schema supports multiple venues and flexible key names per venue.
Encryption implementation
app/infra/crypto.py derives a 32‑byte key from cornice_kms_key using SHA‑256 and uses Fernet for encryption/decryption. All encryption happens in the application layer; the database only sees ciphertext.
Runtime behavior
app/data/arb_ingest.py loads secrets for the configured user, decrypts them, and sets environment variables for the arb engine.
For file‑based secrets:
- If
secret_keyends with_PRIVATE_KEY_PATH, the decrypted value is treated as file contents. - The contents are written to
/tmp/cornice_secrets/user_<id>_<key>.txt - Permissions are set to
0600 - The environment variable is set to that path
User selection and rotation
The arb loop runs under a single “system” user for now. Configuration options:
arb_user_id(single user)arb_user_ids(comma‑separated list)arb_user_rotate_hours(default 5 hours)
If arb_user_ids is set, the loop deterministically rotates which user is selected every arb_user_rotate_hours.
Appendix: Production setup
1) Database migration
Run:
\i migrations/023_create_arb_account_secrets.sql
2) Configure the master key
Set cornice_kms_key in the server environment. This must be stable for decryption to work.
“Stable” means the value should not change between encrypting and decrypting secrets. If it changes,
all previously stored secrets become unreadable until re‑encrypted with the new key.
Recommended pattern:
- Generate once (for example:
openssl rand -base64 32) - Store in the systemd environment file or unit
- Do not rotate unless you plan a re‑encryption migration
3) Create or identify the system user
Create a cornice user (or choose an existing user id).
sudo adduser --system --group --home /var/lib/cornice --shell /usr/sbin/nologin cornice
4) Store secrets
Use the CLI tool to store encrypted secrets:
python -m app.tools.set_arb_secret \
--env-file /etc/systemd/system/cornice.env \
--user-id 123 \
--venue polymarket \
--secret-key POLY_PRIVATE_KEY \
--value-stdin < /path/to/poly_private_key.txt
python -m app.tools.set_arb_secret \
--env-file /etc/systemd/system/cornice.env \
--user-id 123 \
--venue kalshi \
--secret-key KALSHI_API_KEY \
--value-stdin < /path/to/kalshi_api_key.txt
python -m app.tools.set_arb_secret \
--env-file /etc/systemd/system/cornice.env \
--user-id 123 \
--venue kalshi \
--secret-key KALSHI_PRIVATE_KEY_PATH \
--value-stdin < /path/to/kalshi_private_key.pem
Notes:
--value-stdinavoids shell history.--valueis available but not recommended.- For
*_PRIVATE_KEY_PATH, store file contents; the runtime writes a secure temp file.
5) Enable arb loop
If you’re using systemd, set these in the unit file or its EnvironmentFile:
Environment=arb_enabled=true
Environment=arb_user_id=123
Environment=cornice_kms_key=...
Or rotate:
Environment=arb_user_ids=123,124,125
Environment=arb_user_rotate_hours=5
Then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart cornice
6) Restart service
Restart the backend and verify the arb loop starts without credential errors.