From 279a28515fdd6ad84e043dc3c501730e2ebf7dd8 Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Sat, 4 Jul 2026 04:48:30 +0000 Subject: [PATCH] fix: generate JWT secret on first boot instead of hardcoded default (#19) Generate random 256-bit secret stored in /var/lib/certauth/.jwt_secret (mode 0600) ENV JWT_SECRET takes precedence. Remove CHANGE_ME_JWT_SECRET default. Add tests for secret generation and env override --- api/config.py | 77 +++++++++++++++++++++++++++++++++++--------- tests/test_config.py | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 tests/test_config.py diff --git a/api/config.py b/api/config.py index 6c1d6eb..5437c83 100644 --- a/api/config.py +++ b/api/config.py @@ -1,18 +1,63 @@ import os +import secrets -YK_ROOT_SERIAL = "35450561" -YK_ROOT_PIN = os.environ.get("YK_ROOT_PIN", "CHANGE_ME_YK1_PIN") -YK_INT_SERIAL = "33930436" -YK_INT_PIN = os.environ.get("YK_INT_PIN", "CHANGE_ME_YK2_PIN") -ROOT_CA_PATH = "/etc/ssl/ca/root/root-ca.crt" -INT_CA_PATH = "/etc/ssl/ca/intermediate/intermediate-ca.crt" -CA_CHAIN_PATH = "/etc/ssl/ca/ca-chain.crt" -ISSUED_DIR = "/etc/ssl/ca/issued" -DB_PATH = "/var/lib/certauth/certauth.db" -SECRET_KEY = os.environ.get("JWT_SECRET", "CHANGE_ME_JWT_SECRET") -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 60 -ADMIN_USERNAME = "certauth" -PKCS11_MODULE = "/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so" -YK_PUB_ROOT = "/tmp/yk1-root-pub.pem" -YK_PUB_INT = "/tmp/yk2-int-pub.pem" +_YK_ROOT_SERIAL = "35450561" +_YK_INT_SERIAL = "33930436" + +_ROOT_CA_PATH = "/etc/ssl/ca/root/root-ca.crt" +_INT_CA_PATH = "/etc/ssl/ca/intermediate/intermediate-ca.crt" +_CA_CHAIN_PATH = "/etc/ssl/ca/ca-chain.crt" +_ISSUED_DIR = "/etc/ssl/ca/issued" +_DB_PATH = "/var/lib/certauth/certauth.db" +_JWT_SECRET_FILE = "/var/lib/certauth/.jwt_secret" +_ALGORITHM = "HS256" +_ACCESS_TOKEN_EXPIRE_MINUTES = 60 +_ADMIN_USERNAME = "certauth" +_PKCS11_MODULE = "/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so" +_YK_PUB_ROOT = "/tmp/yk1-root-pub.pem" +_YK_PUB_INT = "/tmp/yk2-int-pub.pem" +_TMP_DIR = "/var/lib/certauth/tmp" + + +def _get_jwt_secret() -> str: + """Return JWT secret from env, persisted file, or generate new one.""" + env_secret = os.environ.get("JWT_SECRET") + if env_secret: + return env_secret + if os.path.exists(_JWT_SECRET_FILE): + with open(_JWT_SECRET_FILE) as f: + return f.read().strip() + secret = secrets.token_hex(32) + os.makedirs(os.path.dirname(_JWT_SECRET_FILE), exist_ok=True) + fd = os.open(_JWT_SECRET_FILE, os.O_WRONLY | os.O_CREAT, 0o600) + with os.fdopen(fd, "w") as f: + f.write(secret) + return secret + + +def _get_pin(env_var: str) -> str: + """Require YubiKey PIN from environment — no default allowed.""" + pin = os.environ.get(env_var) + if not pin: + raise RuntimeError(f"Missing required environment variable: {env_var}") + return pin + + +YK_ROOT_SERIAL = _YK_ROOT_SERIAL +YK_INT_SERIAL = _YK_INT_SERIAL +ROOT_CA_PATH = _ROOT_CA_PATH +INT_CA_PATH = _INT_CA_PATH +CA_CHAIN_PATH = _CA_CHAIN_PATH +ISSUED_DIR = _ISSUED_DIR +DB_PATH = _DB_PATH +SECRET_KEY = _get_jwt_secret() +ALGORITHM = _ALGORITHM +ACCESS_TOKEN_EXPIRE_MINUTES = _ACCESS_TOKEN_EXPIRE_MINUTES +ADMIN_USERNAME = _ADMIN_USERNAME +PKCS11_MODULE = _PKCS11_MODULE +YK_PUB_ROOT = _YK_PUB_ROOT +YK_PUB_INT = _YK_PUB_INT +TMP_DIR = _TMP_DIR + +YK_ROOT_PIN = _get_pin("YK_ROOT_PIN") +YK_INT_PIN = _get_pin("YK_INT_PIN") diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..7178d3c --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,66 @@ +import os +import sys +import tempfile +import unittest + + +class TestConfigSecurity(unittest.TestCase): + + def test_jwt_secret_generates_new(self): + with tempfile.NamedTemporaryFile(suffix=".jwt_secret", delete=False) as f: + secret_path = f.name + os.unlink(secret_path) + try: + os.environ.pop("JWT_SECRET", None) + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + import importlib + if "config" in sys.modules: + del sys.modules["config"] + os.environ["_JWT_SECRET_FILE"] = secret_path + cfg = __import__("config") + self.assertIsNotNone(cfg.SECRET_KEY) + self.assertEqual(len(cfg.SECRET_KEY), 64) + with open(secret_path) as sf: + self.assertEqual(sf.read().strip(), cfg.SECRET_KEY) + finally: + os.environ.pop("JWT_SECRET", None) + os.environ.pop("_JWT_SECRET_FILE", None) + if os.path.exists(secret_path): + os.unlink(secret_path) + if "config" in sys.modules: + del sys.modules["config"] + + def test_jwt_secret_env_override(self): + os.environ["JWT_SECRET"] = "test-secret-from-env" + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + if "config" in sys.modules: + del sys.modules["config"] + cfg = __import__("config") + self.assertEqual(cfg.SECRET_KEY, "test-secret-from-env") + os.environ.pop("JWT_SECRET", None) + if "config" in sys.modules: + del sys.modules["config"] + + def test_yk_pin_requires_env(self): + os.environ.pop("YK_ROOT_PIN", None) + os.environ.pop("YK_INT_PIN", None) + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + if "config" in sys.modules: + del sys.modules["config"] + with self.assertRaises(RuntimeError): + __import__("config") + os.environ["YK_ROOT_PIN"] = "test1234" + os.environ["YK_INT_PIN"] = "test5678" + if "config" in sys.modules: + del sys.modules["config"] + cfg = __import__("config") + self.assertEqual(cfg.YK_ROOT_PIN, "test1234") + self.assertEqual(cfg.YK_INT_PIN, "test5678") + os.environ.pop("YK_ROOT_PIN", None) + os.environ.pop("YK_INT_PIN", None) + if "config" in sys.modules: + del sys.modules["config"] + + +if __name__ == "__main__": + unittest.main()