Skip to content

Commit fc1796c

Browse files
somiljain2006timja
andauthored
Add strict mode to fail on unresolved secrets in SecretSourceResolver (#2814)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent c08919f commit fc1796c

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

docs/features/secrets.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,17 @@ credentials:
342342

343343
```
344344

345+
=== Strict Secret Resolution
346+
347+
By default, if a secret variable cannot be resolved, JCasC will log a warning and replace the variable with an empty string. If you are using a remote secrets engine, this can result in credentials being unexpectedly overwritten with empty values during a configuration reload.
348+
349+
To prevent this, you can enable **Strict Secret Resolution**. When enabled, any unresolved secret that does not have a defined default value (using the `:-` operator) will cause the configuration reload to abort entirely, leaving the previous working configuration intact.
350+
351+
You can enable this by setting the following environment variable or Java system property:
352+
353+
* **Environment Variable:** `CASC_STRICT_SECRET_RESOLUTION=true`
354+
* **System Property:** `-Dcasc.strict.secret.resolution=true`
355+
345356
== Useful links
346357

347358
* link:https://jenkins.io/doc/developer/security/secrets/[Jenkins Developer Guide: Storing Secrets in Jenkins]

plugin/src/main/java/io/jenkins/plugins/casc/SecretSourceResolver.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,21 @@ static class UnresolvedLookup implements StringLookup {
115115

116116
static final UnresolvedLookup INSTANCE = new UnresolvedLookup();
117117

118+
private static final String STRICT_MODE_ENV = "CASC_STRICT_SECRET_RESOLUTION";
119+
private static final String STRICT_MODE_PROP = "casc.strict.secret.resolution";
120+
118121
private UnresolvedLookup() {}
119122

120123
@Override
121124
public String lookup(String key) {
125+
boolean isStrict =
126+
Boolean.parseBoolean(System.getProperty(STRICT_MODE_PROP, System.getenv(STRICT_MODE_ENV)));
127+
128+
if (isStrict) {
129+
throw new IllegalStateException(
130+
String.format("Unable to resolve variable '%s'. Aborting configuration reload.", key));
131+
}
132+
122133
LOGGER.log(
123134
Level.WARNING,
124135
String.format(

plugin/src/test/java/io/jenkins/plugins/casc/SecretSourceResolverTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.hamcrest.CoreMatchers.equalTo;
55
import static org.hamcrest.CoreMatchers.startsWith;
66
import static org.hamcrest.MatcherAssert.assertThat;
7+
import static org.junit.Assert.assertThrows;
78
import static org.junit.Assert.assertTrue;
89

910
import io.jenkins.plugins.casc.SecretSourceResolver.Base64Lookup;
@@ -521,4 +522,58 @@ public void resolve_trimWithSpacesOnly_becomesEmpty() {
521522
environment.set("FOO", " ");
522523
assertThat(resolve("${trim:${FOO}}"), equalTo(""));
523524
}
525+
526+
@Test
527+
public void resolve_strictModeEnvVar_throwsExceptionOnMissingVar() {
528+
environment.set("CASC_STRICT_SECRET_RESOLUTION", "true");
529+
530+
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
531+
resolve("${MISSING_SECRET_VAR}");
532+
});
533+
534+
assertThat(exception.getMessage(), containsString("MISSING_SECRET_VAR"));
535+
}
536+
537+
@Test
538+
public void resolve_strictModeSysProp_throwsExceptionOnMissingVar() {
539+
System.setProperty("casc.strict.secret.resolution", "true");
540+
try {
541+
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
542+
resolve("${ANOTHER_MISSING_VAR}");
543+
});
544+
assertThat(exception.getMessage(), containsString("ANOTHER_MISSING_VAR"));
545+
} finally {
546+
System.clearProperty("casc.strict.secret.resolution");
547+
}
548+
}
549+
550+
@Test
551+
public void resolve_strictMode_ignoresExceptionIfDefaultProvided() {
552+
environment.set("CASC_STRICT_SECRET_RESOLUTION", "true");
553+
554+
String output = resolve("${MISSING_SECRET_VAR:-my_fallback_value}");
555+
556+
assertThat(output, equalTo("my_fallback_value"));
557+
}
558+
559+
@Test
560+
public void resolve_strictModeSetToFalse_defaultsToEmptyString() {
561+
environment.set("CASC_STRICT_SECRET_RESOLUTION", "false");
562+
563+
String output = resolve("${MISSING_SECRET_VAR}");
564+
565+
assertThat(output, equalTo(""));
566+
assertTrue(logContains("Configuration import: Found unresolved variable 'MISSING_SECRET_VAR'"));
567+
}
568+
569+
@Test
570+
public void resolve_strictMode_multipleVariables_oneMissing_shouldFail() {
571+
environment.set("CASC_STRICT_SECRET_RESOLUTION", "true");
572+
environment.set("FOO", "hello");
573+
574+
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
575+
resolve("${FOO}:${MISSING}");
576+
});
577+
assertThat(exception.getMessage(), containsString("MISSING"));
578+
}
524579
}

0 commit comments

Comments
 (0)