diff --git a/CreateProfile.psm1 b/CreateProfile.psm1 new file mode 100644 index 00000000..2e099b47 --- /dev/null +++ b/CreateProfile.psm1 @@ -0,0 +1,124 @@ +# Based on code developed by Josh Rickard (@MS_dministrator) and Thom Schumacher (@driberif) +# Location: https://gist.github.com/crshnbrn66/7e81bf20408c05ddb2b4fdf4498477d8 + +#function to register a native method +function Register-NativeMethod +{ + [CmdletBinding()] + [Alias()] + [OutputType([int])] + Param + ( + # Param1 help description + [Parameter(Mandatory=$true, + ValueFromPipelineByPropertyName=$true, + Position=0)] + [string]$dll, + + # Param2 help description + [Parameter(Mandatory=$true, + ValueFromPipelineByPropertyName=$true, + Position=1)] + [string] + $methodSignature + ) + + $script:nativeMethods += [PSCustomObject]@{ Dll = $dll; Signature = $methodSignature; } +} + +#function to add native method +function Add-NativeMethods +{ + [CmdletBinding()] + [Alias()] + [OutputType([int])] + Param($typeName = 'NativeMethods') + + $nativeMethodsCode = $script:nativeMethods | ForEach-Object { " + [DllImport(`"$($_.Dll)`")] + public static extern $($_.Signature); + " } + + Add-Type @" + using System; + using System.Text; + using System.Runtime.InteropServices; + public static class $typeName { + $nativeMethodsCode + } +"@ +} + +#Main function to create the new user profile +function New-UserWithProfile { + [CmdletBinding()] + [Alias()] + [OutputType([int])] + Param + ( + # Param1 help description + [Parameter(Mandatory=$true, + ValueFromPipelineByPropertyName=$true, + Position=0)] + [string]$UserName, + + # Param2 help description + [Parameter(ValueFromPipelineByPropertyName=$true, + Position=1)] + [SecureString] + $Password, + + [Parameter(Mandatory=$false, + ValueFromPipelineByPropertyName=$true, + Position=2)] + [string] + $Description + ) + + Write-Verbose "Creating local user $Username"; + + try + { + if($null -eq $Password) { + New-LocalUser -Name $UserName -NoPassword -Description "$Description" -AccountNeverExpires + } else { + New-LocalUser -Name $UserName -Password $Password -Description "$Description" -AccountNeverExpires + } + } + catch + { + Write-Error $_.Exception.Message; + break; + } + $methodName = 'UserEnvCP' + $script:nativeMethods = @(); + + if (-not ([System.Management.Automation.PSTypeName]$MethodName).Type) + { + Register-NativeMethod "userenv.dll" "int CreateProfile([MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,` + [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,` + [Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath, uint cchProfilePath)"; + + Add-NativeMethods -typeName $MethodName; + } + + $localUser = New-Object System.Security.Principal.NTAccount("$UserName"); + $userSID = $localUser.Translate([System.Security.Principal.SecurityIdentifier]); + $sb = new-object System.Text.StringBuilder(260); + $pathLen = $sb.Capacity; + + Write-Verbose "Creating user profile for $Username"; + + try + { + [UserEnvCP]::CreateProfile($userSID.Value, $Username, $sb, $pathLen) | Out-Null; + } + catch + { + Write-Error $_.Exception.Message; + break; + } + + $profilePath = $sb.ToString() + Write-Verbose "Profile created at $profilePath" +} \ No newline at end of file diff --git a/Dockerfile-windows b/Dockerfile-windows new file mode 100644 index 00000000..023aaa81 --- /dev/null +++ b/Dockerfile-windows @@ -0,0 +1,83 @@ +# escape=` + +# The MIT License +# +# Copyright (c) 2019, Alex Earl +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +ARG WINDOWS_DOCKER_TAG=1809 + +FROM openjdk:8-jdk-windowsservercore-$WINDOWS_DOCKER_TAG +LABEL MAINTAINER="Alex Earl " + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +ARG user=jenkins + +ARG JENKINS_AGENT_HOME=C:/Users/${user} + +ARG OPENSSH_VERSION=v8.0.0.0p1-Beta + +ENV JENKINS_AGENT_USER ${user} + +ENV JENKINS_AGENT_HOME ${JENKINS_AGENT_HOME} + +COPY CreateProfile.psm1 C:/ + +#create jenkins user +RUN Import-Module -Force C:/CreateProfile.psm1 ; ` + New-UserWithProfile -UserName $env:user -Description 'Jenkins Agent User' ; ` + Remove-Item -Force C:/CreateProfile.psm1 ; ` + Set-LocalUser -Name $env:user -PasswordNeverExpires $true ; ` + New-Item -Type Directory -Path "C:\ProgramData\Jenkins" | Out-Null + +# setup SSH server +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` + Invoke-WebRequest -Uri "https://github.com/PowerShell/Win32-OpenSSH/releases/download/${env:OPENSSH_VERSION}/OpenSSH-Win64.zip -OutFile C:/openssh.zip -UseBasicParsing ; ` + Expand-Archive c:/openssh.zip 'C:/Program Files' ; ` + Remove-Item C:/openssh.zip ; ` + $env:PATH = '{0};{1}' -f $env:PATH,'C:\Program Files\OpenSSH-Win64' ; ` + & 'C:/Program Files/OpenSSH-Win64/Install-SSHd.ps1' ; ` + New-Item -Type Directory -Path 'C:\ProgramData\ssh' | Out-Null ; ` + Copy-Item 'C:\Program Files\OpenSSH-Win64\sshd_config_default' 'C:\ProgramData\ssh\sshd_config' ; ` + $content = Get-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` + $content | ForEach-Object { $_ -replace '#PermitRootLogin.*','PermitRootLogin no' ` + -replace '#PasswordAuthentication.*','PasswordAuthentication no' ` + -replace '#PermitEmptyPasswords.*','PermitEmptyPasswords no' ` + -replace '#PubkeyAuthentication.*','PubkeyAuthentication yes' ` + -replace '#SyslogFacility.*','SyslogFacility LOCAL0' ` + -replace '#LogLevel.*','LogLevel DEBUG3' ` + -replace 'Match Group administrators','' ` + -replace '(\s*)AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','' ` + } | ` + Set-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` + Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'ChallengeResponseAuthentication no' ; ` + Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'HostKeyAgent \\.\pipe\openssh-ssh-agent' ; ` + New-Item -Path HKLM:\SOFTWARE -Name OpenSSH -Force | Out-Null ; ` + New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType string -Force | Out-Null + +VOLUME "${JENKINS_AGENT_HOME}" "C:\Users\${user}\AppData\Local\Temp" +WORKDIR "${JENKINS_AGENT_HOME}" + +COPY setup-sshd.ps1 C:/ProgramData/Jenkins/setup-sshd.ps1 + +EXPOSE 22 + +ENTRYPOINT ["powershell.exe", "-NoExit", "-Command", "& C:/ProgramData/Jenkins/setup-sshd.ps1"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..eb631253 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,46 @@ +/* NOTE: this Pipeline mainly aims at catching mistakes (wrongly formed Dockerfile, etc.) + * This Pipeline is *not* used for actual image publishing. + * This is currently handled through Automated Builds using standard Docker Hub feature +*/ +pipeline { + agent { label 'linux' } + + options { + timeout(time: 2, unit: 'MINUTES') + buildDiscarder(logRotator(daysToKeepStr: '10')) + timestamps() + } + + triggers { + pollSCM('H/24 * * * *') // once a day in case some hooks are missed + } + + stages { + stage('Build Docker Image') { + parallel { + stage('Windows') { + agent { + label "windock" + } + steps { + deleteDir() + checkout scm + powershell "& ./make.ps1" + } + } + stage('Linux') { + agent { + label "docker&&linux" + } + steps { + deleteDir() + checkout scm + sh "make build" + } + } + } + } + } +} + +// vim: ft=groovy diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b4f03533 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +IMAGE_NAME:=jenkins4eval/ssh-slave:test + +build: + docker build -t ${IMAGE_NAME} . diff --git a/make.ps1 b/make.ps1 new file mode 100644 index 00000000..68aeac03 --- /dev/null +++ b/make.ps1 @@ -0,0 +1,5 @@ +if([System.String]::IsNullOrWhiteSpace($env:IMAGE_NAME)) { + $env:IMAGE_NAME='jenkins4eval/ssh-agent:test-windows' +} + +& docker build -f Dockerfile-windows -t $env:IMAGE_NAME . diff --git a/setup-sshd.ps1 b/setup-sshd.ps1 new file mode 100644 index 00000000..e17a7be6 --- /dev/null +++ b/setup-sshd.ps1 @@ -0,0 +1,73 @@ +# The MIT License +# +# Copyright (c) 2019, Alex Earl +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Usage: +# docker run jenkins/ssh-agent:windows +# or +# docker run -e "JENKINS_SLAVE_SSH_PUBKEY=" jenkins/ssh-agent:windows + +function Write-Key($Key) { + # this writes the key and sets the permissions correctly for pubkey auth + $sshDir = '{0}\.ssh' -f $env:JENKINS_AGENT_HOME + $authorizedKeys = Join-Path $sshDir 'authorized_keys' + New-Item -Type Directory -Path $sshDir | Out-Null + icacls.exe $sshDir /setowner ${env:JENKINS_AGENT_USER} | Out-Null + icacls.exe $sshDir /grant "${env:JENKINS_AGENT_USER}:(CI)(OI)(F)" /grant "administrators:(CI)(OI)(F)" | Out-Null + icacls.exe $sshDir /inheritance:r | Out-Null + + Set-Content -Path $authorizedKeys -Value "$Key" -Encoding UTF8 + icacls.exe $authorizedKeys /setowner ${env:JENKINS_AGENT_USER} | Out-Null +} + +# Even though we created a profile, the NTUSER.DAT file is missing +# this needs to be in the directory or Windows will not load +# the profile +if(!(Test-Path (Join-Path $env:JENKINS_AGENT_HOME 'NTUSER.DAT'))) { + Copy-Item -Path 'C:\Users\Default\NTUSER.DAT' -Destination (Join-Path $env:JENKINS_AGENT_HOME 'NTUSER.DAT') +} + +# Give the user Full Access to the home directory +icacls.exe $env:JENKINS_AGENT_HOME /grant "${env:JENKINS_AGENT_USER}:(CI)(OI)(F)" | Out-Null + +if($env:JENKINS_SLAVE_SSH_PUBKEY -match "^ssh-.*") { + Write-Key $env:JENKINS_SLAVE_SSH_PUBKEY +} + +if($args.Length -gt 0) { + if($args[0] -match "^ssh-.*") { + Write-Key "$($args[0]) $($args[1]) $($args[2])" + $null, $null, $null, $args = $args + } else { + & "$args" + } +} + + + +# ensure variables passed to docker container are also exposed to ssh sessions +Get-ChildItem env: | ForEach-Object { setx /m $_.Name $_.Value | Out-Null } + +Start-Service sshd +while($true) { + # if we don't do this endless loop, the container exits + Start-Sleep -Seconds 60 +}