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
This commit is contained in:
parent
c4d952430e
commit
279a28515f
@ -1,18 +1,63 @@
|
|||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
YK_ROOT_SERIAL = "35450561"
|
_YK_ROOT_SERIAL = "35450561"
|
||||||
YK_ROOT_PIN = os.environ.get("YK_ROOT_PIN", "CHANGE_ME_YK1_PIN")
|
_YK_INT_SERIAL = "33930436"
|
||||||
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"
|
||||||
ROOT_CA_PATH = "/etc/ssl/ca/root/root-ca.crt"
|
_INT_CA_PATH = "/etc/ssl/ca/intermediate/intermediate-ca.crt"
|
||||||
INT_CA_PATH = "/etc/ssl/ca/intermediate/intermediate-ca.crt"
|
_CA_CHAIN_PATH = "/etc/ssl/ca/ca-chain.crt"
|
||||||
CA_CHAIN_PATH = "/etc/ssl/ca/ca-chain.crt"
|
_ISSUED_DIR = "/etc/ssl/ca/issued"
|
||||||
ISSUED_DIR = "/etc/ssl/ca/issued"
|
_DB_PATH = "/var/lib/certauth/certauth.db"
|
||||||
DB_PATH = "/var/lib/certauth/certauth.db"
|
_JWT_SECRET_FILE = "/var/lib/certauth/.jwt_secret"
|
||||||
SECRET_KEY = os.environ.get("JWT_SECRET", "CHANGE_ME_JWT_SECRET")
|
_ALGORITHM = "HS256"
|
||||||
ALGORITHM = "HS256"
|
_ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
_ADMIN_USERNAME = "certauth"
|
||||||
ADMIN_USERNAME = "certauth"
|
_PKCS11_MODULE = "/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so"
|
||||||
PKCS11_MODULE = "/usr/lib/aarch64-linux-gnu/opensc-pkcs11.so"
|
_YK_PUB_ROOT = "/tmp/yk1-root-pub.pem"
|
||||||
YK_PUB_ROOT = "/tmp/yk1-root-pub.pem"
|
_YK_PUB_INT = "/tmp/yk2-int-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")
|
||||||
|
|||||||
66
tests/test_config.py
Normal file
66
tests/test_config.py
Normal file
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user