diff --git a/Jenkinsfile b/Jenkinsfile index fceb184..1237760 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,6 +5,7 @@ pipeline { options { timestamps() + parallelsAlwaysFailFast() } triggers { @@ -28,20 +29,6 @@ pipeline { } } - // workaround for Jenkins not fetching tags - stage('Fetch tags') { - steps { - withCredentials( - [usernameColonPassword(credentialsId: 'conjur-jenkins-api', variable: 'GITCREDS')] - ) { - sh ''' - git fetch --tags `git remote get-url origin | sed -e "s|https://|https://$GITCREDS@|"` - git tag # just print them out to make sure, can remove when this is robust - ''' - } - } - } - stage('Build') { steps { sh './build.sh' @@ -49,10 +36,80 @@ pipeline { } } - stage('Tests') { parallel { - stage('Linting and unit tests') { + stage('Setup & Hold Win2016 Node') { + steps { + script { + // Node used instead of agent to avoid the automatic git checkout that agent provides. + // This is because git checkout is unreliable on windows agents + node('executor-windows-2016-containers'){ + // because the repo is not auto checked out, fetch the configure script via http + powershell """ + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/cyberark/conjur-puppet/${BRANCH_NAME}/expose-daemon.ps1" -OutFile "expose-daemon.ps1" + """ + powershell '.\\expose-daemon.ps1' + env.WINDOWS_IP = powershell(returnStdout: true, script: '(curl http://169.254.169.254/latest/meta-data/local-ipv4).Content').trim() + env.WINDOWS_DOCKER_CERT_CA = powershell(returnStdout: true, script: 'cat $env:USERPROFILE\\.docker\\ca.pem') + env.WINDOWS_DOCKER_CERT_CERT = powershell(returnStdout: true, script: 'cat $env:USERPROFILE\\.docker\\cert.pem') + env.WINDOWS_DOCKER_CERT_KEY = powershell(returnStdout: true, script: 'cat $env:USERPROFILE\\.docker\\key.pem') + env.WINDOWS_READY = true + + // The windows node is terminated when the containing node block ends, so we wait until the tests are finished + // before letting this block complete. + waitUntil { + return (env.MAIN_NODE_DONE == "true"); + } + } + } + } + } + + stage('Test Win2016') { + stages { + stage("Wait for Windows node") { + steps { + waitUntil { + script { + return (env.WINDOWS_READY == "true"); + } + } + script { + env.WINDOWS_DOCKER_CERT_DIR = "${pwd()}/tmp-docker" + } + + sh "mkdir ${env.WINDOWS_DOCKER_CERT_DIR}" + writeFile file: "${env.WINDOWS_DOCKER_CERT_DIR}/ca.pem", text: env.WINDOWS_DOCKER_CERT_CA + writeFile file: "${env.WINDOWS_DOCKER_CERT_DIR}/cert.pem", text: env.WINDOWS_DOCKER_CERT_CERT + writeFile file: "${env.WINDOWS_DOCKER_CERT_DIR}/key.pem", text: env.WINDOWS_DOCKER_CERT_KEY + } + } + + stage('Puppet 6 & Conjur 5 Integration Tests') { + steps { + dir('examples/puppetmaster') { + sh ''' + MAIN_HOST_IP="$(curl http://169.254.169.254/latest/meta-data/local-ipv4)" \ + WINDOWS_DOCKER_HOST="tcp://${WINDOWS_IP}:2376" \ + WINDOWS_DOCKER_CERT_PATH="${WINDOWS_DOCKER_CERT_DIR}" \ + WINDOWS_DOCKER_TLS_VERIFY=1 \ + ./test.sh + ''' + } + } + } + } + + post { + always { + script { + env.MAIN_NODE_DONE = true + } + } + } + } + + stage('Linting & Unit Tests') { steps { sh './test.sh' } @@ -69,14 +126,6 @@ pipeline { } } } - - stage('E2E - Puppet 6 - Conjur 5') { - steps { - dir('examples/puppetmaster') { - sh './test.sh' - } - } - } } } diff --git a/examples/puppetmaster/code/environments/production/manifests/site.pp b/examples/puppetmaster/code/environments/production/manifests/site.pp index 60d883b..4e5d2fe 100644 --- a/examples/puppetmaster/code/environments/production/manifests/site.pp +++ b/examples/puppetmaster/code/environments/production/manifests/site.pp @@ -2,13 +2,18 @@ node default { if $facts['os']['family'] == 'Windows' { - $cred_file_prefix = 'c:/tmp' + # There's a double backslash at the end of $cred_file_prefix because: + # When a backslash occurs at the very end of a single-quoted string, a double + # backslash must be used instead of a single backslash. For example: + # path => 'C:\Program Files(x86)\\' + $cred_file_prefix = 'c:\tmp\\' + $win_cmd_exe = 'C:\Windows\System32\cmd.exe' } else { - $cred_file_prefix = '/tmp' + $cred_file_prefix = '/tmp/' } - $output_file1 = "${cred_file_prefix}/creds1.txt" - $output_file2 = "${cred_file_prefix}/creds2.txt" + $output_file1 = "${cred_file_prefix}creds1.txt" + $output_file2 = "${cred_file_prefix}creds2.txt" # If using server-supplied identity for the agent's Conjur / DAP connection, # you would use the optional parameters to the `conjur::secret` function as @@ -24,28 +29,42 @@ notify { "Writing regular secret to ${output_file1}...": } file { $output_file1: - ensure => file, + ensure => file, content => Sensitive(Deferred(conjur::secret, ['inventory/db-password'])), } notify { "Writing funky secret to ${output_file2}...": } file { $output_file2: - ensure => file, + ensure => file, content => Sensitive(Deferred(conjur::secret, [ 'inventory/funky/special @#$%^&*(){}[].,+/variable' ])), } - exec { "cat ${output_file1}": - path => '/usr/bin:/usr/sbin:/bin', - provider => shell, - logoutput => true, + if $facts['os']['family'] == 'Windows' { + exec { "Read secret from ${output_file1}...": + command => "${win_cmd_exe} /c type ${output_file1}", + logoutput => true, + } + } else { + exec { "cat ${output_file1}": + path => '/usr/bin:/usr/sbin:/bin', + provider => shell, + logoutput => true, + } } - exec { "cat ${output_file2}": - path => '/usr/bin:/usr/sbin:/bin', - provider => shell, - logoutput => true, + if $facts['os']['family'] == 'Windows' { + exec { "Read secret from ${output_file2}...": + command => "${win_cmd_exe} /c type ${output_file2}", + logoutput => true, + } + } else { + exec { "cat ${output_file2}": + path => '/usr/bin:/usr/sbin:/bin', + provider => shell, + logoutput => true, + } } notify { 'Done!': } diff --git a/examples/puppetmaster/test.sh b/examples/puppetmaster/test.sh index 9f66563..b944c33 100755 --- a/examples/puppetmaster/test.sh +++ b/examples/puppetmaster/test.sh @@ -6,6 +6,15 @@ set -euo pipefail source vagrant/utils.sh +# MAIN_HOST_IP is the IP address of the host where the tests are running that should be +# accessible from containers running in the Windows Docker Daemon. +export MAIN_HOST_IP=${MAIN_HOST_IP:-} +# Configuration for the Windows Docker Daemon. If WINDOWS_DOCKER_HOST is not set then the +# Windows tests are skipped. +export WINDOWS_DOCKER_HOST=${WINDOWS_DOCKER_HOST:-} +export WINDOWS_DOCKER_CERT_PATH=${WINDOWS_DOCKER_CERT_PATH:-} +export WINDOWS_DOCKER_TLS_VERIFY=${WINDOWS_DOCKER_TLS_VERIFY:-0} + CLEAN_UP_ON_EXIT=${CLEAN_UP_ON_EXIT:-true} INSTALL_PACKAGED_MODULE=${INSTALL_PACKAGED_MODULE:-true} COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-puppetmaster_$(openssl rand -hex 3)} @@ -31,12 +40,39 @@ cleanup() { docker-compose down -v || true } +build_windows_puppet_image() { + cat ./windows-agent.Dockerfile | run_with_docker_windows docker build -t puppet-agent - +} + +prepare_windows() { + if [[ -z "${WINDOWS_DOCKER_HOST}" ]]; then + echo "---"; + echo "Windows Daemon not available. Skipping tests for 'Windows'"; + + return + fi + + echo "---" + echo "Windows Daemon available. Preparing test environment for 'Windows'" + + if [[ -z "${MAIN_HOST_IP}" ]]; then + echo "MAIN_HOST_IP envvar must be set to accompany WINDOWS_DOCKER_HOST"; + exit 1; + fi + + echo + echo "=> Building puppet agent image for 'Windows' <=" + build_windows_puppet_image +} + main() { cleanup if [[ "${CLEAN_UP_ON_EXIT}" = true ]]; then trap cleanup EXIT fi + prepare_windows + start_services setup_conjur wait_for_puppetmaster @@ -78,10 +114,46 @@ main() { done done + run_windows_tests + echo "===" echo "ALL TESTS COMPLETED" } +run_windows_tests() { + # Run tests on Windows if there's a Windows Daemon + if [[ -z "${WINDOWS_DOCKER_HOST}" ]]; then + echo "---" + + echo "Tests for 'Windows': Skipped" + return; + fi + + echo "---" + echo "Running tests for 'Windows'..." + + echo + echo "=> Agent config, API Key <=" + converge_windows_node_agent_apikey + + echo + echo "=> Hiera manifest config, API Key <=" + converge_windows_node_hiera_manifest_apikey + + echo "Tests for 'Windows': OK" +} + +run_with_docker_windows() { + local DOCKER_HOST=${WINDOWS_DOCKER_HOST} + local DOCKER_CERT_PATH=${WINDOWS_DOCKER_CERT_PATH} + local DOCKER_TLS_VERIFY=${WINDOWS_DOCKER_TLS_VERIFY} + export DOCKER_HOST; + export DOCKER_CERT_PATH; + export DOCKER_TLS_VERIFY; + + "$@" +} + run_in_conjur() { docker-compose exec -T conjur "$@" } @@ -210,7 +282,7 @@ revoke_cert_for() { converge_node_agent_apikey() { local agent_image="$1" local node_name="agent-apikey-node" - local hostname="${node_name}_$(openssl rand -hex 3)" + local hostname="${node_name}-$(openssl rand -hex 3)" local login="host/$node_name" local api_key=$(get_host_key $node_name) @@ -262,7 +334,7 @@ converge_node_agent_apikey() { converge_node_hiera_manifest_apikey() { local agent_image="$1" local node_name="hiera-manifest-apikey-node" - local hostname="${node_name}_$(openssl rand -hex 3)" + local hostname="${node_name}-$(openssl rand -hex 3)" local login="host/$node_name" local api_key=$(get_host_key $node_name) @@ -349,4 +421,107 @@ $ssl_certificate rm -rf "$manifest_config_file" "$hiera_config_file" } +converge_windows_node_agent_apikey() { + local node_name="agent-apikey-node" + local hostname="${node_name}-$(openssl rand -hex 3)" + + local login="host/$node_name" + local api_key=$(get_host_key $node_name) + echo "API key for $node_name: $api_key" + + revoke_cert_for "$hostname" + + set -x + run_with_docker_windows docker run --rm -t \ + --hostname "${hostname}" \ + puppet-agent \ + powershell -Command " + # Allow resolution of the hostnames for conjur and puppet + Add-Content -Path 'c:\Windows\System32\Drivers\etc\hosts' -Value '${MAIN_HOST_IP} conjur.cyberark.com' + Add-Content -Path 'c:\Windows\System32\Drivers\etc\hosts' -Value '${MAIN_HOST_IP} puppet' + Add-Content -Path 'c:\conjur-ca.crt' -Value '$(cat $PWD/https_config/ca.crt)' + + + # Set conjur.conf equivalent with connection details + reg ADD HKLM\Software\CyberArk\Conjur /v ApplianceUrl /t REG_SZ /d https://conjur.cyberark.com:$(conjur_host_port)/ + reg ADD HKLM\Software\CyberArk\Conjur /v Version /t REG_DWORD /d 5 + reg ADD HKLM\Software\CyberArk\Conjur /v Account /t REG_SZ /d cucumber + reg ADD HKLM\Software\CyberArk\Conjur /v CertFile /t REG_SZ /d c:\conjur-ca.crt + + # Set conjur.identity equivalent with auth details + cmdkey /generic:conjur.cyberark.com /user:${login} /pass:${api_key} + + puppet agent --verbose --onetime --no-daemonize --summarize --masterport $(puppet_host_port) --certname \$(hostname) + " + set +x +} + +converge_windows_node_hiera_manifest_apikey() { + local node_name="hiera-manifest-apikey-node" + local hostname="${node_name}-$(openssl rand -hex 3)" + + local login="host/$node_name" + local api_key=$(get_host_key $node_name) + echo "API key for $node_name: $api_key" + + local hiera_config_file="./code/data/nodes/$hostname.yaml" + local manifest_config_file="./code/environments/production/manifests/00_$hostname.pp" + + local ssl_certificate="$(cat https_config/ca.crt | sed 's/^/ /')" + + echo "--- +lookup_options: + '^conjur::authn_api_key': + convert_to: 'Sensitive' + +conjur::account: 'cucumber' +conjur::appliance_url: 'https://conjur.cyberark.com:$(conjur_host_port)' +conjur::authn_login: 'host/$node_name' +conjur::authn_api_key: '$api_key' +conjur::ssl_certificate: | +$ssl_certificate + " > $hiera_config_file + + echo " + node '$hostname' { + \$pem_file = 'c:\tmp\test.pem' + \$secret = Sensitive(Deferred(conjur::secret, ['inventory/db-password', { + appliance_url => lookup('conjur::appliance_url'), + account => lookup('conjur::account'), + authn_login => lookup('conjur::authn_login'), + authn_api_key => lookup('conjur::authn_api_key'), + ssl_certificate => lookup('conjur::ssl_certificate') + }])) + + notify { \"Writing secret to \${pem_file}...\": } + file { \$pem_file: + ensure => file, + content => \$secret, + } + + exec { \"Read secret from \${pem_file}...\": + command => \"C:\Windows\System32\cmd.exe /c type \${pem_file}\", + logoutput => true, + } + }" > $manifest_config_file + + revoke_cert_for "$hostname" + + set -x + run_with_docker_windows \ + docker run --rm -t \ + --hostname "${hostname}" \ + puppet-agent \ + powershell -Command " + # Allow resolution of the hostnames for conjur and puppet + Add-Content -Path 'c:\Windows\System32\Drivers\etc\hosts' -Value '${MAIN_HOST_IP} conjur.cyberark.com' + Add-Content -Path 'c:\Windows\System32\Drivers\etc\hosts' -Value '${MAIN_HOST_IP} puppet' + + puppet agent --verbose --onetime --no-daemonize --summarize --masterport $(puppet_host_port) --certname \$(hostname) + " + set +x + + rm -rf "$manifest_config_file" "$hiera_config_file" +} + main "$@" diff --git a/examples/puppetmaster/windows-agent.Dockerfile b/examples/puppetmaster/windows-agent.Dockerfile new file mode 100644 index 0000000..6bf92ac --- /dev/null +++ b/examples/puppetmaster/windows-agent.Dockerfile @@ -0,0 +1,12 @@ +FROM mcr.microsoft.com/windows/servercore:1607 + +RUN powershell mkdir "c:\tmp" + +RUN powershell \ + wget https://downloads.puppetlabs.com/windows/puppet6/puppet-agent-x64-latest.msi \ + -outfile puppet6.msi + +RUN powershell Start-Process msiexec.exe \ + -Wait \ + -ArgumentList \ + '/qn /quiet /norestart /L*v C:\puppet_install_log.txt /i C:\puppet6.msi PUPPET_AGENT_STARTUP_MODE=Manual' diff --git a/expose-daemon.ps1 b/expose-daemon.ps1 new file mode 100644 index 0000000..9fc091c --- /dev/null +++ b/expose-daemon.ps1 @@ -0,0 +1,149 @@ +$ErrorActionPreference = 'Stop'; + +if (!(Test-Path $env:USERPROFILE\.docker)) { + mkdir $env:USERPROFILE\.docker +} + +$ipAddresses = ((Get-NetIPAddress -AddressFamily IPv4).IPAddress) -Join ',' + +function ensureDirs($dirs) { + foreach ($dir in $dirs) { + if (!(Test-Path $dir)) { + mkdir $dir + } + } +} + +# https://docs.docker.com/engine/security/https/ +# Thanks to @artisticcheese! https://artisticcheese.wordpress.com/2017/06/10/using-pure-powershell-to-generate-tls-certificates-for-docker-daemon-running-on-windows/ +function createCA($serverCertsPath) { + Write-Host "`n=== Generating CA" + $parms = @{ + type = "Custom" ; + KeyExportPolicy = "Exportable"; + Subject = "CN=Docker TLS Root"; + CertStoreLocation = "Cert:\LocalMachine\My"; + HashAlgorithm = "sha256"; + KeyLength = 4096; + KeyUsage = @("CertSign", "CRLSign"); + TextExtension = @("2.5.29.19 ={critical} {text}ca=1") + } + $rootCert = New-SelfSignedCertificate @parms + + Write-Host "`n=== Generating CA public key" + $parms = @{ + Path = "$serverCertsPath\ca.pem"; + Value = "-----BEGIN CERTIFICATE-----`n" ` + + [System.Convert]::ToBase64String($rootCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) ` + + "`n-----END CERTIFICATE-----"; + Encoding = "ASCII"; + } + Set-Content @parms + return $rootCert +} + +# https://docs.docker.com/engine/security/https/ +function createCerts($rootCert, $serverCertsPath, $serverName, $ipAddresses, $clientCertsPath) { + Write-Host "`n=== Generating Server certificate" + $parms = @{ + CertStoreLocation = "Cert:\LocalMachine\My"; + Signer = $rootCert; + Subject = "CN=serverCert"; + KeyExportPolicy = "Exportable"; + Provider = "Microsoft Enhanced Cryptographic Provider v1.0"; + Type = "SSLServerAuthentication"; + HashAlgorithm = "sha256"; + TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.1", "2.5.29.17={text}DNS=$serverName&DNS=localhost&IPAddress=$($ipAddresses.Split(',') -Join '&IPAddress=')"); + KeyLength = 4096; + } + $serverCert = New-SelfSignedCertificate @parms + + $parms = @{ + Path = "$serverCertsPath\server-cert.pem"; + Value = "-----BEGIN CERTIFICATE-----`n" ` + + [System.Convert]::ToBase64String($serverCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) ` + + "`n-----END CERTIFICATE-----"; + Encoding = "Ascii" + } + Set-Content @parms + + Write-Host "`n=== Generating Server private key" + $privateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert) + $parms = @{ + Path = "$serverCertsPath\server-key.pem"; + Value = ("-----BEGIN PRIVATE KEY-----`n" ` + + [System.Convert]::ToBase64String($privateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) ` + + "`n-----END PRIVATE KEY-----"); + Encoding = "Ascii"; + } + Set-Content @parms + + Write-Host "`n=== Generating Client certificate" + $parms = @{ + CertStoreLocation = "Cert:\LocalMachine\My"; + Subject = "CN=clientCert"; + Signer = $rootCert ; + KeyExportPolicy = "Exportable"; + Provider = "Microsoft Enhanced Cryptographic Provider v1.0"; + TextExtension = @("2.5.29.37= {text}1.3.6.1.5.5.7.3.2") ; + HashAlgorithm = "sha256"; + KeyLength = 4096; + } + $clientCert = New-SelfSignedCertificate @parms + + $parms = @{ + Path = "$clientCertsPath\cert.pem" ; + Value = ("-----BEGIN CERTIFICATE-----`n" + [System.Convert]::ToBase64String($clientCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----"); + Encoding = "Ascii"; + } + Set-Content @parms + + Write-Host "`n=== Generating Client key" + $clientprivateKeyFromCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($clientCert) + $parms = @{ + Path = "$clientCertsPath\key.pem"; + Value = ("-----BEGIN PRIVATE KEY-----`n" ` + + [System.Convert]::ToBase64String($clientprivateKeyFromCert.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob), [System.Base64FormattingOptions]::InsertLineBreaks) ` + + "`n-----END PRIVATE KEY-----"); + Encoding = "Ascii"; + } + Set-Content @parms + + copy $serverCertsPath\ca.pem $clientCertsPath\ca.pem +} + +function updateConfig($daemonJson, $serverCertsPath) { + $config = @{} + if (Test-Path $daemonJson) { + $config = (Get-Content $daemonJson) -join "`n" | ConvertFrom-Json + } + + $config = $config | Add-Member(@{ ` + hosts = @("tcp://0.0.0.0:2376", "npipe://"); ` + tlsverify = $true; ` + tlscacert = "$serverCertsPath\ca.pem"; ` + tlscert = "$serverCertsPath\server-cert.pem"; ` + tlskey = "$serverCertsPath\server-key.pem" ` + }) -Force -PassThru + + Write-Host "`n=== Creating / Updating $daemonJson" + $config | ConvertTo-Json | Set-Content $daemonJson -Encoding Ascii +} + +$dockerData = "$env:ProgramData\docker" +$userPath = "$env:USERPROFILE\.docker" + +ensureDirs @("$dockerData\certs.d", "$dockerData\config", "$userPath") + +$serverCertsPath = "$dockerData\certs.d" +$clientCertsPath = "$userPath" +$rootCert = createCA "$dockerData\certs.d" + +createCerts $rootCert $serverCertsPath $serverName $ipAddresses $clientCertsPath +updateConfig "$dockerData\config\daemon.json" $serverCertsPath $enableLCOW $experimental + +Write-Host "Restarting Docker" +Restart-Service docker + +Write-Host "Opening Docker TLS port" +& netsh advfirewall firewall add rule name="Docker TLS" dir=in action=allow protocol=TCP localport=2376