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:
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:
- User logs into Office 365
- User triggers a Power Automation job (no input required by user)
- Power Automation job gets the user ID and passes it to a webhook which triggers an Azure Automation runbook
- Azure Automation runbook queries Microsoft Graph to obtain the user's sign-on IP address
- Azure Automation runbook runs a Powershell command to update the NSG protecting the resource
- 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
The NSG rule cleanup Runbook which runs on a weekly schedule:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$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 |
Comments
Post a Comment