├── 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</PlainText> 17 | </Password> 18 | <Enabled>true</Enabled> 19 | <Username>Administrator</Username> 20 | </AutoLogon>' -f $password) 21 | $targetElememt.appendchild($xml.ImportNode($autoLogonElement.DocumentElement, $true)) 22 | 23 | $userAccountElement = [xml]('<UserAccounts xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> 24 | <AdministratorPassword> 25 | <Value>{0}</Value> 26 | <PlainText>true</PlainText> 27 | </AdministratorPassword> 28 | <LocalAccounts> 29 | <LocalAccount wcm:action="add"> 30 | <Password> 31 | <Value>{0}</Value> 32 | <PlainText>true</PlainText> 33 | </Password> 34 | <Group>administrators</Group> 35 | <DisplayName>Administrator</DisplayName> 36 | <Name>Administrator</Name> 37 | <Description>Administrator User</Description> 38 | </LocalAccount> 39 | </LocalAccounts> 40 | </UserAccounts>' -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: <ami-id> 16 | ``` 17 | 18 | ## Create instance from AMI 19 | 20 | ### Using AWS CLI 21 | 22 | ``` 23 | $instanceid = aws ec2 run-instances --image-id <ami-id> --block-device-mapping '[{""DeviceName"": ""/dev/sda1"", ""Ebs"": {""VolumeSize"": 100, ""VolumeType"": ""gp2""}}]' --ebs-optimized --count 1 --instance-type c4.xlarge --key-name <key-name> --security-group-ids <security-group-id> --query "Instances[*].InstanceId" --output=text 24 | aws ec2 describe-instances --instance-ids $instanceid --query "Reservations[*].Instances[*].PublicIpAddress" --output=text ; ` 25 | <ip-address> 26 | ``` 27 | 28 | ## Log in 29 | 30 | Wait for the instance to come up. 31 | 32 | ``` 33 | ssh -i <key-path> Administrator@<ip-address> 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 | --------------------------------------------------------------------------------