Skip to content

Commit 23003ba

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

File tree

5 files changed

+706
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)