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:
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:
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.
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.
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.
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"]
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.
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.
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.
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.
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.