├── docker-ci
├── jdk.ps1
├── git.ps1
├── docker.ps1
└── docker-ci.json
├── common
├── specialize-script.ps1
├── button-down.ps1
├── builder-userdata.ps1
└── provision.ps1
└── README.md
/docker-ci/jdk.ps1:
--------------------------------------------------------------------------------
1 | $ProgressPreference = 'SilentlyContinue'
2 | $ErrorActionPreference = 'Stop'
3 |
4 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
5 |
6 | $jdkVersion = "zulu8.19.0.1-jdk8.0.112-win_x64"
7 |
8 | Invoke-Webrequest "http://cdn.azul.com/zulu/bin/$jdkVersion.zip" -OutFile jdk.zip -UseBasicParsing
9 |
10 | New-Item -Type Directory C:\jdk-temp > $null
11 | Expand-Archive jdk.zip -DestinationPath C:\jdk-temp
12 | Remove-Item -Force jdk.zip
13 |
14 | New-Item -Type Directory C:\jdk > $null
15 | mv C:\jdk-temp\$jdkVersion\* C:\jdk\.
16 | Remove-Item -Force -Recurse C:\jdk-temp
17 |
18 | $newPath = 'C:\jdk\bin;' + [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::Machine)
19 | [Environment]::SetEnvironmentVariable("PATH", $newPath, [EnvironmentVariableTarget]::Machine)
20 |
21 | C:\jdk\bin\java.exe -version
22 |
23 | Stop-Transcript
24 |
--------------------------------------------------------------------------------
/docker-ci/git.ps1:
--------------------------------------------------------------------------------
1 | Param(
2 | [Parameter(Mandatory=$True)]
3 | [string]$gitVersion
4 | )
5 |
6 | $ProgressPreference = 'SilentlyContinue'
7 | $ErrorActionPreference = 'Stop'
8 |
9 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
10 |
11 | $otherGitVersion = $gitVersion -replace "\.windows\.\d*", ""
12 |
13 | Invoke-Webrequest "https://github.com/git-for-windows/git/releases/download/v$gitVersion/MinGit-$otherGitVersion-64-bit.zip" -OutFile git.zip -UseBasicParsing
14 | New-Item -Type Directory C:\git > $null
15 | Expand-Archive -Path git.zip -DestinationPath C:\git\.
16 | Remove-Item -Force git.zip
17 |
18 | $newPath = 'C:\git\cmd;' + [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::Machine)
19 | [Environment]::SetEnvironmentVariable("PATH", $newPath, [EnvironmentVariableTarget]::Machine)
20 |
21 | C:\git\cmd\git.exe --version
22 |
23 | Stop-Transcript
24 |
--------------------------------------------------------------------------------
/common/specialize-script.ps1:
--------------------------------------------------------------------------------
1 | $ProgressPreference = 'SilentlyContinue'
2 | $ErrorActionPreference = 'Stop'
3 |
4 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
5 |
6 | Push-Location C:\OpenSSH-Win64
7 | .\ssh-keygen -A
8 | .\ssh-add ssh_host_dsa_key
9 | .\ssh-add ssh_host_rsa_key
10 | .\ssh-add ssh_host_ecdsa_key
11 | .\ssh-add ssh_host_ed25519_key
12 | del *_key
13 | Pop-Location
14 |
15 | $keyPath = "C:\Users\Administrator\.ssh\authorized_keys"
16 | $keyUrl = "http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key"
17 |
18 | New-Item -ErrorAction Ignore -Type Directory C:\Users\Administrator\.ssh > $null
19 |
20 | $ErrorActionPreference = 'SilentlyContinue'
21 | Do {
22 | Start-Sleep 1
23 | Write-Output ("{0:u}: Trying to fetch key from metadata service" -f (Get-Date))
24 | Invoke-WebRequest $keyUrl -UseBasicParsing -OutFile $keyPath
25 | Write-Output $Error[0]
26 | } While ( -Not (Test-Path $keyPath) )
27 | $ErrorActionPreference = 'Stop'
28 | Write-Output ("{0:u}: Key successfully retrieved" -f (Get-Date))
29 |
30 | Stop-Transcript
31 |
--------------------------------------------------------------------------------
/docker-ci/docker.ps1:
--------------------------------------------------------------------------------
1 | Param(
2 | [Parameter(Mandatory=$True)]
3 | [string]$dockerVersion,
4 | [Parameter(Mandatory=$True)]
5 | [string]$dockerComposeVersion
6 | )
7 |
8 | $ProgressPreference = 'SilentlyContinue'
9 | $ErrorActionPreference = 'Stop'
10 |
11 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
12 |
13 | Stop-Service -Force docker
14 | dockerd --unregister-service
15 |
16 | Remove-Item -Force -Recurse $Env:ProgramFiles\docker
17 | Invoke-WebRequest "https://get.docker.com/builds/Windows/x86_64/docker-$dockerVersion-ce.zip" -UseBasicParsing -OutFile docker.zip
18 | Expand-Archive docker.zip -DestinationPath $Env:ProgramFiles
19 | Remove-Item -Force docker.zip
20 | Remove-Item -Force -Recurse $Env:ProgramFiles\docker\completion
21 |
22 | Invoke-WebRequest "https://github.com/docker/compose/releases/download/$dockerComposeVersion/docker-compose-Windows-x86_64.exe" -UseBasicParsing -OutFile $Env:ProgramFiles\docker\docker-compose.exe
23 |
24 | # REMARK: http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/windows-ami-version-history.html#win2k16-amis
25 | dockerd -H npipe:////./pipe/docker_engine -H 0.0.0.0:2375 --experimental --register-service
26 |
27 | Start-Service docker
28 |
29 | docker pull microsoft/nanoserver
30 | docker pull microsoft/windowsservercore
31 |
32 | Stop-Service -ErrorAction Ignore docker
33 |
34 | Stop-Transcript
35 |
--------------------------------------------------------------------------------
/common/button-down.ps1:
--------------------------------------------------------------------------------
1 | $ProgressPreference = 'SilentlyContinue'
2 | $ErrorActionPreference = 'Stop'
3 |
4 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
5 |
6 | Set-MpPreference -DisableRealtimeMonitoring $true
7 |
8 | $services = @(
9 | "diagnosticshub.standardcollector.service"
10 | "DiagTrack"
11 | "dmwappushservice"
12 | "lfsvc"
13 | "MapsBroker"
14 | "NetTcpPortSharing"
15 | "RemoteAccess"
16 | "RemoteRegistry"
17 | "SharedAccess"
18 | "TrkWks"
19 | "WbioSrvc"
20 | "XblAuthManager"
21 | "XblGameSave",
22 | "wuauserv"
23 | )
24 | foreach ($service in $services) {
25 | Get-Service -Name $service | Set-Service -StartupType Disabled
26 | }
27 |
28 | reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" /v AUOptions /t REG_DWORD /d 1 /f
29 | reg add "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /t REG_DWORD /d 1 /f
30 |
31 | schtasks /End /TN "\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser"
32 | schtasks /Change /TN "\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /DISABLE
33 |
34 | $ErrorActionPreference = 'SilentlyContinue'
35 | Uninstall-WindowsFeature Windows-Defender, Windows-Defender-Features, FS-SMB1, WoW64-Support, PowerShell-ISE, NET-WCF-Services45
36 | $ErrorActionPreference = 'Stop'
37 |
38 | Stop-Service -Force sshd, ssh-agent
39 | Restart-Computer -Force
40 |
41 | Stop-Transcript
--------------------------------------------------------------------------------
/common/builder-userdata.ps1:
--------------------------------------------------------------------------------
1 |
2 | $ProgressPreference = 'SilentlyContinue'
3 | $ErrorActionPreference = 'Stop'
4 |
5 | Write-Host "Disabling anti-virus monitoring"
6 | Set-MpPreference -DisableRealtimeMonitoring $true
7 |
8 | Write-Host "Downloading OpenSSH"
9 | Invoke-WebRequest "https://github.com/PowerShell/Win32-OpenSSH/releases/download/v0.0.8.0/OpenSSH-Win64.zip" -OutFile OpenSSH-Win64.zip -UseBasicParsing
10 |
11 | Write-Host "Expanding OpenSSH"
12 | Expand-Archive OpenSSH-Win64.zip C:\
13 | Remove-Item -Force OpenSSH-Win64.zip
14 |
15 | Write-Host "Disabling password authentication"
16 | Add-Content C:\OpenSSH-Win64\sshd_config "`nPasswordAuthentication no"
17 | Add-Content C:\OpenSSH-Win64\sshd_config "`nUseDNS no"
18 |
19 | Push-Location C:\OpenSSH-Win64
20 |
21 | Write-Host "Installing OpenSSH"
22 | & .\install-sshd.ps1
23 |
24 | Write-Host "Installing OpenSSH key auth"
25 | & .\install-sshlsa.ps1
26 |
27 | Write-Host "Generating host keys"
28 | .\ssh-keygen.exe -A
29 |
30 | Pop-Location
31 |
32 | $newPath = 'C:\OpenSSH-Win64;' + [Environment]::GetEnvironmentVariable("PATH", [EnvironmentVariableTarget]::Machine)
33 | [Environment]::SetEnvironmentVariable("PATH", $newPath, [EnvironmentVariableTarget]::Machine)
34 |
35 | Write-Host "Adding public key from instance metadata to authorized_keys"
36 | $keyPath = "~\.ssh\authorized_keys"
37 | $keyUrl = "http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key"
38 | New-Item -Type Directory ~\.ssh > $null
39 | $ErrorActionPreference = 'SilentlyContinue'
40 | Do {
41 | Start-Sleep 1
42 | Invoke-WebRequest $keyUrl -UseBasicParsing -OutFile $keyPath
43 | } While ( -Not (Test-Path $keyPath) )
44 | $ErrorActionPreference = 'Stop'
45 |
46 | Write-Host "Opening firewall port 22"
47 | New-NetFirewallRule -Protocol TCP -LocalPort 22 -Direction Inbound -Action Allow -DisplayName SSH
48 |
49 | Write-Host "Setting sshd service startup type to 'Automatic'"
50 | Set-Service sshd -StartupType Automatic
51 | Set-Service ssh-agent -StartupType Automatic
52 | Write-Host "Setting sshd service restart behavior"
53 | sc.exe failure sshd reset= 86400 actions= restart/500
54 |
55 | Restart-Computer -Force
56 |
--------------------------------------------------------------------------------
/common/provision.ps1:
--------------------------------------------------------------------------------
1 | $ProgressPreference = 'SilentlyContinue'
2 | $ErrorActionPreference = 'Stop'
3 |
4 | Start-Transcript -path ("C:\{0}.log" -f $MyInvocation.MyCommand.Name) -append
5 |
6 | Add-Type -AssemblyName System.Web
7 | $password = [System.Web.Security.Membership]::GeneratePassword(19, 10).replace("&", "a").replace("<", "b").replace(">", "c")
8 |
9 | $unattendPath = "$Env:ProgramData\Amazon\EC2-Windows\Launch\Sysprep\Unattend.xml"
10 | $xml = [xml](Get-Content $unattendPath)
11 | $targetElememt = $xml.unattend.settings.Where{($_.Pass -eq 'oobeSystem')}.component.Where{($_.name -eq 'Microsoft-Windows-Shell-Setup')}
12 |
13 | $autoLogonElement = [xml]('
14 |
15 | {0}
16 | true
17 |
18 | true
19 | Administrator
20 | ' -f $password)
21 | $targetElememt.appendchild($xml.ImportNode($autoLogonElement.DocumentElement, $true))
22 |
23 | $userAccountElement = [xml]('
24 |
25 | {0}
26 | true
27 |
28 |
29 |
30 |
31 | {0}
32 | true
33 |
34 | administrators
35 | Administrator
36 | Administrator
37 | Administrator User
38 |
39 |
40 | ' -f $password)
41 | $targetElememt.appendchild($xml.ImportNode($userAccountElement.DocumentElement, $true))
42 |
43 | $xml.Save($unattendPath)
44 |
45 | Add-Content $Env:ProgramData\Amazon\EC2-Windows\Launch\Sysprep\BeforeSysprep.cmd 'del "C:\Program Files\OpenSSH-Win64\*_key*"'
46 | Add-Content $Env:ProgramData\Amazon\EC2-Windows\Launch\Sysprep\BeforeSysprep.cmd 'del C:\Users\Administrator\.ssh\authorized_keys'
47 | Add-Content $Env:ProgramData\Amazon\EC2-Windows\Launch\Sysprep\BeforeSysprep.cmd 'del C:\provision.ps1'
48 |
49 | Add-Content $Env:ProgramData\Amazon\EC2-Windows\Launch\Sysprep\SysprepSpecialize.cmd 'powershell -ExecutionPolicy Bypass -NoProfile -c "& C:\specialize-script.ps1"'
50 |
51 | & $Env:ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 -Schedule
52 | & $Env:ProgramData\Amazon\EC2-Windows\Launch\Scripts\SysprepInstance.ps1
53 |
54 | Stop-Transcript
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Packer template for building Windows with SSH public key authentication
2 |
3 | This Packer template builds AWS AMIs that support login with SSH public key auth. When using instances created from this template you don't have to ever retrieve, worry about or use a Windows password. You can log in with SSH directly using your SSH key.
4 |
5 | The public key is provisioned using the AWS EC2 key infrastructure. The template doesn't use WinRM at all, SSH is also used by the Packer builder.
6 |
7 | # How to use
8 |
9 | ## Build AMI
10 |
11 | ```
12 | packer build -var "branch=$(git rev-parse --abbrev-ref HEAD)" -var 'docker_version=17.03.0' -var 'docker_compose_version=1.11.2' -var 'git_version=2.12.2.windows.1' .\docker-ci\docker-ci.json
13 | ...
14 | --> amazon-ebs: AMIs were created:
15 | us-west-2:
16 | ```
17 |
18 | ## Create instance from AMI
19 |
20 | ### Using AWS CLI
21 |
22 | ```
23 | $instanceid = aws ec2 run-instances --image-id --block-device-mapping '[{""DeviceName"": ""/dev/sda1"", ""Ebs"": {""VolumeSize"": 100, ""VolumeType"": ""gp2""}}]' --ebs-optimized --count 1 --instance-type c4.xlarge --key-name --security-group-ids --query "Instances[*].InstanceId" --output=text
24 | aws ec2 describe-instances --instance-ids $instanceid --query "Reservations[*].Instances[*].PublicIpAddress" --output=text ; `
25 |
26 | ```
27 |
28 | ## Log in
29 |
30 | Wait for the instance to come up.
31 |
32 | ```
33 | ssh -i Administrator@
34 | ```
35 |
36 | # Notes on Building Windows AMIs with OpenSSH
37 |
38 | The chief concern when using these templates and building on AMIs generated with them, is making sure that the public key from the AWS metadata service is writte to the Administrator user's `~\.ssh\authorized_keys` directory on boot. There are two general ways to achieve this:
39 |
40 | * Do `sysprep` and make sure the key is written when an instance is launched from the sysprepped AMI. The key will only be re-written if another sysprep is done.
41 | * Pass in `userdata` script that writes the key from metadata. This has to be passed on every boot.
42 |
43 | # TODO
44 |
45 | * Clean up the Packer builder to better support Windows
46 | * Don't restart at the end of builder-userdata.ps1 if possible
47 | * Figure out startup script that fetches public key from metadata API (perhaps using local group policy)
48 | * Disable WinRM and RDP after first boot (probably requires disabling the EC2 instance initialization since it relies on those services)
49 |
50 | # Resources
51 |
52 | * http://jen20.com/2015/04/02/windows-amis-without-the-tears.html
53 | * http://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2launch.html
54 | * https://github.com/jhowardmsft/docker-w2wCIScripts (Jenkins setup)
55 |
--------------------------------------------------------------------------------
/docker-ci/docker-ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": {
3 | "instance_type": "t2.xlarge",
4 | "region": "us-west-2",
5 | "branch": "",
6 | "docker_version": null,
7 | "docker_compose_version": null,
8 | "git_version": null
9 | },
10 | "builders": [
11 | {
12 | "type": "amazon-ebs",
13 | "source_ami_filter": {
14 | "filters": {
15 | "virtualization-type": "hvm",
16 | "name": "*Windows_Server-2016-English-Full-Containers-*",
17 | "root-device-type": "ebs"
18 | },
19 | "owners": [
20 | "801119661308"
21 | ],
22 | "most_recent": true
23 | },
24 | "region": "{{user `region`}}",
25 | "instance_type": "{{user `instance_type`}}",
26 | "ami_name": "windows-docker-ci-{{user `branch` | clean_ami_name}}{{timestamp}}",
27 | "user_data_file": "{{template_dir}}/../common/builder-userdata.ps1",
28 | "communicator": "ssh",
29 | "ssh_username": "Administrator",
30 | "ssh_timeout": "30m",
31 | "disable_stop_instance": "true",
32 | "launch_block_device_mappings": [
33 | {
34 | "device_name": "/dev/sda1",
35 | "volume_size": 50,
36 | "volume_type": "gp2",
37 | "delete_on_termination": true
38 | }
39 | ]
40 | }
41 | ],
42 | "_comment": "The shell provisioner doesn't work well with Windows, so some hackery",
43 | "provisioners": [
44 | {
45 | "type": "shell",
46 | "execute_command": "powershell {{ .Path }}",
47 | "remote_path": "C:\\button-down.ps1",
48 | "script": "{{template_dir}}/../common/button-down.ps1",
49 | "binary": "true",
50 | "skip_clean": "true"
51 | },
52 | {
53 | "type": "shell",
54 | "execute_command": "powershell {{ .Path }}",
55 | "remote_path": "C:\\jdk.ps1",
56 | "script": "{{template_dir}}/jdk.ps1",
57 | "binary": "true",
58 | "pause_before": "20s",
59 | "skip_clean": "true"
60 | },
61 | {
62 | "type": "shell",
63 | "execute_command": "powershell {{ .Path }} -gitVersion {{user `git_version`}}",
64 | "remote_path": "C:\\git.ps1",
65 | "script": "{{template_dir}}/git.ps1",
66 | "binary": "true",
67 | "skip_clean": "true"
68 | },
69 | {
70 | "type": "shell",
71 | "execute_command": "powershell -File {{ .Path }} -dockerVersion {{user `docker_version`}} -dockerComposeVersion {{user `docker_compose_version`}}",
72 | "remote_path": "C:\\docker.ps1",
73 | "script": "{{template_dir}}/docker.ps1",
74 | "binary": "true",
75 | "skip_clean": "true"
76 | },
77 | {
78 | "type": "shell",
79 | "execute_command": "exit",
80 | "script": "{{template_dir}}/../common/specialize-script.ps1",
81 | "remote_path": "C:\\specialize-script.ps1",
82 | "binary": "true",
83 | "skip_clean": "true"
84 | },
85 | {
86 | "type": "shell",
87 | "execute_command": "powershell {{ .Path }}",
88 | "remote_path": "C:\\provision.ps1",
89 | "script": "{{template_dir}}/../common/provision.ps1",
90 | "binary": "true",
91 | "skip_clean": "true"
92 | }
93 | ]
94 | }
95 |
--------------------------------------------------------------------------------