Auto-add NSG rules

We use the built-in Windows VPN client to provide access to legacy applications that don't meet the security or usability requirements to be exposed to the internet. One of those applications just didn't work well over a VPN for various reasons but worked great over the internet. The application's network traffic was encrypted but the authentication was only username/password so just publishing it on the internet wasn't a good enough solution.

Allowing access to the resource over the internet from a specific IP address would meet the security requirements of the application but keeping up with manually adding everyone's home IP address to the Azure Network Security Group (NSG) would be impossible.

My solution in this case was to use a combination of the following resources to automate the process of adding a user's IP address to the NSG on a temporary basis:
  • Microsoft Power Automation (Flow)
  • Azure Automation Accounts
  • Powershell
  • Microsoft Graph API
Since our users are required to use multi-factor authentication to authenticate to Office 365/Azure I feel comfortable using the IP address from the sign-in logs as a trusted IP. Here's an overview:
  1. User logs into Office 365
  2. User triggers a Power Automation job (no input required by user)
  3. Power Automation job gets the user ID and passes it to a webhook which triggers an Azure Automation runbook
  4. Azure Automation runbook queries Microsoft Graph to obtain the user's sign-on IP address
  5. Azure Automation runbook runs a Powershell command to update the NSG protecting the resource
  6. A weekly Runbook runs to clean up the temporary rules in the NSG
The Power Automation job is very simple. All it does is get the ID of the logged in user and posts to a webhook for the Azure Automation Runbook:

The NSG rule creator Runbook code:

