CVE-2020-2146
Description
Jenkins Mac Plugin 1.1.0 and earlier does not validate SSH host keys when connecting agents created by the plugin, enabling man-in-the-middle attacks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Mac Plugin up to 1.1.0 fails to validate SSH host keys for agent connections, enabling man-in-the-middle attacks.
Overview
The Jenkins Mac Plugin (versions 1.1.0 and earlier) is designed to configure macOS machines as Jenkins build agents. The plugin connects to these agents over SSH; however, as documented in the official Jenkins security advisory, the plugin does not validate SSH host keys during the connection handshake [1][2]. This omission means the plugin does not verify that the SSH server it connects to is the intended macOS agent.
Attack
Surface
An attacker with network access between the Jenkins controller and the macOS agent can perform a man-in-the-middle (MITM) attack. By intercepting the SSH connection and presenting a rogue SSH server, the attacker can impersonate the legitimate agent without the plugin raising any alert [1]. No authentication bypass on the Jenkins controller itself is required; the vulnerability exists purely in the plugin’s network‑layer trust model.
Impact
If exploited, the attacker gains the ability to intercept, modify, or inject data flowing between the Jenkins controller and the agent. This could lead to unauthorised code execution on the controller (if build commands are altered), exfiltration of secrets stored on the controller (e.g., credentials sent to the agent), or arbitrary file manipulation. The plugin’s own documentation notes that it stores keychain files and sends them to agents, making credential theft a plausible outcome [3].
Mitigation
Users should immediately upgrade to Mac Plugin version 1.2.0, which adds SSH host key verification [2]. No workaround is available for older versions; disabling the plugin or manually enforcing strict host‑key checking in the SSH configuration are not supported alternatives. This vulnerability has been publicly documented in the Jenkins security advisory and the OSS‑Security mailing list [1][2], reinforcing the need for prompt patching.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
fr.edf.jenkins.plugins:macMaven | < 1.2.0 | 1.2.0 |
Affected products
3- Range: <=1.1.0
- Range: unspecified
Patches
1ba1a8206c7efMerge remote-tracking branch 'jenkinsci/master' into SECURITY-1692
5 files changed · +90 −75
README.md+13 −7 modified@@ -1,15 +1,17 @@ # Mac Plugin -[](https://travis-ci.org/jenkinsci/mac-plugin) +[](https://ci.jenkins.io/job/Plugins/job/mac-plugin/job/master/65/) [](https://coveralls.io/github/jenkinsci/mac-plugin?branch=master) [](https://depshield.github.io) A good utility to build yours IOS apps, this plugin create MacOs agents for yours builds. +It can stock your Keychains file on Jenkins and send it to the MacOs Nodes. ## Features - [x] Allow to configure a Mac as Jenkins slave - [x] Run multiples builds on a single Mac -- [x] Run builds on a cloud of Mac +- [x] Isolates each construction from each other +- [x] Run builds on a cloud of Macs - [x] Configure environment variables - [x] Stock keychain file as credentials on Jenkins - [x] Inject keychain on Node filesystem @@ -45,6 +47,10 @@ Create an user on the Mac with administrator privileges. It will be your connect Add sudo NOPASSWD to this user in /etc/sudoers : [see how to configure sudo without password](https://www.robertshell.com/blog/2016/12/3/use-sudo-command-osx-without-password) +To maximize security, you can configure it only for "chmod" and "sysadminctl" command used by the plugin : + +`[USERNAME] ALL = NOPASSWD: /usr/sbin/sysadminctl -addUser mac-?????????? -password ??????????, /usr/sbin/sysadminctl -deleteUser mac-??????????, /bin/chmod -R 700 /Users/mac-??????????/` + ## Plugin configuration In jenkins global configuration, add a new Mac Cloud : @@ -62,12 +68,12 @@ Add a new Mac Host and fill the properties in the fields : The number of simultaneous builds on the same Mac Host depends of the property "Max users". More you have Mac Hosts configured, more you can build simultaneous on many machines. -**For best usage I recommend a limit of 3.** +**The plugin was tested with a limit of 7 users per Mac hosts.** The supported credentials for now is User and Password. -Put an account of your mac with **sudo NOPASSWORD configured**. +Put an account of your mac with **sudo NOPASSWORD configured** (see Configure a Jenkins User). -After it refers the label of your agent. +Refer the label of your agent. Select JNLP for the connector and refer your Jenkins URL. This URL must be accessible by outside, localhost is not working. In a project configuration, refers the label : @@ -76,15 +82,15 @@ In a project configuration, refers the label : ### Keychain Managment Since v1.1.0, you have the possibility to stock keychain files into Jenkins to inject it in the Jenkins Mac agent. -For this check "Upload a keychain file" : +For this, check "Upload a keychain file" : <img src="https://zupimages.net/up/19/49/93el.png" width="400"/> Add a new Secret file credentials. **Prefers to store it as System Credentials to not allow any project to use it directly** : <img src="https://zupimages.net/up/19/49/xw7u.png" width="750"/> -The Keychain is stored as SecretByte on Jenkins and cannot be read directly as a file. It will be send to the Mac agent with SCP in ~/Library/Keychains/ directory before the JNLP connection. +The Keychain will be send to the Mac agent with SCP in ~/Library/Keychains/ directory before the JNLP connection. ### Environment variables Since 1.1.0, you can set environment variables on Mac host. Theses variables will be set on the Node and will be accessible in the build.
src/main/resources/fr/edf/jenkins/plugins/mac/connector/MacComputerJNLPConnector/help-jenkinsUrl.html+1 −1 modified@@ -1,3 +1,3 @@ <div> -URL of the Jenkins Master server. +URL of the Jenkins Master. If empty, takes the given value by Jenkins.getRootUrl </div> \ No newline at end of file
src/main/resources/fr/edf/jenkins/plugins/mac/MacCloud/config.groovy+17 −14 modified@@ -8,20 +8,23 @@ f.entry(title: Messages.Cloud_Name(), field:'name') { f.textbox(default:'mac') } -f.entry(title:Messages.Host_Title()) { - f.repeatableHeteroProperty( - field:'macHosts', - hasHeader: 'true', - addCaption: Messages.Host_Add(), - deleteCaption: Messages.Host_Delete(), - oneEach:'false', - repeatableDeleteButton:'true' - ) -} +f.advanced(title:Messages.Cloud_Details()) { + f.entry(title:Messages.Host_Title()) { + f.repeatableHeteroProperty( + field:'macHosts', + hasHeader: 'true', + addCaption: Messages.Host_Add(), + deleteCaption: Messages.Host_Delete(), + oneEach:'false', + repeatableDeleteButton:'true' + ) + } -f.section(title:Messages.Cloud_AgentsProperties()) { - f.entry(title:Messages.Cloud_IdleMinutes(), field:'idleMinutes') { - f.number(clazz: 'required', min: 1, default: 1) + f.section(title:Messages.Cloud_AgentsProperties()) { + f.entry(title:Messages.Cloud_IdleMinutes(), field:'idleMinutes') { + f.number(clazz: 'required', min: 1, default: 1) + } + f.dropdownDescriptorSelector(title:'Connect method', field:'connector') } - f.dropdownDescriptorSelector(title:'Connect method', field:'connector') } +
src/main/resources/fr/edf/jenkins/plugins/mac/MacHost/config.groovy+56 −52 modified@@ -5,73 +5,77 @@ import fr.edf.jenkins.plugins.mac.Messages def f = namespace(lib.FormTagLib) def c = namespace(lib.CredentialsTagLib) + +f.entry(title: _(Messages.Host_Host()), field: 'host') { + f.textbox(clazz: 'required', checkMethod: 'doCheckHost') +} + f.entry(title: Messages.Host_Disabled(), field:'disabled') { f.checkbox() } -f.entry(title:Messages.Cloud_Labels(), field:'labelString') { - f.textbox() -} +f.advanced(title:Messages.Host_Details()) { -f.entry(title: Messages.Host_MaxTries(), field: 'maxTries') { - f.number(clazz: 'required', min: 1, default: 5) -} + f.entry(title:Messages.Cloud_Labels(), field:'labelString') { + f.textbox() + } -f.entry(title: _(Messages.Host_Host()), field: 'host') { - f.textbox(clazz: 'required') -} + f.entry(title: Messages.Host_MaxTries(), field: 'maxTries') { + f.number(clazz: 'required', min: 1, default: 5) + } -f.entry(title: _(Messages.Host_Port()), field: 'port') { - f.number(clazz: 'required', default: 22, min: 1) -} + f.entry(title: _(Messages.Host_Port()), field: 'port') { + f.number(clazz: 'required', default: 22, min: 1) + } -f.entry(title: _(Messages.MacHostKeyVerifier_HostKey()), field:'key') { - f.textarea(clazz: 'required', checkMethod: 'doCheckKey') -} + f.entry(title: _(Messages.MacHostKeyVerifier_HostKey()), field:'key') { + f.textarea(clazz: 'required', checkMethod: 'doCheckKey') + } -f.entry(title: _(Messages.Host_MaxUsers()), field: 'maxUsers') { - f.number(clazz: 'required', min: 1) -} + f.entry(title: _(Messages.Host_MaxUsers()), field: 'maxUsers') { + f.number(clazz: 'required', min: 1) + } -f.entry(title: _(Messages.Host_Credentials()), field: 'credentialsId') { - c.select(context: app, includeUser: false, expressionAllowed: false) -} + f.entry(title: _(Messages.Host_Credentials()), field: 'credentialsId') { + c.select(context: app, includeUser: false, expressionAllowed: false) + } -f.entry(title: _(Messages.Host_ConnectionTimeout()), field: 'connectionTimeout') { - f.number(clazz: 'required', default: 15, min: 5) -} + f.entry(title: _(Messages.Host_ConnectionTimeout()), field: 'connectionTimeout') { + f.number(clazz: 'required', default: 15, min: 5) + } -f.entry(title: _(Messages.Host_ReadTimeout()), field: 'readTimeout') { - f.number(clazz: 'required', default: 60, min: 30) -} + f.entry(title: _(Messages.Host_ReadTimeout()), field: 'readTimeout') { + f.number(clazz: 'required', default: 60, min: 30) + } -f.entry(title: _(Messages.Host_AgentConnectionTimeout()), field: 'agentConnectionTimeout') { - f.number(clazz: 'required', default: 15, min: 15) -} + f.entry(title: _(Messages.Host_AgentConnectionTimeout()), field: 'agentConnectionTimeout') { + f.number(clazz: 'required', default: 15, min: 15) + } -f.block() { - f.validateButton( - title: _(Messages.Host_TestConnection()), - progress: _('Testing...'), - method: 'verifyConnection', - with: 'host,port,credentialsId,key' - ) -} + f.block() { + f.validateButton( + title: _(Messages.Host_TestConnection()), + progress: _('Testing...'), + method: 'verifyConnection', + with: 'host,port,credentialsId,key' + ) + } -f.optionalBlock(title: _(Messages.Keychain_Title()), field: 'uploadKeychain', + f.optionalBlock(title: _(Messages.Keychain_Title()), field: 'uploadKeychain', checked: null != instance ? instance.uploadKeychain : false, inline: 'true') { - f.entry(title:_(Messages.Keychain_DisplayName()), field:"fileCredentialsId") { - c.select(context: app, includeUser: false, expressionAllowed: false) + f.entry(title:_(Messages.Keychain_DisplayName()), field:"fileCredentialsId") { + c.select(context: app, includeUser: false, expressionAllowed: false) + } } -} -f.entry(title: _(Messages.EnvVar_Title())) { - f.repeatableHeteroProperty( - field:'envVars', - hasHeader: 'true', - addCaption: Messages.EnvVar_Add(), - deleteCaption:Messages.EnvVar_Delete(), - oneEach:'false', - repeatableDeleteButton:'true' - ) -} \ No newline at end of file + f.entry(title: _(Messages.EnvVar_Title())) { + f.repeatableHeteroProperty( + field:'envVars', + hasHeader: 'true', + addCaption: Messages.EnvVar_Add(), + deleteCaption:Messages.EnvVar_Delete(), + oneEach:'false', + repeatableDeleteButton:'true' + ) + } +}
src/main/resources/fr/edf/jenkins/plugins/mac/Messages.properties+3 −1 modified@@ -9,12 +9,13 @@ Cloud.AgentsProperties=Agents Properties Cloud.Labels=Labels Cloud.JenkinsUrl=Jenkins URL Cloud.IdleMinutes=Idle minutes +Cloud.Details=Cloud details # MacHost Host.DisplayName=Mac Host Host.Add=Add Mac Host Host.Delete=Delete Mac Host -Host.Title=Mac Slaves +Host.Title=Mac Hosts Host.Host=Host Host.HostInvalid=The given host is not valid Host.SecurityRestriction=Cannot validate the host due to security restriction @@ -31,6 +32,7 @@ Host.ConnectionSucceeded=Connected as {0} Host.ConnectionFailed=Connection failed : {0} Host.Disabled=Disabled Host.MaxTries=Max connection retries +Host.Details=Host details # Keychain Keychain.DisplayName=Keychain file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rv9g-67f7-grq7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-2146ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/03/09/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/mac-plugin/commit/ba1a8206c7ef990d37498e5abdf210990ef046b5ghsaWEB
- jenkins.io/security/advisory/2020-03-09/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2020-03-09Jenkins Security Advisories · Mar 9, 2020