-
Notifications
You must be signed in to change notification settings - Fork 914
Description
Describe the bug
The additionalExistingSecrets variable templating (described in https://github.com/jenkinsci/helm-charts/blob/main/charts/jenkins/README.md#additional-secrets) does not seem to play nicely with the decodeBase64() functionality described in https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/features/secrets.adoc#additional-variable-substitution.
I am using the following configuration:
controller:
JCasC:
configScripts:
jenkins-casc-configs: |
credentials:
system:
domainCredentials:
- credentials:
- basicSSHUserPrivateKey:
id: "ec2-ssh-key-credential"
description: "jenkins Ed25519 SSH key, with username set to jenkins"
username: "jenkins"
usernameSecret: false
privateKeySource:
directEntry:
privateKey: ${decodeBase64:${jenkins-additional-secrets-ec2-worker-ssh-key-base64}}
scope: GLOBALThe inner variable appears not to be interpreted; certainly, the resulting error looks like this:
java.lang.IllegalArgumentException: Illegal base64 character 24
at java.base/java.util.Base64$Decoder.decode0(Unknown Source)
at java.base/java.util.Base64$Decoder.decode(Unknown Source)
at java.base/java.util.Base64$Decoder.decode(Unknown Source)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.SecretSourceResolver$DecodeBase64Lookup.lookup(SecretSourceResolver.java:204)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.FixedInterpolatorStringLookup.lookup(FixedInterpolatorStringLookup.java:272)
at PluginClassLoader for commons-text-api//org.apache.commons.text.StringSubstitutor.resolveVariable(StringSubstitutor.java:1155)
at PluginClassLoader for commons-text-api//org.apache.commons.text.StringSubstitutor.substitute(StringSubstitutor.java:1521)
at PluginClassLoader for commons-text-api//org.apache.commons.text.StringSubstitutor.substitute(StringSubstitutor.java:1396)
at PluginClassLoader for commons-text-api//org.apache.commons.text.StringSubstitutor.replaceIn(StringSubstitutor.java:1107)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.SecretSourceResolver.resolve(SecretSourceResolver.java:110)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.PrimitiveConfigurator.configure(PrimitiveConfigurator.java:48)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.tryConstructor(DataBoundConfigurator.java:164)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.instance(DataBoundConfigurator.java:75)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.configure(BaseConfigurator.java:274)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.configure(DataBoundConfigurator.java:81)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$doConfigure$16668e2$1(HeteroDescribableConfigurator.java:311)
at PluginClassLoader for configuration-as-code//io.vavr.CheckedFunction0.lambda$unchecked$52349c75$1(CheckedFunction0.java:247)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.doConfigure(HeteroDescribableConfigurator.java:311)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$configure$2(HeteroDescribableConfigurator.java:88)
at PluginClassLoader for configuration-as-code//io.vavr.control.Option.map(Option.java:391)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$configure$3(HeteroDescribableConfigurator.java:88)
at PluginClassLoader for configuration-as-code//io.vavr.Tuple2.apply(Tuple2.java:240)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.configure(HeteroDescribableConfigurator.java:86)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.configure(HeteroDescribableConfigurator.java:57)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.tryConstructor(DataBoundConfigurator.java:164)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.instance(DataBoundConfigurator.java:75)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.configure(BaseConfigurator.java:274)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.configure(DataBoundConfigurator.java:81)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$doConfigure$16668e2$1(HeteroDescribableConfigurator.java:311)
at PluginClassLoader for configuration-as-code//io.vavr.CheckedFunction0.lambda$unchecked$52349c75$1(CheckedFunction0.java:247)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.doConfigure(HeteroDescribableConfigurator.java:311)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$configure$2(HeteroDescribableConfigurator.java:88)
at PluginClassLoader for configuration-as-code//io.vavr.control.Option.map(Option.java:391)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.lambda$configure$3(HeteroDescribableConfigurator.java:88)
at PluginClassLoader for configuration-as-code//io.vavr.Tuple2.apply(Tuple2.java:240)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.configure(HeteroDescribableConfigurator.java:86)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.HeteroDescribableConfigurator.configure(HeteroDescribableConfigurator.java:57)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.tryConstructor(DataBoundConfigurator.java:156)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.instance(DataBoundConfigurator.java:75)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.configure(BaseConfigurator.java:274)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.impl.configurators.DataBoundConfigurator.check(DataBoundConfigurator.java:99)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.configure(BaseConfigurator.java:355)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.check(BaseConfigurator.java:293)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.configure(BaseConfigurator.java:360)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.BaseConfigurator.check(BaseConfigurator.java:293)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.lambda$checkWith$9(ConfigurationAsCode.java:868)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.invokeWith(ConfigurationAsCode.java:811)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.checkWith(ConfigurationAsCode.java:868)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.configureWith(ConfigurationAsCode.java:854)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.configureWith(ConfigurationAsCode.java:733)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.configure(ConfigurationAsCode.java:356)
at PluginClassLoader for configuration-as-code//io.jenkins.plugins.casc.ConfigurationAsCode.init(ConfigurationAsCode.java:345)
Caused: java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at hudson.init.TaskMethodFinder.invoke(TaskMethodFinder.java:109)
Caused: java.lang.Error
at hudson.init.TaskMethodFinder.invoke(TaskMethodFinder.java:115)
at hudson.init.TaskMethodFinder$TaskImpl.run(TaskMethodFinder.java:185)
at org.jvnet.hudson.reactor.Reactor.runTask(Reactor.java:304)
at jenkins.model.Jenkins$5.runTask(Jenkins.java:1149)
at org.jvnet.hudson.reactor.Reactor$2.run(Reactor.java:221)
at org.jvnet.hudson.reactor.Reactor$Node.run(Reactor.java:120)
at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:68)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused: org.jvnet.hudson.reactor.ReactorException
at org.jvnet.hudson.reactor.Reactor.execute(Reactor.java:290)
at jenkins.InitReactorRunner.run(InitReactorRunner.java:49)
at jenkins.model.Jenkins.executeReactor(Jenkins.java:1184)
at jenkins.model.Jenkins.<init>(Jenkins.java:983)
at hudson.model.Hudson.<init>(Hudson.java:102)
at hudson.model.Hudson.<init>(Hudson.java:87)
at hudson.WebAppMain$3.run(WebAppMain.java:249)
Caused: hudson.util.HudsonFailedToLoad
at hudson.WebAppMain$3.run(WebAppMain.java:274)
24 is the hexadecimal ASCII code for the $ character.
From the stack trace, it appears to me that JCasC is running the decodeBase64() functionality, but the inner variable (which I had expected to be populated from the Kubernetes secret) is not getting resolved.
Full details below.
Version of Helm and Kubernetes
Helm:
version.BuildInfo{Version:"v3.16.4", GitCommit:"7877b45b63f95635153b29a42c0c2f4273ec45ca", GitTreeState:"dirty", GoVersion:"go1.23.4"}`
Kubernetes:
Client Version: v1.29.12
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.30.11-eks-bcf3d70
Chart version
jenkins-5.8.43 (latest at the time of issue raise, per https://artifacthub.io/packages/helm/jenkinsci/jenkins)
What happened?
Jenkins failed to start up due to an error in base64 decoding.
What you expected to happen?
Jenkins should have started up successfully.
How to reproduce it
(1.) AWS secret to Kubernetes Secret
I have an AWS secret containing various secrets I need to be mapped into Jenkins credentials, e.g. passwords, SSH keys, etc.
- AWS secrets are stored as JSON blobs, so a secret value must be a single-line string to comply with JSON formatting rules.
- The format of an SSH key (as consumed by some Jenkins credential providers) is a multiline string. To store a multiline string as single-line, we base64-encode it.
- We therefore have a mixture of plaintext and base64-encoded credentials.
- In order to store these in Kubernetes secrets, we use the
kubernetes_secretTerraform provider, per https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret, putting everything indata. - The result is a Kubernetes secret that looks something like this:
apiVersion: v1
data:
ec2-worker-ssh-key-base64: TFMwdExTMUNSVWRKVGlCUFVFVk9VMU5JSUZCU1NWWkJWRVVnUzBWWkxTMHRMUzBLTURGa1pXRmtZbVZsWm1SbFlXUmlaV1ZtWkdWaFpHSmxaV1prWldGa1ltVmxabVJsWVdSaVpXVm1aR1ZoWkdKbFpXWmtaV0ZrWW1WbFptUmxZV1JpWldWbVpHVmhaQW93TW1SbFlXUmlaV1ZtWkdWaFpHSmxaV1prWldGa1ltVmxabVJsWVdSaVpXVm1aR1ZoWkdKbFpXWmtaV0ZrWW1WbFptUmxZV1JpWldWbVpHVmhaR0psWldaa1pXRmtDakF6WkdWaFpHSmxaV1prWldGa1ltVmxabVJsWVdSaVpXVm1aR1ZoWkdKbFpXWmtaV0ZrWW1WbFptUmxZV1JpWldWbVpHVmhaR0psWldaa1pXRmtZbVZsWm1SbFlXUUtNRFJrWldGa1ltVmxabVJsWVdSaVpXVm1aR1ZoWkdKbFpXWmtaV0ZrWW1WbFptUmxZV1JpWldWbVpHVmhaR0psWldaa1pXRmtZbVZsWm1SbFlXUmlaV1ZtWkdWaFpBb3dOV1JsWVdSaVpXVm1aR1ZoWkdKbFpXWmtaV0ZrWW1WbFptUmxZV1JpWldWbVpHVmhaR0psWldaa1pXRmtZbVZsWm1SbFlXUmlaV1ZtWkdWaFpEQXdDaTB0TFMwdFJVNUVJRTlRUlU1VFUwZ2dVRkpKVmtGVVJTQkxSVmt0TFMwdExRbz0K
my-password: aW5zZWN1cmVQYXNzd29yZAo=
kind: SecretA key point here is that ec2-worker-ssh-key-base64 is double-base64-encoded above.
(2.) Kubernetes Secret to Helm chart variables
Per the documentation, I have configuration like this:
controller:
additionalExistingSecrets:
- name: jenkins-additional-secrets
keyName: my-password
- name: jenkins-additional-secrets
keyName: ec2-worker-ssh-key-base64This generates Helm variables named jenkins-additional-secrets-my-password and jenkins-additional-secrets-ec2-worker-ssh-key-base64.
(3.) Helm chart variables to JCasC Credential configuration
controller:
JCasC:
configScripts:
jenkins-casc-configs: |
credentials:
system:
domainCredentials:
- credentials:
- basicSSHUserPrivateKey:
id: "ec2-ssh-key-credential"
description: "jenkins Ed25519 SSH key, with username set to jenkins"
username: "jenkins"
usernameSecret: false
privateKeySource:
directEntry:
privateKey: ${decodeBase64:${jenkins-additional-secrets-ec2-worker-ssh-key-base64}}
scope: GLOBALThen try deploying this, and BOOM.
Without the ${decodeBase64:...} wrapper, secret values are interpolated just fine (see the workaround described below).
Anything else we need to know?
There is a workaround for this issue, that I am currently using: if you change the configuration of the kubernetes_secret Terraform provider to populate binary_data with any secret values that are already base64-encoded, this skips the secondary base64-encoding that happens to values present in data. Therefore, when Jenkins base64-decodes these, they are already in the plain form.
However:
- this is additional complexity to maintain
- it seems that I should be able to avoid this by using the
decodeBase64()functionality described in https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/features/secrets.adoc#additional-variable-substitution - other Jenkins credential types require a base64-encoded input value (e.g.
file, forsecretBytes); it would be nice to be able to clearly handle decoding (or encoding!) where needed in the credential management part of the JCasC section of my Helm values files, instead of being forced to handle it upstream - perhaps a special incantation is needed for this to work? (in which case, others may benefit from reading the replies to the issue, and perhaps from improved documentation!)
- perhaps this "just isn't possible" for some reason? (in which case, I'd like to understand why!)