fix(security): lock down signing-key file permissions

Private JWK files were written under the default umask (observed 0664 — group
and world readable). Create the key directory 0700, chmod private key files
(private_jwks.json, token_jwks.json) to 0600 after they are written, and
refuse to start if a pre-existing private key is group/world accessible.

Tests now use an isolated per-test key directory.

Refs: porchlight-91i

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Johan Lundberg 2026-06-08 15:21:27 +02:00
parent cba63280fb
commit c7550cbf09
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
4 changed files with 67 additions and 6 deletions

View file

@ -1,6 +1,9 @@
import os
import shutil
from pathlib import Path
import pytest
from porchlight.config import Settings
from porchlight.oidc.claims import PorchlightUserInfo
from porchlight.oidc.provider import create_oidc_server
@ -53,3 +56,28 @@ def test_create_server_userinfo_is_porchlight() -> None:
assert isinstance(server.context.userinfo, PorchlightUserInfo)
finally:
shutil.rmtree(key_path, ignore_errors=True)
def test_signing_key_files_have_strict_permissions(tmp_path: Path) -> None:
key_path = tmp_path / "keys"
settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path))
create_oidc_server(settings)
assert (key_path.stat().st_mode & 0o077) == 0, "key directory must not be group/world accessible"
for name in ("private_jwks.json", "token_jwks.json"):
f = key_path / name
assert f.exists()
assert (f.stat().st_mode & 0o077) == 0, f"{name} must be 0600"
def test_startup_fails_on_world_readable_private_key(tmp_path: Path) -> None:
key_path = tmp_path / "keys"
key_path.mkdir()
# Simulate a pre-existing private key left group/world readable.
leaked = key_path / "private_jwks.json"
leaked.write_text("{}")
os.chmod(leaked, 0o644)
settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:", signing_key_path=str(key_path))
with pytest.raises(RuntimeError, match="permission"):
create_oidc_server(settings)