Securing Access from a Public IP
Table of Contents
In this blog post I’ll share some tips on how you can securely use your cloud resources from a public IP, such as your home connection. I’ll be using Azure but the principles will apply to other vendor clouds.
The specific use case we’ll consider is MSDN subscription access, hence the desire to avoid the cost of running a dedicated Azure Firewall, whilst not leaving ourselves vulnerable.
The assumptions are we will be signed in to our console session and running the code locally with the credentials, and from the location from which we wish to access the resources/portal.
TLDR: We will lock down to our (dynamically resolved) current user credential and current IP address.
Terraform Datasources
We’ll start with a quick review of Terraform datasources, which are objects you wish to reference in your code, that are pre-existing and created elsewhere. An important feature of a datasource is that it is dynamic in nature, as such the object referenced will be queried and the latest values retrieved and stored in state each time the code is planned/applied.
The arguments Terraform stores for each datasource varies dependant on the resource type, so check the Terraform website for this, but essentially you specify a couple of argument values, such an object name and location (i.e. enough to identify the object in question) and the datasource retrieves a more comprehensive set of values and stores them in state to allow you to reference these as arguments in your code (such as a current value or ID).
Enumerating Datasources
The Terraform console can also be used enumerate a datasource, useful to see what arguments are held in state and the exact format in which they are stored, this will help you use them in your code. We’ll look at specific examples of this in the following sub-sections.
Current Config
The first useful example of this is azurerm_client_config
data "azurerm_client_config" "current_config" {}
...
As this resolves the current Tenant-ID and Object-ID, these can be used on the built-in firewalls of PaaS services to set access for our current user. Here’s an Azure KeyVault example:
resource "azurerm_key_vault_access_policy" "dave_msdn_kv_access_policy_myuser" {
key_vault_id = azurerm_key_vault.dave_msdn.id
tenant_id = data.azurerm_client_config.current_config.tenant_id
object_id = data.azurerm_client_config.current_config.object_id
key_permissions = ["Get"]
secret_permissions = ["Get", "Set"]
}
...
Current IP Address
The really useful, (at least for MSDN) bit is the local IP address. Here we resolve this via a web-service, we chose this one specifically for its clean output, it literally returns our local public IP with no other formatting or characters.
data "http" "mycurrentexternalipaddress" {
url = "http://whatismyip.akamai.com"
}
...
We can check this in the Terraform console, by just entering the name of the datasource, as it is stored in state. We can retrieve this by listing the contents of state with the command ‘terraform state list’:
❯ terraform console
> data.http.mycurrentexternalipaddress
{
"body" = "88.33.97.86"
"ca_cert_pem" = tostring(null)
"id" = "http://whatismyip.akamai.com"
"insecure" = tobool(null)
"method" = tostring(null)
"request_body" = tostring(null)
"request_headers" = tomap(null) /* of string */
"response_body" = "88.33.97.86"
"response_headers" = tomap({
"Cache-Control" = "max-age=0, no-cache, no-store"
"Content-Length" = "12"
"Content-Type" = "text/html"
"Date" = "Mon, 2 Jan 2023 16:02:58 GMT"
"Expires" = "Mon, 2 Jan 2023 16:02:58 GMT"
"Pragma" = "no-cache"
})
"status_code" = 200
"url" = "http://whatismyip.akamai.com"
}
>
You can see a bunch of information is stored in state, but we’re only interested in the response_body. We can reference this in our code to lock down a PaaS service as shown below, we’ll stick with the KeyVault example:
resource "azurerm_key_vault" "dave_msdn" {
...
network_acls {
default_action = "Deny"
bypass = "AzureServices" # https://learn.microsoft.com/en-us/azure/key-vault/general/overview-vnet-service-endpoints#trusted-services
virtual_network_subnet_ids = []
ip_rules = [join("", [data.http.mycurrentexternalipaddress.response_body, "/32"])]
}
}
Here you can see we use the Join function to add a /32 to the string as required by this resource so we specify just our single IP, this varies per resource so check the help pages, for examples storage accounts use subtly/annoyingly different syntax!
Other Use Cases
Another obvious use case would be for an NSG rule on a VM where you enable RDP access via a public IP.
Limitations
If you have a regular home-user ISP setup, your local IP address will change from time-to-time. So, you will need to re-run your code to regain access. But given the one-to-one security you can achieve at zero cost I think the pros outweight the cons, what do you think?