diff --git a/README.adoc b/README.adoc index 5c2a876810..ff3c00f48e 100644 --- a/README.adoc +++ b/README.adoc @@ -96,6 +96,31 @@ withCredentials([gitUsernamePassword(credentialsId: 'my-credentials-id', powershell 'git push' } ``` + +===== Docker BuildKit secret mount + +To securely forward credentials to `docker.build` the link:https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#run---mounttypesecret[`--mount=type=secret`] approach should be used. This avoids storing the credentials into the image like `ARG` or `ENV` would. When the optinal parameter `hostName` is specified, this binding also provides `GIT_CREDENTIAL_STORE` environment variable which contains path to link:https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage[git credentials store]. + +.Docker BuildKit example +```groovy +# Jenkins pipeline +withCredentials([gitUsernamePassword(credentialsId: 'my-credentials-id', + hostName: 'github.com')]) { + withEnv(['DOCKER_BUILDKIT=1']) { + docker.build '', "--secret id=git_store,src=${GIT_CREDENTIAL_STORE} ." + } +} +``` +```dockerfile +# Dockerfile +RUN --mount=type=secret,id=git_store \ + git config --global credential.helper 'store --file /run/secrets/git_store' && \ + git clone https://github.com/private/repo +``` +The above configuration is best used with the link:https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc[GitHub App authentication] provided by link:https://plugins.jenkins.io/github-branch-source/[GitHub Branch Source plugin]. This issues scoped temporary token valid for one hour, which is then used in HTTPS Basic Auth. + +This removes the need for _service accounts_ configured with static SSH keys or Personal Access Tokens and the management burden associated with these. + [#configuration] == [[GitPlugin-ProjectConfiguration]]Configuration diff --git a/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java b/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java index d500d44265..c79c1dd56d 100644 --- a/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java +++ b/src/main/java/jenkins/plugins/git/GitUsernamePasswordBinding.java @@ -37,12 +37,14 @@ public class GitUsernamePasswordBinding extends MultiBinding run, FilePath filePath, this.unixNodeType); FilePath gitTempFile = gitScript.write(credentials, unbindTempDir.getDirPath()); secretValues.put("GIT_ASKPASS", gitTempFile.getRemote()); + if (this.hostName != null) { + GenerateGitStore gitStore = new GenerateGitStore( + credentials.getUsername(), + credentials.getPassword().getPlainText(), + this.hostName, + credentials.getId()); + FilePath gitStoreTempFile = gitStore.write(credentials, unbindTempDir.getDirPath()); + secretValues.put("GIT_CREDENTIAL_STORE", gitStoreTempFile.getRemote()); + } return new MultiEnvironment(secretValues, publicValues, unbindTempDir.getUnbinder()); } else { taskListener.getLogger().println("JGit and JGitApache type Git tools are not supported by this binding"); @@ -172,6 +187,36 @@ protected Class type() { } } + protected static final class GenerateGitStore extends AbstractOnDiskBinding { + + private final String userVariable; + private final String passVariable; + private final String hostVariable; + + protected GenerateGitStore(String gitUsername, String gitPassword, + String hostname, String credentialId) { + super(gitUsername + ":" + gitPassword, credentialId); + this.userVariable = gitUsername; + this.passVariable = gitPassword; + this.hostVariable = hostname; + } + + @Override + protected FilePath write(StandardUsernamePasswordCredentials credentials, FilePath workspace) + throws IOException, InterruptedException { + FilePath gitStore; + gitStore = workspace.createTempFile("git_store", ""); + gitStore.write("https://" + this.userVariable + ":" + this.passVariable + "@" + this.hostVariable + "\n", null); + gitStore.chmod(0500); + return gitStore; + } + + @Override + protected Class type() { + return StandardUsernamePasswordCredentials.class; + } + } + // Mistakenly defined GitUsernamePassword in first release, prefer gitUsernamePassword as symbol @Symbol({"gitUsernamePassword", "GitUsernamePassword"}) @Extension diff --git a/src/main/resources/jenkins/plugins/git/GitUsernamePasswordBinding/config.jelly b/src/main/resources/jenkins/plugins/git/GitUsernamePasswordBinding/config.jelly index a87549ba32..3e65cc062d 100644 --- a/src/main/resources/jenkins/plugins/git/GitUsernamePasswordBinding/config.jelly +++ b/src/main/resources/jenkins/plugins/git/GitUsernamePasswordBinding/config.jelly @@ -8,4 +8,7 @@ - \ No newline at end of file + + + + diff --git a/src/test/java/jenkins/plugins/git/GitUsernamePasswordBindingTest.java b/src/test/java/jenkins/plugins/git/GitUsernamePasswordBindingTest.java index 130716a3a1..5bbdbd2151 100644 --- a/src/test/java/jenkins/plugins/git/GitUsernamePasswordBindingTest.java +++ b/src/test/java/jenkins/plugins/git/GitUsernamePasswordBindingTest.java @@ -67,6 +67,8 @@ public static Collection data() { private final String password; + private final String hostname; + private final GitTool gitToolInstance; private final String credentialID = DigestUtils.sha256Hex(("Git Usernanme and Password Binding").getBytes(StandardCharsets.UTF_8)); @@ -108,6 +110,7 @@ public static Collection data() { public GitUsernamePasswordBindingTest(String username, String password, GitTool gitToolInstance) { this.username = username; this.password = password; + this.hostname = "hostname"; this.gitToolInstance = gitToolInstance; } @@ -122,7 +125,7 @@ public void basicSetup() throws IOException { CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), credentials); //GitUsernamePasswordBinding instance - gitCredBind = new GitUsernamePasswordBinding(gitToolInstance.getName(), credentials.getId()); + gitCredBind = new GitUsernamePasswordBinding(gitToolInstance.getName(), credentials.getId(), hostname); assertThat(gitCredBind.type(), is(StandardUsernamePasswordCredentials.class)); //Setting Git Tool @@ -144,7 +147,7 @@ private String shellCheck() { public void test_EnvironmentVariables_FreeStyleProject() throws Exception { FreeStyleProject prj = r.createFreeStyleProject(); prj.getBuildWrappersList().add(new SecretBuildWrapper(Collections.> - singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID)))); + singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID, hostname)))); prj.getBuildersList().add(isWindows() ? new BatchFile(batchCheck(isCliGitTool())) : new Shell(shellCheck())); r.configRoundtrip((Item) prj); @@ -158,6 +161,7 @@ public void test_EnvironmentVariables_FreeStyleProject() throws Exception { }else { assertThat(((GitUsernamePasswordBinding) binding).getGitToolName(), equalTo("")); } + assertThat(((GitUsernamePasswordBinding) binding).getHostName(), equalTo(hostname)); FreeStyleBuild b = r.buildAndAssertSuccess(prj); if(credentials.isUsernameSecret()) { @@ -237,7 +241,7 @@ public void test_isCurrentNodeOSUnix(){ public void test_getCliGitTool_using_FreeStyleProject() throws Exception { FreeStyleProject prj = r.createFreeStyleProject(); prj.getBuildWrappersList().add(new SecretBuildWrapper(Collections.> - singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID)))); + singletonList(new GitUsernamePasswordBinding(gitToolInstance.getName(), credentialID, hostname)))); prj.getBuildersList().add(isWindows() ? new BatchFile(batchCheck(false)) : new Shell(shellCheck())); r.configRoundtrip((Item) prj); SecretBuildWrapper wrapper = prj.getBuildWrappersList().get(SecretBuildWrapper.class); @@ -283,6 +287,19 @@ public void test_GenerateGitScript_write() throws IOException, InterruptedExcept assertThat(tempScriptFile.readToString(), containsString(this.password)); } + @Test + public void test_GenerateGitStore_write() throws IOException, InterruptedException { + GitUsernamePasswordBinding.GenerateGitStore tempGenGitStore = new GitUsernamePasswordBinding.GenerateGitStore(this.username, this.password, this.hostname, credentials.getId()); + assertThat(tempGenGitStore.type(), is(StandardUsernamePasswordCredentials.class)); + FilePath tempStoreFile = tempGenGitStore.write(credentials, rootFilePath); + if (!isWindows()) { + assertThat(tempStoreFile.mode(), is(0500)); + } + assertThat(tempStoreFile.readToString(), containsString(this.username)); + assertThat(tempStoreFile.readToString(), containsString(this.password)); + assertThat(tempStoreFile.readToString(), containsString(this.hostname)); + } + /** * inline ${@link hudson.Functions#isWindows()} to prevent a transient * remote classloader issue