Summary
Repo under test: https://github.com/Netflix/lemur
When ADMIN_ONLY_AUTHORITY_CREATION=False (an explicitly supported and documented configuration), POST /api/1/authorities with type=subca never verifies that the caller holds AuthorityPermission on the supplied parent authority. The parent field is resolved by AssociatedAuthoritySchema via a raw fetch_objects(Authority, data) lookup, then passed straight through service.create → mint → cryptography-issuer.create_authority, which loads options["parent"].authority_certificate.private_key and signs a brand-new intermediate CA on the caller's behalf.
Any authenticated non-read-only user can therefore mint a sub-CA chained to any internal root whose private key Lemur holds — including roots they hold no role on — attach a role they already belong to, and immediately issue or offline-sign trusted leaf certificates for arbitrary names.
Affected route
POST /api/1/authorities (with type=subca)
Affected code
Impact
In deployments that set ADMIN_ONLY_AUTHORITY_CREATION=False to enable self-service CA creation, any authenticated non-read-only user — with zero permission on a given internal root CA — can obtain a working intermediate CA chained to that root. They can then:
- Issue TLS certificates for arbitrary names trusted by every relying party that trusts the internal root, bypassing
LEMUR_ALLOWED_DOMAINS, sensitive-domain flags, and the per-user domain-authorization plugin.
- Export the sub-CA private key and sign end-entity certificates entirely outside Lemur, defeating all in-product issuance controls.
This converts "can create a self-contained test CA" into "can mint trusted certs under any internal PKI root in the organisation". The ADMIN_ONLY_AUTHORITY_CREATION documentation does not warn operators of this consequence.
Root cause
AuthoritiesList.post evaluates AuthorityCreatorPermission (a global "may create authorities" flag) and StrictRolePermission, but never evaluates AuthorityPermission(parent.id, parent.roles) against the caller-supplied parent. AssociatedAuthoritySchema is a pure lookup schema with no authz hook, and neither authorities.service.create nor mint re-check before invoking issuer_plugin.create_authority(options). The bundled cryptography-issuer then uses the parent's stored private key directly.
Validated evidence
Static trace, confirmed by code inspection (validation status: CONFIRMED):
parent is loaded via AssociatedAuthoritySchema (raw fetch_objects), passed unchecked through views.post → service.create → mint → plugin.create_authority → issue_certificate, where the parent authority's stored private key is read and used to sign the new intermediate.
- No call to
AuthorityPermission(parent.id, ...) exists anywhere on this path.
- Precondition
ADMIN_ONLY_AUTHORITY_CREATION=False is an explicitly supported config (docs/administration.rst:517).
Proof of concept / reproducer
Status: reconstructed from source report (static control-flow trace; not executed against a live CA).
Preconditions: ADMIN_ONLY_AUTHORITY_CREATION=False; attacker is an authenticated Lemur user holding any role other than read-only; <PARENT_AUTHORITY_ID> is any internal cryptography-issuer root CA the attacker holds no role on; <ATTACKER_ROLE> is any role the attacker already belongs to.
curl -sS -X POST "<TARGET_BASE_URL>/api/1/authorities" \
-H "Authorization: Bearer <AUTH_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "attacker-subca",
"owner": "attacker@example.com",
"description": "poc",
"type": "subca",
"parent": {"id": <PARENT_AUTHORITY_ID>},
"plugin": {"slug": "cryptography-issuer"},
"roles": [{"name": "<ATTACKER_ROLE>"}],
"commonName": "attacker-intermediate",
"validityYears": 1
}'
The response contains a new authority whose authority_certificate is signed by <PARENT_AUTHORITY_ID>'s private key. The caller is recorded as creator and holds <ATTACKER_ROLE> on it, so POST /api/1/certificates against the new authority succeeds (and skips allowed_issuance_for_domain because is_private_authority is true).
Static-trace validation command from the source report:
grep -n 'parent' lemur/authorities/schemas.py lemur/authorities/views.py lemur/authorities/service.py \
&& sed -n '37,55p' lemur/plugins/lemur_cryptography/plugin.py
Source artifact: audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl (run_022, finding cluster lemur-subca-parent-authz, 5/100 runs).
Suggested fix
In AuthoritiesList.post (or authorities.service.create), when data.get('parent') is present, enforce AuthorityPermission(parent.id, [r.name for r in parent.roles]).can() before invoking the issuer plugin, regardless of ADMIN_ONLY_AUTHORITY_CREATION. Additionally, update the ADMIN_ONLY_AUTHORITY_CREATION documentation to state that disabling it currently grants every authenticated user the ability to chain sub-CAs off any internal root whose private key Lemur holds. Consider also requiring admin (or an explicit per-parent capability) for any type=subca creation independent of the global flag.
Summary
Repo under test: https://github.com/Netflix/lemur
When
ADMIN_ONLY_AUTHORITY_CREATION=False(an explicitly supported and documented configuration),POST /api/1/authoritieswithtype=subcanever verifies that the caller holdsAuthorityPermissionon the suppliedparentauthority. Theparentfield is resolved byAssociatedAuthoritySchemavia a rawfetch_objects(Authority, data)lookup, then passed straight throughservice.create → mint → cryptography-issuer.create_authority, which loadsoptions["parent"].authority_certificate.private_keyand signs a brand-new intermediate CA on the caller's behalf.Any authenticated non-read-only user can therefore mint a sub-CA chained to any internal root whose private key Lemur holds — including roots they hold no role on — attach a role they already belong to, and immediately issue or offline-sign trusted leaf certificates for arbitrary names.
Affected route
POST /api/1/authorities(withtype=subca)Affected code
lemur/authorities/views.py:231— gates only onAuthorityCreatorPermission()+StrictRolePermission(); noAuthorityPermissionondata['parent']lemur/authorities/schemas.py:58—parent = fields.Nested(AssociatedAuthoritySchema);validate_subcaonly checks presencelemur/schemas.py:107—AssociatedAuthoritySchema.get_object→fetch_objects(Authority, data)resolves any authority by id/name with no permission checklemur/plugins/lemur_cryptography/plugin.py:40—private_key = options["authority"].authority_certificate.private_key(set fromoptions["parent"]) signs the new intermediatelemur/auth/permissions.py:62—AuthorityCreatorPermissionbecomes always-allow whenADMIN_ONLY_AUTHORITY_CREATION=Falselemur/certificates/views.py:538—is_private_authority(true forcryptography-issuer) skipsUSER_DOMAIN_AUTHORIZATION_PROVIDERfor subsequent leaf issuanceImpact
In deployments that set
ADMIN_ONLY_AUTHORITY_CREATION=Falseto enable self-service CA creation, any authenticated non-read-only user — with zero permission on a given internal root CA — can obtain a working intermediate CA chained to that root. They can then:LEMUR_ALLOWED_DOMAINS, sensitive-domain flags, and the per-user domain-authorization plugin.This converts "can create a self-contained test CA" into "can mint trusted certs under any internal PKI root in the organisation". The
ADMIN_ONLY_AUTHORITY_CREATIONdocumentation does not warn operators of this consequence.Root cause
AuthoritiesList.postevaluatesAuthorityCreatorPermission(a global "may create authorities" flag) andStrictRolePermission, but never evaluatesAuthorityPermission(parent.id, parent.roles)against the caller-suppliedparent.AssociatedAuthoritySchemais a pure lookup schema with no authz hook, and neitherauthorities.service.createnormintre-check before invokingissuer_plugin.create_authority(options). The bundledcryptography-issuerthen uses the parent's stored private key directly.Validated evidence
Static trace, confirmed by code inspection (validation status:
CONFIRMED):parentis loaded viaAssociatedAuthoritySchema(rawfetch_objects), passed unchecked throughviews.post → service.create → mint → plugin.create_authority → issue_certificate, where the parent authority's stored private key is read and used to sign the new intermediate.AuthorityPermission(parent.id, ...)exists anywhere on this path.ADMIN_ONLY_AUTHORITY_CREATION=Falseis an explicitly supported config (docs/administration.rst:517).Proof of concept / reproducer
Status: reconstructed from source report (static control-flow trace; not executed against a live CA).
Preconditions:
ADMIN_ONLY_AUTHORITY_CREATION=False; attacker is an authenticated Lemur user holding any role other thanread-only;<PARENT_AUTHORITY_ID>is any internalcryptography-issuerroot CA the attacker holds no role on;<ATTACKER_ROLE>is any role the attacker already belongs to.The response contains a new authority whose
authority_certificateis signed by<PARENT_AUTHORITY_ID>'s private key. The caller is recorded as creator and holds<ATTACKER_ROLE>on it, soPOST /api/1/certificatesagainst the new authority succeeds (and skipsallowed_issuance_for_domainbecauseis_private_authorityis true).Static-trace validation command from the source report:
Source artifact:
audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl(run_022, finding clusterlemur-subca-parent-authz, 5/100 runs).Suggested fix
In
AuthoritiesList.post(orauthorities.service.create), whendata.get('parent')is present, enforceAuthorityPermission(parent.id, [r.name for r in parent.roles]).can()before invoking the issuer plugin, regardless ofADMIN_ONLY_AUTHORITY_CREATION. Additionally, update theADMIN_ONLY_AUTHORITY_CREATIONdocumentation to state that disabling it currently grants every authenticated user the ability to chain sub-CAs off any internal root whose private key Lemur holds. Consider also requiring admin (or an explicit per-parent capability) for anytype=subcacreation independent of the global flag.