fix(api): refuse the published JWT signing key

This commit is contained in:
pedrooot
2026-10-07 16:44:52 +02:00
parent 28aed610d2
commit 1898ace69b
5 changed files with 74 additions and 36 deletions
@@ -0,0 +1 @@
API refuses to start with a JWT signing key published in this repository, and the Helm minimal-installation example no longer ships working secrets
+29
View File
@@ -38,6 +38,15 @@ PUBLISHED_ENCRYPTION_KEY_DIGESTS = frozenset(
}
)
# SHA-256 of the base64 body of JWT signing keys that were committed to this
# repository with a working value. Anyone holding one can forge tokens.
PUBLISHED_SIGNING_KEY_DIGESTS = frozenset(
{
# contrib/k8s/helm/prowler-app/examples/minimal-installation/secrets.yaml
"375d5755ab4a7d38c58b38c67c1a6d1cab5afc91844445d8489c15ce7779d949",
}
)
ENCRYPTION_KEY_DOCS = (
"https://docs.prowler.com/getting-started/installation/prowler-app"
"#secrets-encryption-key"
@@ -50,6 +59,16 @@ def _digest(value):
return hashlib.sha256(value.encode()).hexdigest()
def _pem_digest(value):
"""Digest of a PEM's base64 body, so indentation and line wrapping do not matter."""
body = "".join(
line.strip()
for line in value.splitlines()
if line.strip() and "-----" not in line
)
return _digest(body)
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"
@@ -207,6 +226,16 @@ class ApiConfig(AppConfig):
signing_key = env.str(SIGNING_KEY_ENV, default="").strip()
verifying_key = env.str(VERIFYING_KEY_ENV, default="").strip()
if signing_key and _pem_digest(signing_key) in PUBLISHED_SIGNING_KEY_DIGESTS:
raise ImproperlyConfigured(
f"'{SIGNING_KEY_ENV}' is set to a key pair that was published in the "
"Prowler repository and is therefore public. Anyone holding it can "
"forge API tokens. Generate a new pair with 'openssl genrsa -out "
"private.pem 2048' and 'openssl rsa -in private.pem -pubout', set both "
"variables and restart. Existing sessions end and users sign in again; "
"no stored data needs re-encrypting."
)
if not signing_key or not verifying_key:
logger.info(
f"Generating JWT RSA key pair. In production, set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' "
+34
View File
@@ -251,6 +251,40 @@ def test_ensure_secrets_refuses_published_encryption_key(monkeypatch, secrets_di
assert not (secrets_dir / ENCRYPTION_KEY_FILE).exists()
def test_ensure_crypto_keys_refuses_published_signing_key(monkeypatch, tmp_path):
published, verifying = _stub_keys()
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.setattr(api_apps_module, "_keys_initialized", False, raising=False)
monkeypatch.setenv(SIGNING_KEY_ENV, published)
monkeypatch.setenv(VERIFYING_KEY_ENV, verifying)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
monkeypatch.setattr(
api_apps_module,
"PUBLISHED_SIGNING_KEY_DIGESTS",
frozenset({api_apps_module._pem_digest(published)}),
raising=False,
)
with pytest.raises(ImproperlyConfigured) as exc_info:
ApiConfig("api", api_apps_module)._ensure_crypto_keys()
assert SIGNING_KEY_ENV in str(exc_info.value)
assert "PRIVATE" not in str(exc_info.value)
assert not (Path(tmp_path) / PRIVATE_KEY_FILE).exists()
def test_pem_digest_ignores_indentation_and_wrapping():
# derived from the existing stub so no PEM literal is added to this file
flat = _stub_keys()[0]
indented = "".join(f" {line}\n" for line in flat.splitlines())
wrapped = flat.replace("PRIVATE\n", "PRIV\nATE\n", 1)
assert api_apps_module._pem_digest(indented) == api_apps_module._pem_digest(flat)
assert api_apps_module._pem_digest(wrapped) == api_apps_module._pem_digest(flat)
def test_ensure_secrets_generates_encryption_key_when_empty(secrets_dir):
_make_app()._ensure_secrets()
+1 -1
View File
@@ -86,7 +86,7 @@ Before installing the Helm chart, you must create a Kubernetes Secret containing
NEO4J_AUTH: "neo4j/[prowler-password]"
```
> **Note:** You can use the [example secrets file](./examples/minimal-installation/secrets.yaml) as a template, but **always replace the placeholder values with your own secure keys** before applying.
> **Note:** You can use the [example secrets file](./examples/minimal-installation/secrets.yaml) as a template, but **always replace the placeholder values with your own secure keys** before applying. Earlier revisions of that example shipped working values; the API refuses to start with any key that was published in this repository, so a deployment still using one must generate a new one.
3. **Apply the secret to your cluster:**
@@ -4,54 +4,28 @@ type: Opaque
metadata:
name: prowler-secret
stringData:
# Every value below must be generated for your own installation. The values that
# used to ship here were public, and the API refuses to start with them.
# openssl genrsa -out private.pem 2048
DJANGO_TOKEN_SIGNING_KEY: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCIro0QiLAxw7rF
GO0NgAWJfkpYE5ysMGDCbId07HUrv+/SCoRjqKVzGJVIvmNP5oByzSehPgswW9v3
3dqe2r9sCS1JyMa+XO3qfZCR0uRDcPCwZjIyr0QQLpWAymdBa8baeHsU1/3Orjcb
Vrr+lNx4HQJOiSn094iXPReW/25hYeq/SXs79V2CR87PGdoZAhb8IllAxJgdfkeB
/iWohY/1vfRTmIuMweWGXk0aKzPsBdvE/DqG4HjiNVEPh18G3vid0YTZNmm7u8vO
Cue3x9NQWGHA4QtxNtLtxlHcOEryqZ9ChO2nC+ew0Xl/v706XFNyLFicjisIKNQo
qdkaMS33AgMBAAECggEAGdJIChCYoL4mYafk2MEPyrrWFq+V0J3PGcvhB0DInfxD
tT2RZzZsE0NYqIZ3Qpf8OjPxwa9z863W74u1Cn+u3B0bti29BieONteD4VijEO6c
OecEorijth7m1Y7nVN+kkI9kSTrI0yvsczi+WOwMfpCUZ/vXtlSxNEkxVLBqzPCo
9VxAFIjgWOj2rpw8nxPedves36PUrC5ghLqrOTe1jmw/Di0++47AXG+DsTXc00sc
5+oybopm3Kimsxrqbf9s8SZf2A8NiwqcbLj8OtP2j2g4TCEgZYLD5Zmt+JN/wN4B
WsQG/Hwp4KPPm9QTHEpuuoPFP1CZWZeq8gPcV4apYQKBgQC+TuXjJCYhZqNIttTZ
z/i3hkKUEKQLkzTZnXaDzL5wHyEMVqM2E/WkilO0C9ZZwh0ENPzkp+JsHf7LEhHy
wSHOti81VzUCjN/YpCBKlOlClqSiDlOonImrobLei8xgvmA0VmGtirCXZyyzZUoV
OyPr17WpK6G/M5piX59MvKQg0QKBgQC33NBoQFD8A6FjrTopYmWfK099k9uQh9NE
bvUYsNAPunSDslmc/0PPHQC7fRX5Ime2BinXAN1PYtB/Fsu3jv/+FCUM5hVil0Dd
KBvt13+RYSCJKlhcGP1EkWoIg1F2XXBOZKJrC8VQ+Vyl2t06UcWQqy5M9J4VZaqI
fruOLU/URwKBgE55GjJfZZnASPRi78IhD94dbra/ZeWf/dr+IzCV7LEvJOGBmCtk
b5Y5s+o6N1krwetKLj3bPHJ4q+fwu5XuLZKfbTgBjcpPbL5YbzhRzx22IIzye2y7
n8k2FBvQaaY62lC6jeyRk9/am4Qd8D5w9I77k9z+MOQ20yJda8KoxsUBAoGBAIQ9
5QPmppjsf4ry0C9t30uhWhYnX7fPiYviBpVQrwVxBVan076Q9xOjd6BicohzT4bj
XfqPW546o12VZsbKqqLzmEZzwpPb2EJ5E8V4xv8ojb86Xr03GArWUB55XQE2aY1o
4kz99VitUg7UoWPN5ryL8sxU8NLRAdwU0w+K1a0HAoGAZaU7O94u9IIPZ6Ohobs2
Vjf/eV0brCKgX61b4z/YhuJdZsyTujhBZUihZwqR696kiFKuzmHx1ghE2ITvnPVN
q0iHxRZzBCnRQ+mQlS0trzphaCP0NVy3osFeAD9mJfnOnSmkU0ua4F81mkvke1eN
6nnaoAdy2lmMr96/Tye2ty4=
REPLACE WITH THE CONTENTS OF private.pem
-----END PRIVATE KEY-----
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiK6NEIiwMcO6xRjtDYAF
iX5KWBOcrDBgwmyHdOx1K7/v0gqEY6ilcxiVSL5jT+aAcs0noT4LMFvb993antq/
bAktScjGvlzt6n2QkdLkQ3DwsGYyMq9EEC6VgMpnQWvG2nh7FNf9zq43G1a6/pTc
eB0CTokp9PeIlz0Xlv9uYWHqv0l7O/VdgkfOzxnaGQIW/CJZQMSYHX5Hgf4lqIWP
9b30U5iLjMHlhl5NGisz7AXbxPw6huB44jVRD4dfBt74ndGE2TZpu7vLzgrnt8fT
UFhhwOELcTbS7cZR3DhK8qmfQoTtpwvnsNF5f7+9OlxTcixYnI4rCCjUKKnZGjEt
9wIDAQAB
REPLACE WITH THE CONTENTS OF public.pem
-----END PUBLIC KEY-----
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY: "qYAIWnRK52aBT5YQkBoMEw08j7j3+QIPZXS6+A8Su44="
# Protects stored provider, integration and LLM credentials. Changing it later
# leaves the existing ones unreadable, so set it before the first scan.
DJANGO_SECRETS_ENCRYPTION_KEY: ""
# openssl rand -base64 32
AUTH_SECRET: "CM9w3Nco2P1RdHaYmD+fmy2nJmSofusdHd4g7Z4KDG4="
AUTH_SECRET: ""
# Unfortunatelly, we need to duplicate the password in two different keys because the Neo4j Helm Chart expects the password in the NEO4J_AUTH key and the application expects it in the NEO4J_PASSWORD key.
NEO4J_PASSWORD: "prowler-password-fake"