Lab
Flask September 2026 5 min read

The secret key that sat in the code

A fallback value in os.environ.get looks convenient until it turns out the production server is running on exactly that.

A Flask application of mine had a line that looked perfectly normal:

app.secret_key = os.environ.get('SECRET_KEY', 'Softropix-2026-key-final')

It reads like this: take the key from the environment variables, and if it is not there, use this one. Convenient. The application starts anywhere, with nothing extra to configure.

That convenience is precisely the problem.

What this key does

Flask signs sessions with it. When you save something into session, the data goes into the visitor cookie with a signature made by that key alongside it. On the next request Flask checks the signature and knows the contents were not tampered with.

As long as only the server knows the key, the scheme works. If somebody else knows it, they can build a cookie with any contents at all and sign it so that the server believes it.

My session held nothing but the chosen interface language. Nothing to steal. But the moment an account login appeared, anyone with access to the repository could have signed in under somebody else name.

The real problem is not the key

The line with the key can be changed in a minute. The worse part is this: a fallback value hides the truth from you.

The application runs. The logs are clean. The site opens. And you sincerely believe the environment variable on the server is set — otherwise how would any of it work.

It was running on the fallback key. The whole time.

A default value turns the question of whether the environment is configured into a question nobody will ever ask you.

How I found out

The fix is simple — remove the fallback:

app.secret_key = os.environ['SECRET_KEY']

Square brackets instead of get. No variable, and the application falls over at once on startup with a clear error.

I added the variable in the hosting panel, pushed the change, waited for the build and opened the logs:

KeyError: 'SECRET_KEY'
[ERROR] Worker (pid:6) exited with code 3
[ERROR] Reason: Worker failed to boot.

The site went down. And that was the best possible outcome.

The crash meant exactly one thing: the variable really was not on the server. Which is to say that until that moment the production application had been running on a key baked into the source and sitting in an open repository.

This came out only because I took the safety net away. With the old line I would never have known.

Why the variable was not picked up

It turned out I had added it in the panel dialog but never saved the settings form itself. Classic carelessness. I went back, added it again, saved, and started a rebuild:

[INFO] Starting gunicorn 23.0.0
[INFO] Listening at: http://0.0.0.0:8000
[INFO] Booting worker with pid: 13

Not a single error. Now the application is definitely running on the key I set.

How to run it locally

After the fix, running it locally stopped working too — there is no such variable on my machine. Expected, but inconvenient.

The python-dotenv package solves it. You put a .env file in the project root:

SECRET_KEY=test123

And two lines at the top of the application:

from dotenv import load_dotenv
load_dotenv()

Important: the .env holds a test value, not the production key. The real one lives only in the hosting panel. And the file itself must be added to .gitignore, otherwise the whole move was pointless.

What to do with the old key

An important detail that is easy to forget: the key is still in the repository history.

You changed the line and pushed — the key is gone from the current version of the code. But anyone who opens the earlier commits reads it without any trouble.

So the old key counts as compromised for good. Do not move it into the environment variables — generate a new one:

python3 -c "import secrets; print(secrets.token_hex(32))"

Sixty-four characters of random data. Copy it into the hosting panel and save it in your password manager — otherwise you will be generating it again, as happened to me.

The rule that follows

Default values are good for settings that decide nothing: page size, interface language, a timeout. A safety net belongs there.

For anything to do with security the opposite rule applies: let it fall over. A loud error on startup beats quiet work on the wrong data. The first you fix in ten minutes. The second can go unnoticed for years.

If the application can start without a setting that is mandatory, then that setting is not mandatory. Check whether that is really what you wanted.