Describe the Bug
The outbound secret filter (secret-filter.mode, added in #7568) only collects secrets from BPMN elements carrying a zeebe:modelerTemplate attribute:
// ProcessDefinitionSecretKeyCache.retrieveOutboundEligibleElementsFromProcess
if (iet.isInstance(element) && isElementTemplate(element)) {
A task without that attribute never enters the map, so getSecretKeys returns Collections.emptyList() and SecretFilter.allowOnly(List.of()) denies every name — as its Javadoc states, "An empty list denies all secrets."
The symptom is a wrong value rather than an error. SecretHandler returns null for a denied name and SecretUtil.resolveSecretValue returns matcher.group(), so the literal {{secrets.MY_SECRET}} is bound as the credential and sent to the target. Users see an authentication failure from the remote system; the only local trace is a DEBUG line.
LAX does not mitigate this. It allows all only when the lookup throws — LazyLoadingSecretFilter does secretNames = names != null ? Set.copyOf(names) : null. An empty list is a successful lookup, so LAX and STRICT behave identically here.
Steps to Reproduce
- Add a service task whose
zeebe:taskDefinition type is a runtime-served connector (e.g. io.camunda:http-json:1) without applying an element template, so it has no zeebe:modelerTemplate attribute — hand-written or generated BPMN, or setting the type directly.
- Reference
{{secrets.MY_SECRET}} from one of its zeebe:input mappings, with a provider that can resolve it.
- Set
camunda.connector.secret-resolver.secret-filter.mode=STRICT (or LAX) and run the process.
Expected Behavior
The secret resolves. Whether a secret is allowed should depend on what the element declares, not on how it was authored — zeebe:modelerTemplate records authoring provenance, not function.
Compare the inbound side: ProcessDefinitionInspector identifies a connector by rawProperties.containsKey(INBOUND_TYPE_KEYWORD), the functional marker, and uses getElementTemplateDetails only for metadata — it has no isElementTemplate predicate. ProcessDefinitionSecretKeyCache is the only place the attribute gates anything, and the // pre-existing, move to util from ProcessDefinitionInspector comment on the copied accessor suggests the predicate was built on top of code that never filtered on it. ProcessDefinitionSecretKeyCacheTest.getSecretKeys_taskWithoutTemplate_excluded_returnsEmptyList pins the current behaviour, but no comment, PR description or issue records why.
Suggested Fix
Drop && isElementTemplate(element), delete the then-unused isElementTemplate and getElementTemplateDetails from that class, and invert the test above.
Safe by construction: the allow-list only ever permits, and it is keyed per element id and built from that element's own input mappings, so scanning more elements cannot admit a secret the model did not declare.
Why now
secret-filter.mode ships DISABLED, so no default deployment is affected. It becomes user-visible once the filter is switched on — planned for SaaS under #8222, with STRICT enabled by default. Fixing this is a prerequisite for that rollout.
Environment
Describe the Bug
The outbound secret filter (
secret-filter.mode, added in #7568) only collects secrets from BPMN elements carrying azeebe:modelerTemplateattribute:A task without that attribute never enters the map, so
getSecretKeysreturnsCollections.emptyList()andSecretFilter.allowOnly(List.of())denies every name — as its Javadoc states, "An empty list denies all secrets."The symptom is a wrong value rather than an error.
SecretHandlerreturnsnullfor a denied name andSecretUtil.resolveSecretValuereturnsmatcher.group(), so the literal{{secrets.MY_SECRET}}is bound as the credential and sent to the target. Users see an authentication failure from the remote system; the only local trace is a DEBUG line.LAXdoes not mitigate this. It allows all only when the lookup throws —LazyLoadingSecretFilterdoessecretNames = names != null ? Set.copyOf(names) : null. An empty list is a successful lookup, soLAXandSTRICTbehave identically here.Steps to Reproduce
zeebe:taskDefinitiontype is a runtime-served connector (e.g.io.camunda:http-json:1) without applying an element template, so it has nozeebe:modelerTemplateattribute — hand-written or generated BPMN, or setting the type directly.{{secrets.MY_SECRET}}from one of itszeebe:inputmappings, with a provider that can resolve it.camunda.connector.secret-resolver.secret-filter.mode=STRICT(orLAX) and run the process.Expected Behavior
The secret resolves. Whether a secret is allowed should depend on what the element declares, not on how it was authored —
zeebe:modelerTemplaterecords authoring provenance, not function.Compare the inbound side:
ProcessDefinitionInspectoridentifies a connector byrawProperties.containsKey(INBOUND_TYPE_KEYWORD), the functional marker, and usesgetElementTemplateDetailsonly for metadata — it has noisElementTemplatepredicate.ProcessDefinitionSecretKeyCacheis the only place the attribute gates anything, and the// pre-existing, move to util from ProcessDefinitionInspectorcomment on the copied accessor suggests the predicate was built on top of code that never filtered on it.ProcessDefinitionSecretKeyCacheTest.getSecretKeys_taskWithoutTemplate_excluded_returnsEmptyListpins the current behaviour, but no comment, PR description or issue records why.Suggested Fix
Drop
&& isElementTemplate(element), delete the then-unusedisElementTemplateandgetElementTemplateDetailsfrom that class, and invert the test above.Safe by construction: the allow-list only ever permits, and it is keyed per element id and built from that element's own input mappings, so scanning more elements cannot admit a secret the model did not declare.
Why now
secret-filter.modeshipsDISABLED, so no default deployment is affected. It becomes user-visible once the filter is switched on — planned for SaaS under #8222, withSTRICTenabled by default. Fixing this is a prerequisite for that rollout.Environment
8.10.0-alpha4; Camunda 8.10camunda.connector.secret-resolver.secret-filter.modeset toSTRICTorLAX