mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
fix(api): refuse the published JWT signing key
This commit is contained in:
@@ -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
|
||||
@@ -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}' "
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user