Framework/Core/SVT/Services/KubernetesService.ps1
Set-StrictMode -Version Latest class KubernetesService: AzSVTBase { hidden [PSObject] $ResourceObject; KubernetesService([string] $subscriptionId, [SVTResource] $svtResource): Base($subscriptionId, $svtResource) { $this.GetResourceObject(); } hidden [PSObject] GetResourceObject() { if (-not $this.ResourceObject) { $ResourceAppIdURI = [WebRequestHelper]::GetResourceManagerUrl(); $AccessToken = [ContextHelper]::GetAccessToken($ResourceAppIdURI) if ($null -ne $AccessToken) { $header = "Bearer " + $AccessToken $headers = @{"Authorization" = $header; "Content-Type" = "application/json"; } $uri = [system.string]::Format("{0}subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ContainerService/managedClusters/{3}?api-version=2020-06-01", $ResourceAppIdURI, $this.SubscriptionContext.SubscriptionId, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceName) $result = "" $err = $null try { $propertiesToReplace = @{} $propertiesToReplace.Add("httpapplicationroutingzonename", "_httpapplicationroutingzonename") $result = [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Get, $uri, $headers, $null, $null, $propertiesToReplace); if (($null -ne $result) -and (($result | Measure-Object).Count -gt 0)) { $this.ResourceObject = $result[0] } } catch { $err = $_ if ($null -ne $err) { throw ([SuppressedException]::new(("Resource '{0}' not found under Resource Group '{1}'" -f ($this.ResourceContext.ResourceName), ($this.ResourceContext.ResourceGroupName)), [SuppressedExceptionType]::InvalidOperation)) } } } } return $this.ResourceObject; } hidden [controlresult[]] CheckClusterRBAC([controlresult] $controlresult) { if ([Helpers]::CheckMember($this.ResourceObject, "Properties")) { if ([Helpers]::CheckMember($this.ResourceObject.Properties, "enableRBAC") -and $this.ResourceObject.Properties.enableRBAC) { $controlResult.VerificationResult = [VerificationResult]::Passed } else { $controlResult.VerificationResult = [VerificationResult]::Failed } } return $controlResult; } hidden [controlresult[]] CheckAADEnabled([controlresult] $controlresult) { if ([Helpers]::CheckMember($this.ResourceObject, "Properties")) { # Legacy AAD Auth integration if ([Helpers]::CheckMember($this.ResourceObject.Properties, "aadProfile") -and [Helpers]::CheckMember($this.ResourceObject.Properties.aadProfile, "clientAppID") -and [Helpers]::CheckMember($this.ResourceObject.Properties.aadProfile, "serverAppID") -and [Helpers]::CheckMember($this.ResourceObject.Properties.aadProfile, "tenantID")) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("AAD profile configuration details", $this.ResourceObject.Properties.aadProfile)); } # AKS-managed Azure AD integration elseif ([Helpers]::CheckMember($this.ResourceObject.Properties, "aadProfile") -and [Helpers]::CheckMember($this.ResourceObject.Properties.aadProfile, "managed")) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("AAD profile configuration details", $this.ResourceObject.Properties.aadProfile)); } else { $controlResult.VerificationResult = [VerificationResult]::Failed } } return $controlResult; } hidden [controlresult[]] CheckKubernetesVersion([controlresult] $controlresult) { if (([Helpers]::CheckMember($this.ResourceObject, "Properties")) -and [Helpers]::CheckMember($this.ResourceObject.Properties, "kubernetesVersion")) { $requiredKubernetesVersion = $null $requiredKubernetesVersionPresent = $false <# $ResourceAppIdURI = [WebRequestHelper]::GetResourceManagerUrl(); $AccessToken = [ContextHelper]::GetAccessToken($ResourceAppIdURI) $header = "Bearer " + $AccessToken $headers = @{"Authorization"=$header;"Content-Type"="application/json";} $uri=[system.string]::Format("{0}subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ContainerService/managedClusters/{3}/upgradeProfiles/default?api-version=2018-03-31",$ResourceAppIdURI,$this.SubscriptionContext.SubscriptionId, $this.ResourceContext.ResourceGroupName, $this.ResourceContext.ResourceName) $result = "" $err = $null try { $propertiesToReplace = @{} $propertiesToReplace.Add("httpapplicationroutingzonename", "_httpapplicationroutingzonename") $result = [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Get, $uri, $headers, $null, $null, $propertiesToReplace); if(($null -ne $result) -and (($result | Measure-Object).Count -gt 0)) { $upgradeProfile = $result.properties.controlPlaneProfile.upgrades $requiredKubernetsVersion = "0.0.0" $upgradeProfile | Foreach-Object { if([System.Version] $requiredKubernetsVersion -le [System.Version] $_) { $requiredKubernetsVersion = $_ } } $requiredKubernetsVersion = [System.Version] $requiredKubernetsVersion } } catch{ #If any exception occurs, get required kubernetes version from config $requiredKubernetsVersion = [System.Version] $this.ControlSettings.KubernetesService.kubernetesVersion } #> $supportedKubernetesVersion = $this.ControlSettings.KubernetesService.kubernetesVersion $resourceKubernetesVersion = [System.Version] $this.ResourceObject.Properties.kubernetesVersion $supportedKubernetesVersion | ForEach-Object { if ($resourceKubernetesVersion -eq [System.Version] $_) { $requiredKubernetesVersionPresent = $true } } if (-not $requiredKubernetesVersionPresent) { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("AKS cluster is not running on required Kubernetes version.")); $controlResult.AddMessage([MessageData]::new("Current Kubernetes version: ", $resourceKubernetesVersion.ToString())); $controlResult.AddMessage([MessageData]::new("Kubernetes cluster must be running on any one of the following versions: ", $supportedKubernetesVersion)); } else { $controlResult.VerificationResult = [VerificationResult]::Passed } } return $controlResult; } hidden [controlresult[]] CheckMonitoringConfiguration([controlresult] $controlresult) { if ([Helpers]::CheckMember($this.ResourceObject, "Properties")) { if ([Helpers]::CheckMember($this.ResourceObject.Properties, "addonProfiles.omsagent") -and [Helpers]::CheckMember($this.ResourceObject.Properties.addonProfiles.omsagent, "config")) { if ($this.ResourceObject.Properties.addonProfiles.omsagent.config -and $this.ResourceObject.Properties.addonProfiles.omsagent.enabled -eq $true) { $controlResult.AddMessage([VerificationResult]::Passed, [MessageData]::new("Configuration of monitoring agent for resource " + $this.ResourceObject.name + " is ", $this.ResourceObject.Properties.addonProfiles.omsagent)); } else { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Monitoring agent is not enabled for resource " + $this.ResourceObject.name)); } } else { $controlResult.AddMessage([VerificationResult]::Failed, [MessageData]::new("Monitoring agent is not configured for resource " + $this.ResourceObject.name)); } } return $controlResult; } hidden [controlresult[]] CheckNodeOpenPorts([controlresult] $controlresult) { # If node rg property is null, set control state to manual and return if ([Helpers]::CheckMember($this.ResourceObject, "Properties") -and [Helpers]::CheckMember($this.ResourceObject.Properties, "nodeResourceGroup")) { $nodeRG = $this.ResourceObject.Properties.nodeResourceGroup } else{ $controlResult.AddMessage([VerificationResult]::Manual, "Unable to validate open ports as node ResourceGroup property is null."); return $controlResult; } $agentPoolType = "" # Check if backend pool contains VM or VMSS if([Helpers]::CheckMember($this.ResourceObject.Properties, "agentPoolProfiles")){ if([Helpers]::CheckMember($this.ResourceObject.Properties.agentPoolProfiles[0], "type") -and $this.ResourceObject.Properties.agentPoolProfiles[0].type -eq "VirtualMachineScaleSets"){ $agentPoolType = "VirtualMachineScaleSets" }else{ $agentPoolType = "VirtualMachines" } }else{ # if there are no nodes, set control state to manual and return $controlResult.AddMessage([VerificationResult]::Manual, "Unable to validate open ports as node ResourceGroup property is null."); return $controlResult; } if($agentPoolType -eq "VirtualMachines"){ # Check open mgt. ports in backend VMs $vms = Get-AzVM -ResourceGroupName $nodeRG -ErrorAction SilentlyContinue if (($vms | Measure-Object).Count -gt 0) { $isManual = $false $vulnerableNSGsWithRules = @(); $effectiveNSG = $null; $openPortsList = @(); $VMControlSettings = $this.ControlSettings.VirtualMachine.Linux $controlResult.AddMessage("Checking for Virtual Machine management ports", $VMControlSettings.ManagementPortList); $vmWithoutNSG = @(); $vms | ForEach-Object { $vmObject = $_ if ($vmObject.NetworkProfile -and $vmObject.NetworkProfile.NetworkInterfaces) { $vmObject.NetworkProfile.NetworkInterfaces | ForEach-Object { $nicResourceIdParts = $_.Id.Split("/") $nicResourceName = $nicResourceIdParts[-1] $nicRGName = $nicResourceIdParts[4] try { $effectiveNSG = Get-AzEffectiveNetworkSecurityGroup -NetworkInterfaceName $nicResourceName -ResourceGroupName $nicRGName -WarningAction SilentlyContinue -ErrorAction Stop } catch { $isManual = $true $statusCode = ($_.Exception).InnerException.Response.StatusCode; if ($statusCode -eq [System.Net.HttpStatusCode]::BadRequest -or $statusCode -eq [System.Net.HttpStatusCode]::Forbidden) { $controlResult.AddMessage(($_.Exception).InnerException.Message); } else { throw $_ } } if ($effectiveNSG) { $vulnerableRules = @() if ($VMControlSettings -and $VMControlSettings.ManagementPortList) { $inbloundRules = $effectiveNSG.EffectiveSecurityRules | Where-Object { ($_.direction -eq "Inbound" -and $_.Name -notlike "defaultsecurityrules*") } Foreach ($PortDetails in $VMControlSettings.ManagementPortList) { $portVulnerableRules = $this.CheckIfPortIsOpened($inbloundRules, $PortDetails.Port) if (($null -ne $portVulnerableRules) -and ($portVulnerableRules | Measure-Object).Count -gt 0) { $vulnerableRules += $PortDetails } } } if ($vulnerableRules.Count -ne 0) { $vulnerableNSGsWithRules += @{ Association = $effectiveNSG.Association; NetworkSecurityGroup = $effectiveNSG.NetworkSecurityGroup; VulnerableRules = $vulnerableRules; NicId = $_.Id }; } } else { $vmWithoutNSG += $vmObject.Name } } } } if ($isManual) { $controlResult.AddMessage([VerificationResult]::Manual, "Unable to check the NSG rules for some NICs. Please validate manually."); #Setting this property ensures that this control result will not be considered for the central telemetry, as control does not have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; if ($vulnerableNSGsWithRules.Count -ne 0) { $controlResult.AddMessage([VerificationResult]::Manual, "Management ports are open on node VM. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules); } } elseif (($vmWithoutNSG | Measure-Object).Count -gt 0) { #If the VM is connected to ERNetwork and there is no NSG, then we should not fail as this would directly conflict with the NSG control as well. $controlResult.AddMessage([VerificationResult]::Failed, "Verify if NSG is attached to all node VM."); $controlResult.AddMessage("Following VM nodes don't have any NSG attached:", $vmWithoutNSG); } else { #If the VM is connected to ERNetwork or not and there is NSG, then teams should apply the recommendation and attest this control for now. if ($vulnerableNSGsWithRules.Count -eq 0) { $controlResult.AddMessage([VerificationResult]::Passed, "No management ports are open on node VM"); } else { $controlResult.AddMessage([VerificationResult]::Verify, "Management ports are open on node VM. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules); $controlResult.SetStateData("Management ports list on node VM", $vulnerableNSGsWithRules); } } } else { $controlResult.AddMessage([VerificationResult]::Manual, "Unable to fetch node VM details. Please verify NSG rules manually on node VM."); } } else{ # Check open mgt. ports in backend VMSS $vmss = Get-AzVmss -ResourceGroupName $nodeRG -ErrorAction SilentlyContinue $vmssWithoutNSG = @() $isManual = $false $vulnerableNSGsWithRules = @(); $effectiveNSG = $null; $openPortsList =@(); $nsgAtSubnetLevelChecked = $false $nsgAtSubnetLevel = $null $vmssMgtPortList = @($this.ControlSettings.VirtualMachineScaleSet.Linux.ManagementPortList + $this.ControlSettings.VirtualMachineScaleSet.Windows.ManagementPortList ) $applicableNSGForVMSS = @{} $vmss | ForEach-Object { $currentVMSS = $_ if([Helpers]::CheckMember($currentVMSS,"VirtualMachineProfile.NetworkProfile")){ $currentVMSS.VirtualMachineProfile.NetworkProfile.NetworkInterfaceConfigurations| ForEach-Object { $effectiveNSGForCurrentNIC = $null #Get the NSGs applied at subnet level if(-not $nsgAtSubnetLevelChecked -and $_.IpConfigurations) { $nsgAtSubnetLevelChecked = $true $subnetId = $_.IpConfigurations[0].Subnet.Id; $subnetName = $subnetId.Substring($subnetId.LastIndexOf("/") + 1); # get vnet name and rg name from subnet id # sample subnet id: /subscriptions/00000000000000000000/resourceGroups/rgName/providers/Microsoft.Network/virtualNetworks/vnetName/subnets/subNetName $subnetIdParts = $subnetId.Trim().Split("/") $vnetResourceGroupName = $subnetIdParts[4] $vnetResourceName = $subnetIdParts[8] $vnetObject = Get-AzVirtualNetwork -Name $vnetResourceName -ResourceGroupName $vnetResourceGroupName if($vnetObject) { $subnetConfig = Get-AzVirtualNetworkSubnetConfig -Name $subnetName -VirtualNetwork $vnetObject if($subnetConfig -and $subnetConfig.NetworkSecurityGroup -and $subnetConfig.NetworkSecurityGroup.Id) { # get nsg name and rg name from nsg id # sample nsg id: /subscriptions/000000000000000000/resourceGroups/rgName/providers/Microsoft.Network/networkSecurityGroups/nsgName $nsgIdParts = $subnetConfig.NetworkSecurityGroup.Id.Trim().Split("/") $nsgResourceGroupName = $nsgIdParts[4] $nsgResourceName = $nsgIdParts[8] $nsgObject = Get-AzNetworkSecurityGroup -Name $nsgResourceName -ResourceGroupName $nsgResourceGroupName if($nsgObject) { $nsgAtSubnetLevel = $nsgObject $applicableNSGForVMSS[$nsgAtSubnetLevel.Id] = $nsgAtSubnetLevel } } } } #Get NSGs applied at NIC level if($_.NetworkSecurityGroup) { if (-not $applicableNSGForVMSS.ContainsKey($_.NetworkSecurityGroup.Id)){ # get nsg name and rg name from nsg id # sample nsg id: /subscriptions/000000000000000000/resourceGroups/rgName/providers/Microsoft.Network/networkSecurityGroups/nsgName $nsgIdParts = $_.NetworkSecurityGroup.Id.Trim().Split("/") $nsgResourceGroupName = $nsgIdParts[4] $nsgResourceName = $nsgIdParts[8] $nsgObject = Get-AzNetworkSecurityGroup -Name $nsgResourceName -ResourceGroupName $nsgResourceGroupName if($nsgObject) { $effectiveNSGForCurrentNIC = $nsgObject.Id $applicableNSGForVMSS[$_.NetworkSecurityGroup.Id] = $nsgObject }else { $effectiveNSGForCurrentNIC = $nsgAtSubnetLevel } }else{ $effectiveNSGForCurrentNIC = $_.NetworkSecurityGroup.Id } } else{ $effectiveNSGForCurrentNIC = $nsgAtSubnetLevel } if(-not $effectiveNSGForCurrentNIC){ $vmssWithoutNSG = $currentVMSS.Name } } }else{ $isManual = $true } } $applicableNSGForVMSS.Keys | ForEach-Object { $currentNSG = $applicableNSGForVMSS[$_] $vulnerableRules = @() if($vmssMgtPortList.Count -gt 0) { $vmssMgtPortList = $vmssMgtPortList | Select-Object -Unique -Property Port,Name $inbloundRules = $currentNSG.SecurityRules | Where-Object { ($_.direction -eq "Inbound" ) } Foreach($PortDetails in $vmssMgtPortList) { $portVulnerableRules = $this.CheckIfPortIsOpened($inbloundRules,$PortDetails.Port) if(($null -ne $portVulnerableRules) -and ($portVulnerableRules | Measure-Object).Count -gt 0) { $vulnerableRules += $PortDetails } } } if($vulnerableRules.Count -ne 0) { $vulnerableNSGsWithRules += @{ NetworkSecurityGroupName = $currentNSG.Name; NetworkSecurityGroupId = $currentNSG.Id; VulnerableRules = $vulnerableRules }; } } if ($isManual) { $controlResult.AddMessage([VerificationResult]::Manual, "Unable to check the NSG rules. Please validate manually."); #Setting this property ensures that this control result will not be considered for the central telemetry, as control does not have the required permissions $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess = $false; if ($vulnerableNSGsWithRules.Count -ne 0) { $controlResult.AddMessage([VerificationResult]::Manual, "Management ports are open on AKS backend node pools. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules); } } elseif (($vmssWithoutNSG | Measure-Object).Count -gt 0) { $controlResult.AddMessage([VerificationResult]::Failed, "Verify if NSG is attached to all node pools."); $controlResult.AddMessage("Following VMSS node pools don't have any NSG attached:", $vmssWithoutNSG); } else { if ($vulnerableNSGsWithRules.Count -eq 0) { $controlResult.AddMessage([VerificationResult]::Passed, "No management ports are open on AKS backend node pools"); } else { $controlResult.AddMessage([VerificationResult]::Verify, "Management ports are open on AKS backend node pools. Please verify and remove the NSG rules in order to comply.", $vulnerableNSGsWithRules); $controlResult.SetStateData("Management ports list on AKS backend node pools", $vulnerableNSGsWithRules); } } } return $controlResult; } hidden [PSObject] CheckIfPortIsOpened([PSObject] $inbloundRules, [int] $port ) { $vulnerableRules = @(); foreach ($securityRule in $inbloundRules) { foreach ($destPort in $securityRule.destinationPortRange) { $range = $destPort.Split("-") #For ex. in case of VM if we provide the input 22 in the destination port range field, it will be interpreted as 22-22 as we are passing effective NSG secuirty rules #Or if NSG rules contains a open port range like 22-28 if ($range.Count -eq 2) { $startPort = $range[0] $endPort = $range[1] if (($port -ge $startPort -and $port -le $endPort) -and $securityRule.access.ToLower() -eq "deny") { break; } elseif (($port -ge $startPort -and $port -le $endPort) -and $securityRule.access.ToLower() -eq "allow") { $vulnerableRules += $securityRule } else { continue; } } #In case of VMSS if we are passing the raw NSG secuirty rules so it will keep single port as single port only elseif($range.Count -eq 1 -and $destPort -eq $port) { $vulnerableRules += $securityRule } } } return $vulnerableRules; } hidden [controlresult[]] CheckHTTPAppRouting([controlresult] $controlresult) { if ([Helpers]::CheckMember($this.ResourceObject, "Properties")) { if ([Helpers]::CheckMember($this.ResourceObject.Properties, "Addonprofiles.httpApplicationRouting") -and $this.ResourceObject.Properties.Addonprofiles.httpApplicationRouting.enabled -eq $true) { $controlResult.AddMessage([VerificationResult]::Failed, "HTTP application routing is 'Enabled' for this cluster."); } else { $controlResult.AddMessage([VerificationResult]::Passed, "HTTP application routing is 'Disabled' for this cluster."); } } return $controlResult; } } |