Oct 28, 20266 min read

How to Add Secrets to a Flask App

Managing SECRET_KEY, API keys and webhook signing keys in Flask with Encore, python-dotenv or AWS Secrets Manager, with per-environment values and nothing in git.

A Flask app has at least one secret from the start: SECRET_KEY, which signs the session cookie. Payment keys and webhook signing keys follow soon after. Each needs a value per environment, a path into the app that does not go through git, and code that never prints it.

This guide adds secrets to a Flask app with Encore, python-dotenv and AWS Secrets Manager. The example signs a session cookie, verifies a signed payment webhook and reports whether the payment key is a test or live key without revealing it.

What you need to decide:

  • Where each secret is stored for development, staging and production.
  • How the app reads it, and what rotating it involves.
  • How you keep values out of logs and the repository.

Secrets options for Flask at a glance

CapabilityEncorepython-dotenvAWS Secrets Manager
Declared in codeSecret and SharedSecretVariable namesSecret names
Per environmentencore secret set --typeA file or variables per hostOne secret per environment
Local values.secrets.user.toml.envA dev secret or a fallback
Production storeYour cloud's secret managerWhatever sets the variablesSecrets Manager
Access controlIAM role for the service, provisioned on deployAnyone who can read the envIAM policies you write
Readable by AI coding agentsNo, values are not stored in the repo (except a .secrets.user.toml override)Yes, .env sits in the project directoryNovalues stay in Secrets Manager

Encore: secrets for your existing Flask app

Encore combines an open source infrastructure SDK for TypeScript, Go, Python and Rust with a platform that deploys into your own AWS or GCP account. The Python SDK lets a Flask app declare its secrets at module scope. A SharedSecret has one value for the whole app, and a Secret belongs to one service. Values come from .secrets.user.toml under encore run and from your cloud's secret manager when deployed. It needs Python 3.9 or newer.

import hashlib import hmac import os from encore.secrets import Secret, SharedSecret from flask import Flask, abort, request, session from waitress import serve # Declared once at module scope. Values come from .secrets.user.toml under # `encore run` and from the cloud secret store in deployed environments. flask_secret_key = Secret[str]("FlaskSecretKey") webhook_signing_key = Secret[bytes]("WebhookSigningKey") stripe_key = SharedSecret[str]("StripeKey") app = Flask(__name__) # Flask signs the session cookie with this key. app.config["SECRET_KEY"] = flask_secret_key.value @app.post("/cart/items") def add_to_cart(): session["items"] = session.get("items", 0) + 1 return {"items": session["items"]} @app.get("/billing/mode") def billing_mode(): # Report a fact about the key, never the key itself. mode = "live" if stripe_key.value.startswith("sk_live_") else "test" return {"mode": mode} @app.post("/webhooks/payments") def payment_webhook(): expected = hmac.new(webhook_signing_key.value, request.get_data(), hashlib.sha256).hexdigest() received = request.headers.get("X-Signature", "") if not hmac.compare_digest(expected, received): abort(401) return {"accepted": True} if __name__ == "__main__": host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":") serve(app, host=host or "0.0.0.0", port=int(port))

The webhook reads the raw body with request.get_data(), since the signature covers the exact bytes, and compares with hmac.compare_digest so the check takes the same time for any wrong signature. Local values go in .secrets.user.toml, which stays out of git:

[shared] StripeKey = "sk_test_51abc" [services.web] FlaskSecretKey = "dev-only-3f9c2a7e5b1d4c8a" WebhookSigningKey = "whsec_local_123"

The session cookie survives between requests because it is signed with the secret, and the webhook rejects a wrong signature:

~/web
$ encore run$ curl -c jar -b jar -X POST localhost:4000/cart/items{"items":1}$ curl -c jar -b jar -X POST localhost:4000/cart/items{"items":2}$ BODY='{"id":"evt_1"}'$ SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac whsec_local_123 | awk '{print $2}')$ curl -X POST localhost:4000/webhooks/payments -H "X-Signature: $SIG" -d "$BODY"{"accepted":true}$ curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:4000/webhooks/payments \ -H "X-Signature: bad" -d "$BODY"401$ curl localhost:4000/billing/mode{"mode":"test"}$ encore secret set --type production FlaskSecretKey

On deploy, the values are stored in AWS Secrets Manager or GCP Secret Manager in your own account, and Encore provisions an IAM role for the service. Production, staging and preview environments each get their own values, so a pull request never signs cookies with the production key.

Secret values are not stored in the codebase or in a .env file. Shared values live in Encore's secrets manager and, once deployed, in your cloud's secret manager, so an AI coding agent working in the repository sees which secrets a service declares but never sees the values in the files it works with, and encore secret list never prints a value. The exception is a .secrets.user.toml override, a plaintext file in the app root that Encore adds to .gitignore, which keeps it out of commits but not out of reach of an agent reading the project.

Try Encore →

python-dotenv

The common self-managed setup reads environment variables, with python-dotenv loading a .env file in development:

import os from dotenv import load_dotenv from flask import Flask load_dotenv() # does not override variables that are already set app = Flask(__name__) app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"] WEBHOOK_SIGNING_KEY = os.environ["WEBHOOK_SIGNING_KEY"].encode()

Flask can also load every variable with a prefix through app.config.from_prefixed_env(), so FLASK_SECRET_KEY becomes SECRET_KEY. The Flask configuration docs cover both.

A .env file sits in the project directory, where any AI coding agent or tool with access to the repository can read it, and that is a common way keys end up in prompts, logs or commits.

Good to know

Using os.environ[...] rather than .get makes a missing key fail at startup instead of on the first request. Put .env in .gitignore. In production the values reach the app through the host or task definition, and anyone who can read that configuration can read the keys.

Try python-dotenv →

AWS Secrets Manager

To read secrets directly, use boto3 at startup and keep the values in config:

import boto3 client = boto3.client("secretsmanager") app.config["SECRET_KEY"] = client.get_secret_value(SecretId="web/flask-secret-key")["SecretString"]

Good to know

The app's IAM role needs secretsmanager:GetSecretValue on each secret, and development needs AWS credentials or a fallback. If you rotate SECRET_KEY, keep the previous value in SECRET_KEY_FALLBACKS for a while so signed-in users are not logged out at once. AWS also publishes a caching client for Python that cuts repeated calls.

Try AWS Secrets Manager →

How to choose

Pick Encore when you want secrets declared in the Flask code that uses them, local values in one ignored file, and per-environment values in your cloud's secret manager. Pick python-dotenv for a single deployment where environment variables are already handled well. Pick Secrets Manager directly when you need rotation and audit logs on AWS and are ready to write the IAM and caching.

You can sign up for early access to Encore for Python to try it with your Flask app.

Frequently asked questions

How do I set SECRET_KEY in Flask securely?

Generate a long random value, for example with python -c 'import secrets; print(secrets.token_hex())', keep it out of the code and load it into app.config["SECRET_KEY"] at startup. With Encore, declare flask_secret_key = Secret[str]("FlaskSecretKey"), set it with encore secret set per environment, and assign flask_secret_key.value to the config.

What happens if I change the Flask SECRET_KEY?

Flask signs the session cookie with it, so existing sessions stop validating and users are logged out. Flask 3.1 added SECRET_KEY_FALLBACKS so old keys can still verify sessions during a rotation.

How do I verify a webhook signature in Flask?

Read the raw body with request.get_data(), compute hmac.new(key, body, hashlib.sha256).hexdigest() and compare it with the header using hmac.compare_digest, which runs in constant time.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders