By default Cloud Shell sessions run inside a container inside a Microsoft network separate from any resources you may have deployed in Azure. So what happens when you want to access services you have deployed inside a Virtual Network such as a private AKS cluster, a Virtual Machine or Private Endpoint enabled services?
Well as you can imagine the solution is to deploy Cloud Shell into an Azure Virtual Network in such a way that the container, it runs on, can acces your private resources. This solution also protects the backing Storage Account that Cloud Shell uses for you the user profiles and data so that you end up with a locked down environmemt.
The following diagram shows the solution archictecture:
If you need more information about the solution, the services and any limitations please check the documentation here: Cloud Shell in an Azure Virtual Network.
Now let’s se how can we use terraform to make your Cloud Shell private!
Deploy a Private Azure Cloud Shell with Terraform
If Cloud Shell has been used in the past, the existing clouddrive must be unmounted. To do this run
clouddrive unmount
from an active Cloud Shell session.
Create a provider.tf file with the following contents:
1terraform {
2 required_version = ">= 0.13.5"
3}
4
5provider "azurerm" {
6 version = "= 2.46.1"
7 features {}
8}
9
10provider "azuread" {
11 version = "= 1.3.0"
12}
13
14provider "http" {
15 version = "= 2.0.0"
16}
Note that we are using azurerm
to deploy Azure services, azuread
to get some Service Principal information and http
to get your current public ip address so that only you can reach your Cloud Shell.
Create a variables.tf file with the following contents:
1variable location {
2 default = "west europe"
3}
4
5variable resource_group {
6 default = "<resource group name>"
7}
8
9variable "vnet_name" {
10 default = "<vnet name>"
11}
12
13variable sa_name {
14 default = "<Backing Storage Account name>"
15}
16
17variable relay_name {
18 default = "<Azure Relay name>"
19}
Make sure you replace the placeholders with the default values you want to use.
Create a main.tf file with the following contents:
1# Get Azure Container Instance Service Principal. Amazing right? Cloud Shell uses this Service Principal!
2data "azuread_service_principal" "container" {
3 display_name = "Azure Container Instance Service"
4}
5
6# Create Resource Groupto hold the resources.
7resource "azurerm_resource_group" "rg" {
8 name = var.resource_group
9 location = var.location
10}
11
12# Create a VNET.
13resource "azurerm_virtual_network" "vnet" {
14 name = var.vnet_name
15 address_space = ["10.0.0.0/16"]
16 location = azurerm_resource_group.rg.location
17 resource_group_name = azurerm_resource_group.rg.name
18}
19
20# Create a Containers Subnet. Here is where Cloud Shell will run.
21resource "azurerm_subnet" "containers" {
22 name = "cloudshell-containers"
23 resource_group_name = azurerm_resource_group.rg.name
24 virtual_network_name = azurerm_virtual_network.vnet.name
25 address_prefixes = ["10.0.0.0/24"]
26
27 # Delegate the subnet to "Microsoft.ContainerInstance/containerGroups".
28 delegation {
29 name = "cloudshell-delegation"
30
31 service_delegation {
32 name = "Microsoft.ContainerInstance/containerGroups"
33 }
34 }
35
36 # Add service enpoint so Cloud Shell can reach Storage Accounts. At the moment the solution does not work with Private Enpoints for the Storage Account.
37 service_endpoints = ["Microsoft.Storage"]
38}
39
40# Create a subnet to host Azure Relay service.
41resource "azurerm_subnet" "relay" {
42 name = "relay"
43 resource_group_name = azurerm_resource_group.rg.name
44 virtual_network_name = azurerm_virtual_network.vnet.name
45 address_prefixes = ["10.0.1.0/24"]
46
47 enforce_private_link_endpoint_network_policies = true
48}
49
50# Create a network profile for the Cloud Shell containers.
51resource "azurerm_network_profile" "networkprofile" {
52 name = "cloudshell"
53 location = azurerm_resource_group.rg.location
54 resource_group_name = azurerm_resource_group.rg.name
55
56 container_network_interface {
57 name = "cloudshell-containers"
58
59 ip_configuration {
60 name = "ipconfig"
61 subnet_id = azurerm_subnet.containers.id
62 }
63 }
64}
65
66# Assign Network Contributor to the Azure Container Instance Service Principal.
67resource "azurerm_role_assignment" "network_contributor" {
68 scope = azurerm_network_profile.networkprofile.id
69 role_definition_name = "Network Contributor"
70 principal_id = data.azuread_service_principal.container.object_id
71}
72
73# Create an Azure Relay namespace.
74resource "azurerm_relay_namespace" "relay" {
75 name = var.relay_name
76 location = azurerm_resource_group.rg.location
77 resource_group_name = azurerm_resource_group.rg.name
78
79 sku_name = "Standard"
80}
81
82# Add a private enpoint to the Azure Relay namespace.
83resource "azurerm_private_endpoint" "endpoint" {
84 name = "cloudshell-privateendpoint"
85 location = azurerm_resource_group.rg.location
86 resource_group_name = azurerm_resource_group.rg.name
87 subnet_id = azurerm_subnet.relay.id
88
89 private_service_connection {
90 name = "privateendpoint"
91 private_connection_resource_id = azurerm_relay_namespace.relay.id
92 is_manual_connection = false
93 subresource_names = ["namespace"]
94 }
95}
96
97# Assign Contributor to the Azure Container Instance Service Principal.
98resource "azurerm_role_assignment" "contributor" {
99 scope = azurerm_relay_namespace.relay.id
100 role_definition_name = "Contributor"
101 principal_id = data.azuread_service_principal.container.object_id
102}
103
104# Create the Storage Account to hold the Cloud Shell profiles.
105resource "azurerm_storage_account" "sa" {
106 name = var.sa_name
107 resource_group_name = azurerm_resource_group.rg.name
108 location = azurerm_resource_group.rg.location
109 account_tier = "Standard"
110 account_replication_type = "GRS"
111 enable_https_traffic_only = true
112}
113
114# Create a file share to hold the user profiles.
115resource "azurerm_storage_share" "share" {
116 name = "profile"
117 storage_account_name = azurerm_storage_account.sa.name
118 quota = 6
119}
120
121# Get your current public IP.
122data "http" "current_public_ip" {
123 url = "http://ipinfo.io/json"
124 request_headers = {
125 Accept = "application/json"
126 }
127}
128
129# Protect the Storage Account setting the firewall.
130# This is done only after the file share is created.
131resource "azurerm_storage_account_network_rules" "sa_rules" {
132 resource_group_name = azurerm_resource_group.rg.name
133 storage_account_name = azurerm_storage_account.sa.name
134
135 default_action = "Deny"
136 virtual_network_subnet_ids = [azurerm_subnet.containers.id]
137
138 # ip_rules = [
139 # jsondecode(data.http.current_public_ip.body).ip
140 # ]
141
142 depends_on = [
143 azurerm_storage_share.share
144 ]
145}
146
147# Create DNS Zone for Relay
148resource "azurerm_private_dns_zone" "private" {
149 name = "privatelink.servicebus.windows.net"
150 resource_group_name = azurerm_resource_group.rg.name
151}
152
153# Create A record for the Relay
154resource "azurerm_private_dns_a_record" "relay" {
155 name = var.relay_name
156 zone_name = azurerm_private_dns_zone.private.name
157 resource_group_name = azurerm_resource_group.rg.name
158 ttl = 3600
159 records = [azurerm_private_endpoint.endpoint.private_service_connection[0].private_ip_address]
160}
161
162# Link the Private Zone with the VNet
163resource "azurerm_private_dns_zone_virtual_network_link" "relay" {
164 name = "relay"
165 resource_group_name = azurerm_resource_group.rg.name
166 private_dns_zone_name = azurerm_private_dns_zone.private.name
167 virtual_network_id = azurerm_virtual_network.vnet.id
168}
169
170# Open the relay firewall to local IP
171resource "null_resource" "open_relay_firewall" {
172 provisioner "local-exec" {
173 interpreter = ["powershell"]
174 command = "az rest --method put --uri '${azurerm_relay_namespace.relay.id}/networkrulesets/default?api-version=2017-04-01' --body '{\"properties\":{\"defaultAction\":\\\"Deny\\\",\"ipRules\":[{\"ipMask\":\\\"${jsondecode(data.http.current_public_ip.body).ip}\\\"}],\"virtualNetworkRules\":[],\"trustedServiceAccessEnabled\":false}}'"
175 }
176 depends_on = [
177 data.http.current_public_ip,
178 azurerm_relay_namespace.relay
179 ]
180}
Deploy the solution:
Run the following commands:
1terraform init
2terraform plan -out tf.plan
3terraform apply ./tf.plan
Hope it helps! Please find the complete code here
Comments