Skip to content

Commit 1815d02

Browse files
committed
Add vm-tailscale lab: Azure Linux VM with Tailscale SSH
1 parent 6a0e4da commit 1815d02

File tree

5 files changed

+696
-0
lines changed

5 files changed

+696
-0
lines changed

linux/vm-tailscale/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Linux VM in Azure with Tailscale SSH
2+
3+
[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fopen-source-labs%2Fmain%2Flinux%2Fvm-tailscale%2Fvm.json)
4+
5+
Deploy an Azure Linux VM with [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh/) — no public SSH ports, no SSH keys to manage. Connect securely over your [Tailscale tailnet](https://tailscale.com/kb/1136/tailnet/).
6+
7+
## Prerequisites
8+
9+
- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli)
10+
- A [Tailscale](https://tailscale.com/) account
11+
- A Tailscale [Auth key](https://login.tailscale.com/admin/settings/keys) (one-off recommended)
12+
13+
## Quick Start
14+
15+
Create a resource group:
16+
17+
```bash
18+
RESOURCE_GROUP='260200-linux-ts'
19+
LOCATION='eastus'
20+
21+
az group create \
22+
--name $RESOURCE_GROUP \
23+
--location $LOCATION
24+
```
25+
26+
Deploy directly from URL (no local files needed):
27+
28+
```bash
29+
az deployment group create \
30+
--resource-group $RESOURCE_GROUP \
31+
--template-uri https://raw.githubusercontent.com/Azure-Samples/open-source-labs/main/linux/vm-tailscale/vm.json \
32+
--parameters \
33+
cloudInit='tailscale' \
34+
env='{"tskey":"<YOUR_TAILSCALE_AUTH_KEY>"}'
35+
```
36+
37+
Or deploy using a local file:
38+
39+
```bash
40+
az deployment group create \
41+
--resource-group $RESOURCE_GROUP \
42+
--template-file vm.bicep \
43+
--parameters \
44+
cloudInit='tailscale' \
45+
env='{"tskey":"<YOUR_TAILSCALE_AUTH_KEY>"}'
46+
```
47+
48+
Once deployed, SSH via Tailscale (with [MagicDNS](https://tailscale.com/kb/1081/magicdns/)):
49+
50+
```bash
51+
ssh azureuser@vm1
52+
```
53+
54+
Or by Tailscale IP:
55+
56+
```bash
57+
ssh azureuser@<tailscale-ip>
58+
```
59+
60+
## Parameters
61+
62+
| Parameter | Default | Description |
63+
|-----------|---------|-------------|
64+
| `vmName` | `vm1` | VM name (also the Tailscale hostname) |
65+
| `vmSize` | `Standard_B2s_v2` | VM size (see VM Sizes below) |
66+
| `osImage` | `Ubuntu 24.04-LTS` | OS image (`Ubuntu 24.04-LTS (arm64)` for Arm) |
67+
| `osDiskSize` | `64` | OS disk size in GB |
68+
| `cloudInit` | `none` | `tailscale` or `none` |
69+
| `env` | `{}` | JSON object with `tskey` for Tailscale auth key |
70+
| `adminUsername` | `azureuser` | VM admin username |
71+
| `adminPasswordOrKey` | _(placeholder)_ | SSH public key (not needed with Tailscale SSH) |
72+
73+
## VM Sizes
74+
75+
| Size | vCPUs | RAM | Arch | Notes |
76+
|------|-------|-----|------|-------|
77+
| `Standard_B2ts_v2` | 2 | 1 GiB | x64 | Free tier eligible |
78+
| `Standard_B2ls_v2` | 2 | 4 GiB | x64 | |
79+
| `Standard_B2s_v2` | 2 | 8 GiB | x64 | Default |
80+
| `Standard_B4ls_v2` | 4 | 8 GiB | x64 | |
81+
| `Standard_B4s_v2` | 4 | 16 GiB | x64 | |
82+
| `Standard_D2s_v5` | 2 | 8 GiB | x64 | |
83+
| `Standard_D4s_v5` | 4 | 16 GiB | x64 | |
84+
| `Standard_D2ps_v5` | 2 | 8 GiB | arm64 | Ampere Altra |
85+
| `Standard_D4ps_v5` | 4 | 16 GiB | arm64 | Ampere Altra |
86+
87+
## Arm64 VMs
88+
89+
To deploy on [Ampere Altra Arm64-based VMs](https://azure.microsoft.com/blog/azure-virtual-machines-with-ampere-altra-arm-based-processors-generally-available/):
90+
91+
```bash
92+
az deployment group create \
93+
--resource-group $RESOURCE_GROUP \
94+
--template-uri https://raw.githubusercontent.com/Azure-Samples/open-source-labs/main/linux/vm-tailscale/vm.json \
95+
--parameters \
96+
vmName='arm1' \
97+
cloudInit='tailscale' \
98+
env='{"tskey":"<YOUR_TAILSCALE_AUTH_KEY>"}' \
99+
vmSize='Standard_D2ps_v5' \
100+
osImage='Ubuntu 24.04-LTS (arm64)'
101+
```
102+
103+
## Cleanup
104+
105+
Empty the resource group (keeps the group, removes all resources):
106+
107+
```bash
108+
az deployment group create \
109+
--resource-group $RESOURCE_GROUP \
110+
--template-uri https://raw.githubusercontent.com/Azure-Samples/open-source-labs/main/linux/vm-tailscale/empty.json \
111+
--mode Complete
112+
```

linux/vm-tailscale/empty.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

linux/vm-tailscale/empty.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
3+
"contentVersion": "1.0.0.0",
4+
"metadata": {
5+
"_generator": {
6+
"name": "bicep",
7+
"version": "0.40.2.10011",
8+
"templateHash": "1185148849829504269"
9+
}
10+
},
11+
"resources": []
12+
}

linux/vm-tailscale/vm.bicep

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
@description('The name of your Virtual Machine.')
2+
param vmName string = 'vm1'
3+
4+
@description('The Virtual Machine size.')
5+
@allowed([
6+
'Standard_B2ts_v2'
7+
'Standard_B2ls_v2'
8+
'Standard_B2s_v2'
9+
'Standard_B4ls_v2'
10+
'Standard_B4s_v2'
11+
'Standard_D2s_v5'
12+
'Standard_D4s_v5'
13+
'Standard_D2ps_v5'
14+
'Standard_D4ps_v5'
15+
])
16+
param vmSize string = 'Standard_B2s_v2'
17+
18+
@description('The Storage Account Type for OS and Data disks.')
19+
@allowed([
20+
'Standard_LRS'
21+
'Premium_LRS'
22+
])
23+
param diskAccountType string = 'Premium_LRS'
24+
25+
@description('The OS Disk size.')
26+
@allowed([
27+
1024
28+
512
29+
256
30+
128
31+
64
32+
32
33+
])
34+
param osDiskSize int = 64
35+
36+
@description('The OS image for the VM.')
37+
@allowed([
38+
'Ubuntu 24.04-LTS'
39+
'Ubuntu 24.04-LTS (arm64)'
40+
])
41+
param osImage string = 'Ubuntu 24.04-LTS'
42+
43+
@description('Location for all resources.')
44+
param location string = resourceGroup().location
45+
46+
@description('Username for the Virtual Machine.')
47+
param adminUsername string = 'azureuser'
48+
49+
@secure()
50+
@description('SSH Key for the Virtual Machine.')
51+
param adminPasswordOrKey string = ''
52+
53+
@description('Deploy with Tailscale SSH.')
54+
@allowed([
55+
'none'
56+
'tailscale'
57+
])
58+
param cloudInit string = 'none'
59+
60+
@description('Environment variables as JSON object (e.g. {"tskey":"tskey-auth-..."}).')
61+
@secure()
62+
param env object = {}
63+
64+
var rand = substring(uniqueString(resourceGroup().id), 0, 6)
65+
var vnetName = '${resourceGroup().name}-vnet'
66+
var subnetName = 'default'
67+
var keyData_var = adminPasswordOrKey != '' ? adminPasswordOrKey : 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3gkRpKwprN00sT7yekr0xO0F+uTllDua02puhu1v0zGu3aENvUsygBHJiTy+flgrO2q3mY9F5/D67+WHDeSpr5s71UtnbzMxTams89qmo+raTm+IqjzdNujaWf0/pbT6JUkQq0fR0BfIvg3/7NTXhlzjmCOP2EpD91LzN6b5jAm/5hXr0V5mcpERo8kk2GWxjKmwmDOV+huH1DIFDpMxT3WzR2qvZp1DZbNSYmKkrite3FHlPGLXA1I3bRQT+iTj8vRGpxOPSiMdPK4RNMEZVXSGQ3OZbSl2FBCbd/tdJ1idKo8/ZCkHxdh9/em28/yfPUK0D164shgiEdIkdOQJv'
68+
var publicIPAddressName = '${vmName}-ip'
69+
var networkInterfaceName = '${vmName}-nic'
70+
var ipConfigName = '${vmName}-ipconfig'
71+
var subnetAddressPrefix = '10.1.0.0/24'
72+
var addressPrefix = '10.1.0.0/16'
73+
74+
75+
var cloudInitTailscale = '''
76+
#cloud-config
77+
# vim: syntax=yaml
78+
79+
packages:
80+
- jq
81+
- curl
82+
83+
write_files:
84+
- path: /home/azureuser/env.json
85+
content: {0}
86+
encoding: b64
87+
- path: /home/azureuser/tailscale.sh
88+
content: |
89+
curl -fsSL https://tailscale.com/install.sh | sh
90+
sudo tailscale up --ssh --authkey "$1"
91+
92+
runcmd:
93+
- cd /home/azureuser/
94+
- bash tailscale.sh "$(jq -r '.tskey' env.json)"
95+
- echo $(date) > hello.txt
96+
- chown -R azureuser:azureuser /home/azureuser/
97+
'''
98+
99+
var cloudInitTailscaleFormat = format(cloudInitTailscale, base64(string(env)))
100+
101+
var kvCloudInit = {
102+
none: null
103+
tailscale: base64(cloudInitTailscaleFormat)
104+
}
105+
106+
var kvImageReference = {
107+
'Ubuntu 24.04-LTS': {
108+
publisher: 'canonical'
109+
offer: 'ubuntu-24_04-lts'
110+
sku: 'server'
111+
version: 'latest'
112+
}
113+
'Ubuntu 24.04-LTS (arm64)': {
114+
publisher: 'canonical'
115+
offer: 'ubuntu-24_04-lts'
116+
sku: 'server-arm64'
117+
version: 'latest'
118+
}
119+
}
120+
121+
// Only Tailscale UDP port — SSH access is via Tailscale, not public internet
122+
var nsgSecurityRules = [
123+
{
124+
name: 'Port_41641'
125+
properties: {
126+
protocol: 'Udp'
127+
sourcePortRange: '*'
128+
destinationPortRange: '41641'
129+
sourceAddressPrefix: 'Internet'
130+
destinationAddressPrefix: '*'
131+
access: 'Allow'
132+
priority: 100
133+
direction: 'Inbound'
134+
}
135+
}
136+
]
137+
138+
resource identityName 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
139+
name: '${resourceGroup().name}-identity'
140+
location: location
141+
}
142+
143+
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
144+
name: '${resourceGroup().name}-nsg'
145+
location: location
146+
properties: {
147+
securityRules: nsgSecurityRules
148+
}
149+
}
150+
151+
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
152+
name: vnetName
153+
location: location
154+
properties: {
155+
addressSpace: {
156+
addressPrefixes: [
157+
addressPrefix
158+
]
159+
}
160+
subnets: [
161+
{
162+
name: subnetName
163+
properties: {
164+
addressPrefix: subnetAddressPrefix
165+
}
166+
}
167+
]
168+
}
169+
}
170+
171+
resource publicIP 'Microsoft.Network/publicIPAddresses@2023-11-01' = {
172+
name: publicIPAddressName
173+
location: location
174+
sku: {
175+
name: 'Standard'
176+
}
177+
properties: {
178+
publicIPAllocationMethod: 'Static'
179+
dnsSettings: {
180+
domainNameLabel: toLower('${vmName}-${rand}')
181+
}
182+
}
183+
}
184+
185+
resource nic 'Microsoft.Network/networkInterfaces@2023-11-01' = {
186+
name: networkInterfaceName
187+
location: location
188+
properties: {
189+
ipConfigurations: [
190+
{
191+
name: ipConfigName
192+
properties: {
193+
subnet: {
194+
id: vnet.properties.subnets[0].id
195+
}
196+
privateIPAllocationMethod: 'Dynamic'
197+
publicIPAddress: { id: publicIP.id }
198+
}
199+
}
200+
]
201+
networkSecurityGroup: {
202+
id: nsg.id
203+
}
204+
}
205+
}
206+
207+
resource vm 'Microsoft.Compute/virtualMachines@2024-03-01' = {
208+
name: vmName
209+
location: location
210+
identity: {
211+
type: 'UserAssigned'
212+
userAssignedIdentities: {
213+
'${identityName.id}': {}
214+
}
215+
}
216+
properties: {
217+
hardwareProfile: {
218+
vmSize: vmSize
219+
}
220+
storageProfile: {
221+
osDisk: {
222+
managedDisk: {
223+
storageAccountType: diskAccountType
224+
}
225+
name: '${vmName}-osdisk1'
226+
diskSizeGB: osDiskSize
227+
createOption: 'FromImage'
228+
}
229+
imageReference: kvImageReference[osImage]
230+
}
231+
networkProfile: {
232+
networkInterfaces: [
233+
{
234+
id: nic.id
235+
}
236+
]
237+
}
238+
osProfile: {
239+
computerName: vmName
240+
customData: kvCloudInit[cloudInit]
241+
adminUsername: adminUsername
242+
linuxConfiguration: {
243+
disablePasswordAuthentication: true
244+
ssh: {
245+
publicKeys: [
246+
{
247+
keyData: keyData_var
248+
path: '/home/${adminUsername}/.ssh/authorized_keys'
249+
}
250+
]
251+
}
252+
}
253+
}
254+
}
255+
}
256+
257+
output adminUsername string = adminUsername
258+
output hostname string = publicIP.properties.dnsSettings.fqdn
259+
output sshCommand string = 'ssh ${adminUsername}@${publicIP.properties.dnsSettings.fqdn}'

0 commit comments

Comments
 (0)