Param(
[object]$WebhookData
)
$WebhookBody=$WebHookData.RequestBody
$Input = (ConvertFrom-Json -InputObject $WebhookBody)
$userid = $Input.userid
$VerbosePreference = ‘Continue’
function Connect-MSGraphAPI {
# Get the stored credentials from Azure Automation Credential Manager to use in the PowerShell Runbook for MS Graph authentication
$graphClientCreds = Get-AutomationPSCredential -Name 'XXXXX'
$graphClientPW = $graphClientCreds.GetNetworkCredential().Password
$graphUserCreds = Get-AutomationPSCredential -Name 'XXXXX'
$graphUserPW = $graphUserCreds.GetNetworkCredential().Password
# MS Graph Token Request Body for Azure PowerShell Runbook:
$graphTokenRequestBody = @{
"scope" = "https://graph.microsoft.com/.default";
"grant_type" = "password";
"client_id" = "$($graphClientCreds.UserName)";
"client_secret" = "$graphClientPW";
"username" = "$($graphUserCreds.UserName)";
"password" = "$graphUserPW";
}
# Define the URI using your own Azure tenant name:
$tenantName = "your-Azure-tenant-name"
$graphRequestUri = "https://login.microsoftonline.com/$tenantName.onmicrosoft.com/oauth2/v2.0/token"
# Make sure the error variable starts empty:
$GraphAPITokenRequestError = $null
# Send the Post request to Microsoft and receive an OAuth2 token:
$script:GraphAPIAuthResult = (Invoke-RestMethod -Method Post -Uri $graphRequestUri -Body $graphTokenRequestBody -ErrorAction SilentlyContinue -ErrorVariable GraphAPITokenRequestError)
# If there's an error requesting the token, say so, display the error, and break:
if ($GraphAPITokenRequestError) {
Write-Output "FAILED - Unable to retreive MS Graph API Authentication Token - $($GraphAPITokenRequestError)"
Break
}
}
# CHECK AUTH TOKEN STATUS, GET ANOTHER IF CLOSE TO EXPIRATION, SET HEADERS WITH IT IF ALL IS WELL
function Invoke-GraphAPIAuthTokenCheck {
$currentDateTimePlusTen = (Get-Date).AddMinutes(10)
if ($script:GraphAPIAuthResult) {
if (!($currentDateTimePlusTen -le $script:GraphAPIAuthResult.expiration_time)) {
Connect-MSGraphAPI
Set-GraphAPIRequestHeader
} else {
Set-GraphAPIRequestHeader
}
} else {
Connect-MSGraphAPI
Invoke-GraphAPIAuthTokenCheck
}
}
# SET THE HEADER FOR ALL MS GRAPH API REQUESTS
function Set-GraphAPIRequestHeader {
$script:graphAPIReqHeader = @{
'Content-Type'='application/json'
Authorization = "Bearer $($script:GraphAPIAuthResult.access_token)"
Host = "graph.microsoft.com"
}
}
function sendmail ([string]$subject, [string]$emailBody) {
$emailAddress = ""
$cred = Get-AutomationPSCredential -Name ""
Send-MailMessage -to $emailAddress -From "" -SmtpServer "smtp.office365.com" -Port 587 -UseSsl -Credential $cred -Subject $subject -Body $emailBody
}
# Call the function
Invoke-GraphAPIAuthTokenCheck
# We have a token so now let's do stuff with it
$headers = $script:graphAPIReqHeader
$headers
#$graphApiVersion = "v1.0"
# Fire Graph command to get most recent IP address from sign-in logs
$Uri = "https://graph.microsoft.com/beta/auditLogs/signIns?api-version=beta&`$filter=(userId%20eq%20%27$userid%27)&`$top=1"
$uri
$userProfile = (Invoke-WebRequest -Uri $uri -Method "GET" -Headers $headers -UseBasicParsing | convertfrom-Json).value
foreach ($identifier in $userProfile) {
$ipaddress = $identifier.ipaddress
$username = $identifier.userPrincipalName
}
# Run powershell to update the NSG and add the IP address retrieved to a new rule
$nsg = "NSG name"
$nsgResourceGroup = "NSG resource group"
$destip = "destination IP address of resource"
$destport = ""
$connectionName = "AzureRunAsConnection"
try
{
# Get the connection "AzureRunAsConnection "
$servicePrincipalConnection=Get-AutomationConnection -Name $connectionName
"Logging in to Azure..."
Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch {
if (!$servicePrincipalConnection)
{
$ErrorMessage = "Connection $connectionName not found."
throw $ErrorMessage
} else{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
# Get the priority of the most recent addition and increment. The priority values are stored in an Azure Automation account variable
$priority = Get-AutomationVariable -Name "your-variable-name"
$newPriority = $priority + 1
$date = get-date -Format ddmmyyhhmmss
Get-AzureRmNetworkSecurityGroup -Name $nsg -ResourceGroupName $nsgResourceGroup |
Add-AzureRmNetworkSecurityRuleConfig -Name "temp_rule$date" -Description "Allow from $ipaddress for $username" -Access `
Allow -Protocol Tcp -Direction Inbound -Priority $newPriority -SourceAddressPrefix $ipaddress `
-SourcePortRange * -DestinationAddressPrefix $destip -DestinationPortRange $destport | `
Set-AzureRmNetworkSecurityGroup `
Set-AutomationVariable -Name "your-variable-name" -value $newPriority
$emailBody = $ipaddress, $username
$subject = "User IP address retrieved"
sendmail $subject $emailBody
view raw NSGWhitelist hosted with ❤ by GitHub

The NSG rule cleanup Runbook which runs on a weekly schedule:

$connectionName = "AzureRunAsConnection"
try
{
# Get the connection "AzureRunAsConnection "
$servicePrincipalConnection=Get-AutomationConnection -Name $connectionName
"Logging in to Azure..."
Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch {
if (!$servicePrincipalConnection)
{
$ErrorMessage = "Connection $connectionName not found."
throw $ErrorMessage
} else{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
# Name of the NSG containing the temporary rules
$nsgName = "your-NSG-name"
$resourceGroupName = "your-NSG-resource-group-name"
$nsgObject = get-AzureRmNetworkSecurityGroup -name $nsgName -resourcegroupname $resourceGroupName
$rules = $nsgObject | get-azurermnetworksecurityruleconfig
foreach ($rule in $rules) {
if ($rule.name -like "temp*") {
Remove-AzureRmNetworkSecurityRuleConfig -Name $rule.name -NetworkSecurityGroup $nsgObject | Set-AzureRmNetworkSecurityGroup
}
}
# Reset automation variable so the priority of rules starts over at 101
Set-AutomationVariable -Name "your-automation-variable-name" -value 100
view raw NSGCleanupJob hosted with ❤ by GitHub

Comments

Popular posts from this blog

Auto-installing extensions on Firefox using Intune

Disable DNS over HTTPS in Firefox using Intune

How to find the OMA-URI from ingested ADMX files