Skip to content

Commit 410c3ae

Browse files
jetersenv1v
andauthored
SecretResolver support file and base64 variable expansion (#1408)
Co-authored-by: Victor Martinez <[email protected]>
1 parent fedfc22 commit 410c3ae

File tree

23 files changed

+684
-141
lines changed

23 files changed

+684
-141
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
* text=auto eol=lf
2+
*.jks binary
3+
*.p12 binary

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ plugins.txt
2020
# Ignore DS_Store (Mac)
2121
.DS_Store
2222

23+
# Ignore benchmark reports
24+
jmh-report*.json

demos/artifactory/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ unclassified:
1515
credentialsId: "artifactory"
1616
resolverCredentialsConfig:
1717
username: artifactory_user
18-
password: ${ARTIFACTORY_PASSWORD}
18+
password: "${ARTIFACTORY_PASSWORD}"
1919
```
2020
2121
## implementation note

demos/credentials/README.md

+70-25
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
Requires `credentials` >= 2.2.0
44

5-
## sample configuration
5+
All values with `"${SOME_SECRET}"` is resolved by our Secret Sources Resolver you can [read more about which sources are supported](../../docs/features/secrets.adoc#secret-sources)
6+
7+
Since JCasC version v1.42 we have added support for variable expansion for `base64`, `readFileBase64` and `readFile`
8+
[Read more about the syntax](../../docs/features/secrets.adoc#passing-secrets-through-variables)
9+
10+
You can also see an [example below](#example)
11+
12+
## Sample Configuration
613

714
```yaml
815
credentials:
@@ -19,33 +26,15 @@ credentials:
1926
scope: SYSTEM
2027
id: sudo_password
2128
username: root
22-
password: ${SUDO_PASSWORD}
29+
password: "${SUDO_PASSWORD}"
2330
```
2431
25-
## implementation note
26-
27-
Credentials plugin support relies on a custom adaptor components `CredentialsRootConfigurator` and `SystemCredentialsProviderConfigurator`.
28-
29-
Global credentials can be registered by just not providing a domain (i.e `null`).
30-
31-
Credentials symbol name is inferred from implementation class simple name: `UsernamePasswordCredentialsImpl`
32-
descriptor's clazz is `Credentials`
33-
we consider the `Impl` suffix as a common pattern to flag implementation class.
34-
=> symbol name is `usernamePassword`
35-
36-
## Examples
37-
38-
A list of some of the more common credentials.
39-
40-
### SSH Credentials
41-
42-
Requires `ssh-credentials` >= 1.16
43-
44-
Example that uses the [SSH credentials plugin](https://plugins.jenkins.io/ssh-credentials).
45-
46-
As of version 1.14, it is no longer possible to load a ssh key from a file. It has been deprecated due to [CVE-2018-1000601](https://jenkins.io/security/advisory/2018-06-25/#SECURITY-440).
32+
## Example
4733
4834
```yaml
35+
jenkins:
36+
systemMessage: "Example of configuring credentials in Jenkins"
37+
4938
credentials:
5039
system:
5140
domainCredentials:
@@ -58,5 +47,61 @@ credentials:
5847
description: "SSH passphrase with private key file. Private key provided"
5948
privateKeySource:
6049
directEntry:
61-
privateKey: ${SSH_PRIVATE_KEY}
50+
privateKey: "${SSH_PRIVATE_KEY}"
51+
# Another option passing via a file via ${readFile:/path/to/file}
52+
- basicSSHUserPrivateKey:
53+
scope: SYSTEM
54+
id: ssh_with_passphrase_provided_via_file
55+
username: ssh_root
56+
passphrase: "${SSH_KEY_PASSWORD}"
57+
description: "SSH passphrase with private key file. Private key provided"
58+
privateKeySource:
59+
directEntry:
60+
privateKey: "${readFile:${SSH_PRIVATE_FILE_PATH}}" # Path to file loaded from Environment Variable
61+
- usernamePassword:
62+
scope: GLOBAL
63+
id: "username"
64+
username: "some-user"
65+
password: "${SOME_USER_PASSWORD}"
66+
description: "Username/Password Credentials for some-user"
67+
- string:
68+
scope: GLOBAL
69+
id: "secret-text"
70+
secret: "${SECRET_TEXT}"
71+
description: "Secret Text"
72+
- aws:
73+
scope: GLOBAL
74+
id: "AWS"
75+
accessKey: "${AWS_ACCESS_KEY}"
76+
secretKey: "${AWS_SECRET_ACCESS_KEY}"
77+
description: "AWS Credentials"
78+
- file:
79+
scope: GLOBAL
80+
id: "secret-file"
81+
fileName: "mysecretfile.txt"
82+
secretBytes: "${base64:${readFile:${SECRET_FILE_PATH}}}" # secretBytes requires base64 encoded content
83+
- file:
84+
scope: GLOBAL
85+
id: "secret-file_via_binary_file"
86+
fileName: "mysecretfile.txt"
87+
secretBytes: "${readFileBase64:${SECRET_FILE_PATH}}" # secretBytes requires base64 encoded content
88+
- certificate:
89+
scope: GLOBAL
90+
id: "secret-certificate"
91+
password: "${SECRET_PASSWORD_CERT}"
92+
description: "my secret cert"
93+
keyStoreSource:
94+
uploaded:
95+
uploadedKeystore: "${readFileBase64:${SECRET_CERT_FILE_PATH}}" # uploadedKeystore requires BINARY base64 encoded content
6296
```
97+
98+
## implementation note
99+
100+
Credentials plugin support relies on a custom adaptor components `CredentialsRootConfigurator` and `SystemCredentialsProviderConfigurator`.
101+
102+
Global credentials can be registered by just not providing a domain (i.e `null`).
103+
104+
Credentials symbol name is inferred from implementation class simple name: `UsernamePasswordCredentialsImpl`
105+
descriptor's clazz is `Credentials`
106+
we consider the `Impl` suffix as a common pattern to flag implementation class.
107+
=> symbol name is `usernamePassword`

demos/credentials/credentials.yaml

-46
This file was deleted.

demos/jenkins/jenkins.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ unclassified:
5858
artifactoryUrl: http://acme.com/artifactory
5959
resolverCredentialsConfig:
6060
username: artifactory_user
61-
password: ${ARTIFACTORY_PASSWORD}
61+
password: "${ARTIFACTORY_PASSWORD}"
6262

6363
globalLibraries:
6464
libraries:

demos/ldap/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jenkins:
1010
- server: ldap.acme.com
1111
rootDN: dc=acme,dc=fr
1212
managerDN: "manager"
13-
managerPasswordSecret: ${LDAP_PASSWORD}
13+
managerPasswordSecret: "${LDAP_PASSWORD}"
1414
userSearch: "(&(objectCategory=User)(sAMAccountName={0}))"
1515
groupSearchFilter: "(&(cn={0})(objectclass=group))"
1616
groupMembershipStrategy:

docs/features/secrets.adoc

+13
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ For example, `key: "${VALUE:-defaultvalue}"` will evaluate to `defaultvalue` if
4848
To escape a string from secret interpolation, put `^` in front of the value.
4949
For example, `Jenkins: "^${some_var}"` will produce the literal `Jenkins: "${some_var}"`.
5050

51+
=== Additional variable substitution
52+
53+
Currently we have `base64`, `readFile` and `readFileBase64` all serving a different purpose.
54+
`readFileBase64` is useful for credential plugin or any other plugin that needs binary base64 encoding of a file, in this specific case that would be useful for link:https://tools.ietf.org/html/rfc7292[a PKCS#12 certificate file].
55+
56+
- `${base64:HELLO WORLD}` into `SEVMTE8gV09STEQ=`
57+
- `${readFile:/secret/file.txt}` into the content of a file.
58+
- `${base64:${readFile:/secret/file.txt}` into a base64 representation of the content in a file
59+
- `${base64:${readFile:${SECRET_FILE_PATH}}` nest it all together with regular secret expansion.
60+
- `${readFileBase64:/secret/certificate.p12}` into a base64 representation of the binary file.
61+
- `${readFile:/secret/file.txt}` an alias for readFile
62+
- `${fileBase64:/secret/certificate.p12}` an alias for readFileBase64
63+
5164
=== Security and compatibility considerations
5265

5366
// TODO(oleg_nenashev): Add a link to the advisory once ready

integrations/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@
547547
<dependency>
548548
<groupId>org.apache.commons</groupId>
549549
<artifactId>commons-text</artifactId>
550-
<version>1.7</version>
550+
<version>1.8</version>
551551
</dependency>
552552
<dependency>
553553
<groupId>io.netty</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package io.jenkins.plugins.casc;
2+
3+
import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
4+
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
5+
import com.cloudbees.plugins.credentials.Credentials;
6+
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
7+
import com.cloudbees.plugins.credentials.CredentialsProvider;
8+
import com.cloudbees.plugins.credentials.CredentialsScope;
9+
import com.cloudbees.plugins.credentials.SecretBytes;
10+
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
11+
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
12+
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;
13+
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl.UploadedKeyStoreSource;
14+
import io.jenkins.plugins.casc.misc.ConfiguredWithReadme;
15+
import io.jenkins.plugins.casc.misc.Env;
16+
import io.jenkins.plugins.casc.misc.EnvVarsRule;
17+
import io.jenkins.plugins.casc.misc.Envs;
18+
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithReadmeRule;
19+
import java.nio.charset.StandardCharsets;
20+
import java.nio.file.Files;
21+
import java.nio.file.Paths;
22+
import java.util.Base64;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import jenkins.model.Jenkins;
26+
import org.apache.commons.io.IOUtils;
27+
import org.jenkinsci.plugins.plaincredentials.FileCredentials;
28+
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
29+
import org.junit.Rule;
30+
import org.junit.Test;
31+
import org.junit.rules.RuleChain;
32+
33+
import static org.hamcrest.MatcherAssert.assertThat;
34+
import static org.hamcrest.Matchers.anyOf;
35+
import static org.hamcrest.Matchers.containsString;
36+
import static org.hamcrest.Matchers.equalTo;
37+
import static org.hamcrest.Matchers.hasSize;
38+
import static org.hamcrest.Matchers.is;
39+
import static org.jvnet.hudson.test.JenkinsMatchers.hasPlainText;
40+
41+
public class CredentialsReadmeTest {
42+
43+
public static final String PASSPHRASE = "passphrase";
44+
public static final String PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n"
45+
+ "MIICXgIBAAKBgQC2xOoDBS+RQiwYN+rY0YXYQ/WwmC9ICH7BCXfLUBSHAkF2Dvd0\n"
46+
+ "cvM2Ph2nOPiHdntrvD8JkzIv+S1RIqlBrzK6NRQ0ojoCvyawzY3cgzfQ4dfaOqUF\n"
47+
+ "2bn4PscioLlq+Pbi3KYYwWUFue2iagRIxp0+/3F5WqOWPPy1twW7ddWPLQIDAQAB\n"
48+
+ "AoGBAKOX7DKZ4LLvfRKMcpxyJpCme/L+tUuPtw1IUT7dxhH2deubh+lmvsXtoZM9\n"
49+
+ "jk+KQPz0+aOzanfAXMzD7qZJkGfQ91aG8OiD+YJnRqOc6C6vQBXiZgeHRqWH0VMG\n"
50+
+ "rp9Xqd8MxEYScaJYMwoHiBCG/cb3c4kpEpZ03IzkekZdXlmhAkEA7iFEk5k1BZ1+\n"
51+
+ "BnKN9LxLR0EOKoSFJjxBihRP6+UD9BF+/1AlKlLW4hSq4458ppV5Wt4glHTcAQi/\n"
52+
+ "U+wOOz6DyQJBAMR8G0yjtmLjMBy870GaDxmwWjqSeYwPoHbvRTOml8Fz9fP4gBMi\n"
53+
+ "PUEGJaEHMuPECIegZ93kwAGBT51Q7AZcukUCQGGmnNOWISsjUXndYh85U/ltURzY\n"
54+
+ "aS2rygiQmdGXgY6F2jliqUr424ushAN6+9zoMPK1YlDetxVpe+QzSga7dRkCQQCB\n"
55+
+ "+DI6rORdXziZGeUNuPGaJYxZyEA8hK25Xqag9ubVYXZlLpDRl0l7dKx5awCfpzGZ\n"
56+
+ "PWLXZZQYqsfWIQwvXTEdAkEA2bziyReYAb9fi17alcvwZXGzyyMY8WOGns8NZKcq\n"
57+
+ "INF8D3PDpcCyOvQI/TS3qHYmGyWdHiKCWsgBqE6kyjqpNQ==\n"
58+
+ "-----END RSA PRIVATE KEY-----\n";
59+
public static final String PASSWORD = "password";
60+
public static final String TEXT = "text";
61+
public static final String ACCESS_KEY = "access-key";
62+
public static final String SECRET_ACCESS_KEY = "secret-access-key";
63+
public static final String MYSECRETFILE_TXT = "mysecretfile.txt";
64+
public static final String TEST_CERT = "test.p12";
65+
public JenkinsConfiguredWithReadmeRule j = new JenkinsConfiguredWithReadmeRule();
66+
67+
public EnvVarsRule environment = new EnvVarsRule();
68+
69+
@Rule
70+
public RuleChain chain = RuleChain
71+
.outerRule(environment)
72+
.around(j);
73+
74+
@Test
75+
@ConfiguredWithReadme("credentials/README.md#0")
76+
@Envs({
77+
@Env(name = "SUDO_PASSWORD", value = "SUDO")
78+
})
79+
public void testDomainScopedCredentials() {
80+
List<StandardUsernamePasswordCredentials> creds = CredentialsProvider
81+
.lookupCredentials(StandardUsernamePasswordCredentials.class,
82+
Jenkins.getInstanceOrNull(), null, Collections.emptyList());
83+
assertThat(creds.size(), is(1));
84+
StandardUsernamePasswordCredentials cred = creds.get(0);
85+
assertThat(cred.getId(), is("sudo_password"));
86+
assertThat(cred.getUsername(), is("root"));
87+
assertThat(cred.getPassword(), hasPlainText("SUDO"));
88+
}
89+
90+
@Test
91+
@ConfiguredWithReadme("credentials/README.md#1")
92+
@Envs({
93+
@Env(name = "SSH_KEY_PASSWORD", value = PASSPHRASE),
94+
@Env(name = "SSH_PRIVATE_KEY", value = PRIVATE_KEY),
95+
@Env(name = "SSH_PRIVATE_FILE_PATH", value = "private-key.pem"),
96+
@Env(name = "SOME_USER_PASSWORD", value = PASSWORD),
97+
@Env(name = "SECRET_TEXT", value = TEXT),
98+
@Env(name = "AWS_ACCESS_KEY", value = ACCESS_KEY),
99+
@Env(name = "AWS_SECRET_ACCESS_KEY", value = SECRET_ACCESS_KEY),
100+
@Env(name = "SECRET_FILE_PATH", value = MYSECRETFILE_TXT),
101+
@Env(name = "SECRET_PASSWORD_CERT", value = PASSWORD),
102+
@Env(name = "SECRET_CERT_FILE_PATH", value = TEST_CERT),
103+
})
104+
public void testGlobalScopedCredentials() throws Exception {
105+
List<Credentials> creds = CredentialsProvider.lookupCredentials(
106+
Credentials.class, Jenkins.get(), null, Collections.emptyList());
107+
assertThat(creds, hasSize(8));
108+
for (Credentials credentials : creds) {
109+
if (credentials instanceof BasicSSHUserPrivateKey) {
110+
BasicSSHUserPrivateKey key = (BasicSSHUserPrivateKey) credentials;
111+
assertThat(key.getPassphrase(), hasPlainText(PASSPHRASE));
112+
assertThat(key.getPrivateKey(), equalTo(PRIVATE_KEY));
113+
assertThat(key.getId(), anyOf(
114+
is("ssh_with_passphrase_provided"),
115+
is("ssh_with_passphrase_provided_via_file")));
116+
assertThat(key.getUsername(), is("ssh_root"));
117+
assertThat(key.getScope(), is(CredentialsScope.SYSTEM));
118+
} else if (credentials instanceof UsernamePasswordCredentials) {
119+
UsernamePasswordCredentials user = (UsernamePasswordCredentials) credentials;
120+
assertThat(user.getUsername(), is("some-user"));
121+
assertThat(user.getPassword(), hasPlainText(PASSWORD));
122+
assertThat(user.getScope(), is(CredentialsScope.GLOBAL));
123+
} else if (credentials instanceof StringCredentials) {
124+
StringCredentials string = (StringCredentials) credentials;
125+
assertThat(string.getId(), is("secret-text"));
126+
assertThat(string.getSecret(), hasPlainText(TEXT));
127+
assertThat(string.getScope(), is(CredentialsScope.GLOBAL));
128+
} else if (credentials instanceof AWSCredentialsImpl) {
129+
AWSCredentialsImpl aws = (AWSCredentialsImpl) credentials;
130+
assertThat(aws.getId(), is("AWS"));
131+
assertThat(aws.getAccessKey(), equalTo(ACCESS_KEY));
132+
assertThat(aws.getSecretKey(), hasPlainText(SECRET_ACCESS_KEY));
133+
assertThat(aws.getScope(), is(CredentialsScope.GLOBAL));
134+
} else if (credentials instanceof FileCredentials) {
135+
FileCredentials file = (FileCredentials) credentials;
136+
assertThat(file.getId(), anyOf(is("secret-file"), is("secret-file_via_binary_file")));
137+
assertThat(file.getFileName(), is(MYSECRETFILE_TXT));
138+
String fileContent = IOUtils.toString(file.getContent(), StandardCharsets.UTF_8);
139+
assertThat(fileContent, containsString("SUPER SECRET"));
140+
assertThat(file.getScope(), is(CredentialsScope.GLOBAL));
141+
} else if (credentials instanceof CertificateCredentialsImpl) {
142+
CertificateCredentialsImpl cert = (CertificateCredentialsImpl) credentials;
143+
assertThat(cert.getId(), is("secret-certificate"));
144+
assertThat(cert.getPassword(), hasPlainText(PASSWORD));
145+
byte[] fileContent = Files.readAllBytes(Paths.get(getClass().getResource(TEST_CERT).toURI()));
146+
SecretBytes secretBytes = SecretBytes
147+
.fromString(Base64.getEncoder().encodeToString(fileContent));
148+
UploadedKeyStoreSource keyStoreSource = (UploadedKeyStoreSource) cert.getKeyStoreSource();
149+
assertThat(keyStoreSource.getUploadedKeystore().getPlainData(),
150+
is(secretBytes.getPlainData()));
151+
assertThat(cert.getKeyStore().containsAlias("1"), is(true));
152+
assertThat(cert.getKeyStore().getCertificate("1").getType(), is("X.509"));
153+
assertThat(CredentialsNameProvider.name(cert), is("[email protected], CN=pkcs12, O=Fort-Funston, L=SanFrancisco, ST=CA, C=US (my secret cert)"));
154+
assertThat(cert.getScope(), is(CredentialsScope.GLOBAL));
155+
}
156+
}
157+
}
158+
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SUPER SECRET

0 commit comments

Comments
 (0)