Skip to content

Commit ca62751

Browse files
authored
Read the session key from the SESSION_KEY environment variable (#480)
* Read the session key from the SESSION_KEY environment variable The Fernet key that encrypts the session cookie was generated fresh on every start, so each deploy or restart signed everyone out. The key now comes from SESSION_KEY, set in the cabotage UI like the other secrets. When it is missing, the app still generates a random key, which keeps local development working, and logs a warning. * Name SESSION_KEY in the error when its value is not a Fernet key A malformed value used to fail inside EncryptedCookieStorage with a message that did not say which variable was wrong. Also tightens the readme wording and drops an import the tests no longer need. * Point SESSION_KEY errors at the readme instead of embedding the command The generate command now lives in its own readme section under Deploy. The startup error and the missing-key warning just say what is wrong and refer to that section. * Assert the readme pointer in the session key tests Without this the tests stayed green when the pointer was removed.
1 parent 3e07819 commit ca62751

3 files changed

Lines changed: 80 additions & 4 deletions

File tree

‎blurb_it/__main__.py‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import base64
44
import json
5+
import logging
56
import os
67

78
import aiohttp
@@ -308,14 +309,36 @@ async def handle_add_blurb_post(request: Request) -> Response:
308309
raise web.HTTPFound(location=request.app.router["add_blurb"].url_for())
309310

310311

311-
if __name__ == "__main__": # pragma: no cover
312-
fernet_key = fernet.Fernet.generate_key()
313-
secret_key = base64.urlsafe_b64decode(fernet_key)
312+
def get_session_key() -> str:
313+
"""Return the Fernet key that encrypts the session cookie.
314+
315+
The key comes from the ``SESSION_KEY`` environment variable so that it
316+
stays the same across restarts and deploys. Without it a random key is
317+
generated, which is fine for local development, but then every restart
318+
invalidates all existing sessions and signs everyone out.
319+
"""
320+
session_key = os.environ.get("SESSION_KEY")
321+
if session_key:
322+
try:
323+
fernet.Fernet(session_key)
324+
except ValueError as exc:
325+
raise ValueError(
326+
"SESSION_KEY is not a valid session key. "
327+
"See the Deploy section of the readme."
328+
) from exc
329+
return session_key
330+
logging.warning(
331+
"SESSION_KEY is not set; using a random session key, so sessions "
332+
"will not survive a restart. See the Deploy section of the readme."
333+
)
334+
return fernet.Fernet.generate_key().decode()
335+
314336

337+
if __name__ == "__main__": # pragma: no cover
315338
app = web.Application(
316339
middlewares=[
317340
middleware.error_middleware,
318-
session_middleware(EncryptedCookieStorage(secret_key)),
341+
session_middleware(EncryptedCookieStorage(get_session_key())),
319342
]
320343
)
321344

‎readme.rst‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,25 @@ Configuration variables, set in the cabotage UI:
6161
- ``GH_APP_ID``, ``GH_PRIVATE_KEY``: the GitHub App ID and private key.
6262
- ``APP_URL``: the public hostname, without scheme. Must match the OAuth App
6363
callback URL.
64+
- ``SESSION_KEY``: the key that encrypts the session cookie. See
65+
`Generating the session key`_ below.
6466
- ``SENTRY_DSN``: where errors are reported.
6567

68+
Generating the session key
69+
~~~~~~~~~~~~~~~~~~~~~~~~~~
70+
71+
``SESSION_KEY`` is a Fernet key. Generate it once, in any environment that
72+
has the ``cryptography`` package installed:
73+
74+
.. code-block:: console
75+
76+
$ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
77+
78+
Copy the whole printed line into ``SESSION_KEY``. Keep it the same across
79+
deploys: changing it signs everyone out. Without it the app generates a
80+
random key at startup, with the same effect on every restart. If the value is
81+
not a valid Fernet key, the app refuses to start.
82+
6683

6784
Requirements and dependencies
6885
=============================

‎tests/test_main.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import asyncio
44
import base64
5+
import logging
56
import pathlib
67

78
import aiohttp
@@ -613,3 +614,38 @@ async def get_installation(gh, jwt, username):
613614
# Assert
614615
assert resp.status == 503
615616
assert "GitHub is not responding right now" in await resp.text()
617+
618+
619+
def test_get_session_key_from_env(monkeypatch, caplog) -> None:
620+
expected = fernet.Fernet.generate_key().decode()
621+
monkeypatch.setenv("SESSION_KEY", expected)
622+
623+
session_key = __main__.get_session_key()
624+
625+
assert session_key == expected
626+
assert "SESSION_KEY is not set" not in caplog.text
627+
# The key must be usable as-is by the cookie storage.
628+
EncryptedCookieStorage(session_key)
629+
630+
631+
def test_get_session_key_random_when_unset(monkeypatch, caplog) -> None:
632+
monkeypatch.delenv("SESSION_KEY", raising=False)
633+
634+
with caplog.at_level(logging.WARNING):
635+
session_key = __main__.get_session_key()
636+
637+
assert session_key != __main__.get_session_key()
638+
assert "SESSION_KEY is not set" in caplog.text
639+
assert "See the Deploy section of the readme." in caplog.text
640+
EncryptedCookieStorage(session_key)
641+
642+
643+
def test_get_session_key_rejects_invalid_value(monkeypatch) -> None:
644+
monkeypatch.setenv("SESSION_KEY", "not-a-fernet-key")
645+
646+
with pytest.raises(
647+
ValueError,
648+
match=r"SESSION_KEY is not a valid session key\. "
649+
r"See the Deploy section of the readme\.",
650+
):
651+
__main__.get_session_key()

0 commit comments

Comments
 (0)