Introduction
Creating a secure private VNet for CloudPC (Windows 365 Enterprise) and Azure DevOps agent allows for an isolated network environment, enabling secure access, management, and control of cloud resources.
This guide will walk you through the process of setting up a private VNet using Pulumi, integrating it with the Hub VNet was created in the previous article.
Table of Contents
Open Table of Contents
The project modules
The CloudPcFirewallRules
Module
This module defines the firewall policies for:
-
CloudPC: We adhere to the recommended network rules for Windows 365 Enterprise. Additionally, we ensure that all machines within the CloudPC subnet have access to AKS, DevOps subnets, and other Azure resources.
View code:
import { currentRegionCode } from '@az-commons'; import * as inputs from '@pulumi/azure-native/types/input'; import * as pulumi from '@pulumi/pulumi'; import { subnetSpaces } from '../../config'; const netRules: pulumi.Input<inputs.network.NetworkRuleArgs>[] = [ { ruleType: 'NetworkRule', name: 'cloudPC-to-aks', description: 'Allows CloudPC access to AKS and DevOps.', ipProtocols: ['TCP'], sourceAddresses: [subnetSpaces.cloudPC], destinationAddresses: [subnetSpaces.devOps, subnetSpaces.aks], destinationPorts: ['443'], }, { ruleType: 'NetworkRule', name: 'cloudPC-services-tags', description: 'Allows CloudPC access to Azure Resources.', ipProtocols: ['TCP'], sourceAddresses: [subnetSpaces.cloudPC], destinationAddresses: [`AzureCloud.${currentRegionCode}`], destinationPorts: ['443'], }, { ruleType: 'NetworkRule', name: `cloudPC-net-allow-win365-windows-net`, description: 'CloudPc allows Windows 365 windows.net', ipProtocols: ['TCP'], sourceAddresses: [subnetSpaces.cloudPC], destinationAddresses: ['40.83.235.53'], destinationPorts: ['1688'], }, { ruleType: 'NetworkRule', name: `cloudPC-net-allow-win365-azure-devices`, description: 'CloudPc allows Windows 365 azure-devices', ipProtocols: ['TCP'], sourceAddresses: [subnetSpaces.cloudPC], destinationAddresses: [ '23.98.104.204', '40.78.238.4', '20.150.179.224', '52.236.189.131', '13.69.71.14', '13.69.71.2', '13.70.74.193', '13.86.221.39', '13.86.221.36', '13.86.221.43', ], destinationPorts: ['443', '5671'], }, { ruleType: 'NetworkRule', name: `cloudPC-net-allow-win365-udp-tcp`, description: 'CloudPc allows Windows 365 udp tcp', ipProtocols: ['UDP', 'TCP'], sourceAddresses: [subnetSpaces.cloudPC], destinationAddresses: ['20.202.0.0/16'], destinationPorts: ['443', '3478'], }, ]; const appRules: pulumi.Input<inputs.network.ApplicationRuleArgs>[] = [ { ruleType: 'ApplicationRule', name: `cloudPC-app-allow-update`, description: 'Allows Windows Updates', sourceAddresses: [subnetSpaces.cloudPC], fqdnTags: ['WindowsUpdate', 'WindowsDiagnostics', 'AzureBackup'], protocols: [{ protocolType: 'Https', port: 443 }], }, { ruleType: 'ApplicationRule', name: `cloudPC-app-allow-win365`, description: 'Allows Windows365', sourceAddresses: [subnetSpaces.cloudPC], fqdnTags: ['Windows365', 'MicrosoftIntune'], protocols: [{ protocolType: 'Https', port: 443 }], }, { ruleType: 'ApplicationRule', name: `cloudPC-app-allow-azure-portal`, description: 'Allows Azure Portal', sourceAddresses: [subnetSpaces.cloudPC], targetFqdns: [ 'login.microsoftonline.com', '*.azure.com', '*.azure.net', '*.microsoftonline.com', '*.msauth.net', '*.msauthimages.net', '*.msecnd.net', '*.msftauth.net', '*.msftauthimages.net', 'www.microsoft.com', 'learn.microsoft.com', ], protocols: [{ protocolType: 'Https', port: 443 }], }, ]; export default { appRules, netRules };
-
DevOps: The current setup allows all machines in the DevOps subnet unrestricted access to all resources, including those on the internet. To improve security, it is recommended to restrict access to only necessary resources and implement more granular firewall rules.
View code:
import * as inputs from '@pulumi/azure-native/types/input'; import * as pulumi from '@pulumi/pulumi'; import { subnetSpaces } from '../../config'; const netRules: pulumi.Input<inputs.network.NetworkRuleArgs>[] = [ { ruleType: 'NetworkRule', name: 'devops-to-aks', description: 'Allows devops to internet.', ipProtocols: ['TCP', 'UDP'], sourceAddresses: [subnetSpaces.devOps], destinationAddresses: ['*'], destinationPorts: ['443', '80'], }, ]; const appRules: pulumi.Input<inputs.network.ApplicationRuleArgs>[] = []; export default { appRules, netRules };
-
Index File: Combines CloudPC and DevOps rules into a unified
FirewallPolicyRuleCollectionGroup
, linking them to the root policy established in theaz-02-hub-vnet
project.View code:
import { getName } from '@az-commons'; import * as network from '@pulumi/azure-native/network'; import * as inputs from '@pulumi/azure-native/types/input'; import * as pulumi from '@pulumi/pulumi'; import cloudPCRules from './cloudpcPolicyGroup'; import devOpsRules from './devopsPolicyGroup'; export default ( name: string, //This FirewallPolicyRuleCollectionGroup need to be linked to the Root Policy that had been created in `az-02-hub-vnet` rootPolicy: { name: pulumi.Input<string>; resourceGroupName: pulumi.Input<string>; } ) => { const netRules = [...cloudPCRules.netRules, ...devOpsRules.netRules]; const appRules = [...cloudPCRules.appRules, ...devOpsRules.appRules]; const ruleCollections: pulumi.Input<inputs.network.FirewallPolicyFilterRuleCollectionArgs>[] = []; if (netRules.length > 0) { ruleCollections.push({ name: 'net-rules-collection', priority: 300, ruleCollectionType: 'FirewallPolicyFilterRuleCollection', action: { type: network.FirewallPolicyFilterRuleCollectionActionType .Allow, }, rules: netRules, }); } if (appRules.length > 0) { ruleCollections.push({ name: 'app-rules-collection', priority: 301, ruleCollectionType: 'FirewallPolicyFilterRuleCollection', action: { type: network.FirewallPolicyFilterRuleCollectionActionType .Allow, }, rules: appRules, }); } return new network.FirewallPolicyRuleCollectionGroup( getName(name, 'fw-group'), { resourceGroupName: rootPolicy.resourceGroupName, firewallPolicyName: rootPolicy.name, priority: 301, ruleCollections, } ); };
The VNet.ts
Module
This module is responsible for creating a virtual network (VNet) with two subnets.
It also establishes peering with the Hub VNet that was set up in the previous project, similar to the VNet component used for AKS in az-03-aks-cluster
project.
View code:
export default (
name: string,
{
rsGroup,
subnets,
routes,
peeringVnet,
securityRules,
}: {
rsGroup: resources.ResourceGroup;
subnets: inputs.network.SubnetArgs[];
/**The optional additional rules for NetworkSecurityGroup*/
securityRules?: pulumi.Input<inputs.network.SecurityRuleArgs>[];
/**The optional of routing rules for RouteTable*/
routes?: pulumi.Input<inputs.network.RouteArgs>[];
peeringVnet?: {
name: pulumi.Input<string>;
id: pulumi.Input<string>;
resourceGroupName: pulumi.Input<string>;
};
}
) => {
const sgroup = createSecurityGroup(name, { rsGroup, securityRules });
const routeTable = routes
? createRouteTable(name, { rsGroup, routes })
: undefined;
const vnetName = getName(name, 'vnet');
const vnet = new network.VirtualNetwork(
vnetName,
{
// Resource group name
resourceGroupName: rsGroup.name,
//Enable VN protection
enableVmProtection: true,
//Enable Vnet encryption
encryption: {
enabled: true,
enforcement:
network.VirtualNetworkEncryptionEnforcement
.AllowUnencrypted,
},
addressSpace: {
addressPrefixes: subnets.map((s) => s.addressPrefix!),
},
subnets: subnets.map((s) => ({
...s,
//Inject NetworkSecurityGroup to all subnets if available
networkSecurityGroup: sgroup ? { id: sgroup.id } : undefined,
//Inject RouteTable to all subnets if available
routeTable: routeTable ? { id: routeTable.id } : undefined,
})),
},
{
dependsOn: routeTable ? [sgroup, routeTable] : sgroup,
//Ignore this property as the peering will be manage by instance of VirtualNetworkPeering
ignoreChanges: ['virtualNetworkPeerings'],
}
);
// Create sync Peering from `az-02-hub-vnet` to `az-03-aks-cluster`
// The peering need to be happened 2 ways in order to establish the connection
if (peeringVnet) {
//from `az-02-hub-vnet` to `az--04-cloudPc`
new network.VirtualNetworkPeering(
`hun-to-${vnetName}`,
{
resourceGroupName: peeringVnet.resourceGroupName,
virtualNetworkName: peeringVnet.name,
allowVirtualNetworkAccess: true,
allowForwardedTraffic: true,
syncRemoteAddressSpace: 'true',
remoteVirtualNetwork: { id: vnet.id },
peeringSyncLevel: 'FullyInSync',
},
{ dependsOn: vnet }
);
//from `az-04-cloudPc` to `az-02-hub-vnet`
new network.VirtualNetworkPeering(
`${vnetName}-to-hub`,
{
resourceGroupName: rsGroup.name,
virtualNetworkName: vnet.name,
allowVirtualNetworkAccess: true,
allowForwardedTraffic: true,
syncRemoteAddressSpace: 'true',
remoteVirtualNetwork: { id: peeringVnet.id },
peeringSyncLevel: 'FullyInSync',
},
{ dependsOn: vnet }
);
}
return vnet;
};
The DiskEncryptionSet.ts
Module
This module demonstrates how to encrypt Azure resources using a custom encryption key stored in Azure Key Vault. It includes the following components:
-
User Assigned Identity: This identity is used to grant access to the Key Vault, allowing it to read the encryption key.
View code:
const createUserAssignIdentity = ( name: string, { rsGroup, vault, }: { rsGroup: azure.resources.ResourceGroup; vault: VaultInfo } ) => { //Create Identity const identity = new azure.managedidentity.UserAssignedIdentity( getName(name, 'uid'), { resourceGroupName: rsGroup.name, }, { dependsOn: rsGroup } ); //Add identity to readOnly group of Vault to allow reading encryption Key new ad.GroupMember( getName(name, 'uid-vault-read'), { groupObjectId: vault.readOnlyGroupId, memberObjectId: identity.principalId, }, { dependsOn: identity } ); return identity; };
-
Vault Encryption Key: A custom encryption key with a size of 4096 bits, configured for automatic rotation every year within the Key Vault.
View code:
export const createEncryptionKey = (name: string, vault: VaultInfo) => new azure.keyvault.Key( name, { ...vault, keyName: name, properties: { kty: azure.keyvault.JsonWebKeyType.RSA, keySize: 4096, //2048 | 3072 | 4096 keyOps: [ 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey', ], attributes: { enabled: true }, rotationPolicy: { attributes: { // Rotate every 365 days expiryTime: 'P365D', }, lifetimeActions: [ { action: { type: azure.keyvault.KeyRotationPolicyActionType .Rotate, }, trigger: { // Trigger rotation 7 days before expiry timeBeforeExpiry: 'P7D', }, }, ], }, }, }, { retainOnDelete: true } );
-
Disk Encryption Set: This component creates a
DiskEncryptionSet
using the User Assigned Identity and the custom encryption key mentioned above.View code:
export default ( name: string, { rsGroup, vault, }: { rsGroup: azure.resources.ResourceGroup; vault: VaultInfo } ) => { const diskName = getName(name, 'disk-encrypt'); const uid = createUserAssignIdentity(diskName, { rsGroup, vault }); //Create Key on vault const key = createEncryptionKey(diskName, vault); return new azure.compute.DiskEncryptionSet( diskName, { resourceGroupName: rsGroup.name, rotationToLatestKeyVersionEnabled: true, encryptionType: 'EncryptionAtRestWithCustomerKey', identity: { type: azure.compute.ResourceIdentityType .SystemAssigned_UserAssigned, userAssignedIdentities: [uid.id], }, activeKey: { keyUrl: key.keyUriWithVersion }, }, { dependsOn: [rsGroup, key, uid], retainOnDelete: true, } );
The VM.ts
Module
This module facilitates the provisioning of a Linux virtual machine (VM) on Azure with automatically generated login credentials and disk encryption and connects the VM to a subnet within the virtual network.
View code:
import { getName } from '@az-commons';
import * as azure from '@pulumi/azure-native';
import * as pulumi from '@pulumi/pulumi';
import * as random from '@pulumi/random';
import { createEncryptionKey } from './DiskEncryptionSet';
type VaultInfo = {
id: pulumi.Input<string>;
resourceGroupName: pulumi.Input<string>;
vaultName: pulumi.Input<string>;
readOnlyGroupId: pulumi.Input<string>;
};
/**
* Generate the username and password for the vm
* */
const generateLogin = (name: string, vault: VaultInfo) => {
const usernameKey = getName(name, 'username');
const username = new random.RandomString(usernameKey, {
length: 15,
special: false,
});
const passwordKey = getName(name, 'password');
const password = new random.RandomPassword(passwordKey, {
length: 50,
});
//Store secrets to the vault
[
{ name: usernameKey, value: username.result },
{ name: passwordKey, value: password.result },
].map(
(item) =>
new azure.keyvault.Secret(
item.name,
{
...vault,
secretName: item.name,
properties: { value: item.value },
},
{ dependsOn: [username, password] }
)
);
return { username, password };
};
/**
* Create a Linux VM:
* - `username` and `password` will be generated using pulumi random and store in Key Vault.
* - The AzureDevOps extension will be installed automatically and become a private AzureDevOps agent.
* - It will be encrypted with a custom key from Key Vault also.
* - The disk also will be encrypted with a DiskEncryptionSet.
* */
export default (
name: string,
{
rsGroup,
vault,
vmSize = 'Standard_B2s',
diskEncryptionSet,
vnet,
}: {
vmSize: string;
rsGroup: azure.resources.ResourceGroup;
diskEncryptionSet: azure.compute.DiskEncryptionSet;
vault: VaultInfo;
vnet: azure.network.VirtualNetwork;
}
) => {
const vmName = getName(name, 'vm');
//Create VM login info
const loginInfo = generateLogin(vmName, vault);
//Create VM NIC
const nic = new azure.network.NetworkInterface(
vmName,
{
resourceGroupName: rsGroup.name,
ipConfigurations: [
{
name: 'ipconfig',
subnet: {
id: vnet.subnets.apply(
(ss) => ss!.find((s) => s.name === 'devops')!.id!
),
},
primary: true,
},
],
nicType: azure.network.NetworkInterfaceNicType.Standard,
},
{ dependsOn: [rsGroup, vnet] }
);
//Create VM
const vm = new azure.compute.VirtualMachine(
vmName,
{
resourceGroupName: rsGroup.name,
hardwareProfile: { vmSize },
licenseType: 'None',
networkProfile: {
networkInterfaces: [{ id: nic.id, primary: true }],
},
//az feature register --name EncryptionAtHost --namespace Microsoft.Compute
securityProfile: { encryptionAtHost: true },
osProfile: {
computerName: vmName,
adminUsername: loginInfo.username.result,
adminPassword: loginInfo.password.result,
allowExtensionOperations: true,
linuxConfiguration: {
//ssh: { publicKeys: [{ keyData: linux.sshPublicKey! }] },
//TODO: this shall be set as 'true' when ssh is provided
disablePasswordAuthentication: false,
provisionVMAgent: true,
patchSettings: {
assessmentMode:
azure.compute.LinuxPatchAssessmentMode
.AutomaticByPlatform,
automaticByPlatformSettings: {
bypassPlatformSafetyChecksOnUserSchedule: true,
rebootSetting:
azure.compute
.LinuxVMGuestPatchAutomaticByPlatformRebootSetting
.Never,
},
patchMode:
azure.compute.LinuxVMGuestPatchMode
.AutomaticByPlatform,
},
},
},
storageProfile: {
imageReference: {
offer: '0001-com-ubuntu-server-jammy',
publisher: 'Canonical',
sku: '22_04-lts',
version: 'latest',
},
osDisk: {
diskSizeGB: 256,
caching: 'ReadWrite',
createOption: 'FromImage',
osType: azure.compute.OperatingSystemTypes.Linux,
managedDisk: {
diskEncryptionSet: { id: diskEncryptionSet.id },
storageAccountType:
azure.compute.StorageAccountTypes.StandardSSD_LRS,
},
},
//Sample to create data disk if needed
// dataDisks: [
// {
// diskSizeGB: 256,
// createOption: azure.compute.DiskCreateOptionTypes.Empty,
// lun: 1,
// managedDisk: {
// diskEncryptionSet: {
// id: diskEncryptionSet.id,
// },
// storageAccountType:
// azure.compute.StorageAccountTypes
// .StandardSSD_LRS,
// },
// },
// ],
},
diagnosticsProfile: { bootDiagnostics: { enabled: true } },
},
{
dependsOn: [
diskEncryptionSet,
loginInfo.username,
loginInfo.password,
nic,
],
}
);
return vm;
};
The PrivateDNS.ts
Module
To optimize internal network communication, we implement a DNS resolver. This module sets up a private DNS zone that facilitates efficient name resolution within our network infrastructure.
- Private DNS Zone: Creates a dedicated DNS zone for internal use.
- VNet Links: Establishes connections between the private DNS zone and both the Hub and CloudPC virtual networks.
- A Record: Configures A record that points to the private IP address of our NGINX ingress controller.
In the following topics, We will cover the NGINX ingress controller deployed on AKS as private ingress, and it will be assigned the internal IP address
192.168.31.250
. This IP must be within the AKS subnet range.
By linking this private DNS to both the Hub and CloudPC VNets, we ensure that all DNS requests for our internal services are correctly routed to the NGINX ingress controller. This setup enhances security and improves network performance by keeping internal traffic within our private network.
View code:
import * as azure from '@pulumi/azure-native';
import * as pulumi from '@pulumi/pulumi';
export default (
name: string,
{
privateIpAddress,
vnetIds,
rsGroup,
}: {
privateIpAddress: pulumi.Input<string>;
rsGroup: azure.resources.ResourceGroup;
vnetIds: pulumi.Input<string>[];
}
) => {
//Create Zone
const zone = new azure.network.PrivateZone(
name,
{
privateZoneName: name,
resourceGroupName: rsGroup.name,
//The location here must be 'global'
location: 'global',
},
{ dependsOn: rsGroup }
);
//Create Root Records to the private Ip Address
new azure.network.PrivateRecordSet(
`${name}-Root`,
{
privateZoneName: zone.name,
resourceGroupName: rsGroup.name,
relativeRecordSetName: '@',
recordType: 'A',
aRecords: [{ ipv4Address: privateIpAddress }],
ttl: 3600,
},
{ dependsOn: zone }
);
new azure.network.PrivateRecordSet(
`${name}-Star`,
{
privateZoneName: zone.name,
resourceGroupName: rsGroup.name,
relativeRecordSetName: '*',
recordType: 'A',
aRecords: [{ ipv4Address: privateIpAddress }],
ttl: 3600,
},
{ dependsOn: zone }
);
//Link to VNET
vnetIds.map(
(v, index) =>
new azure.network.VirtualNetworkLink(
`${name}-${index}-link`,
{
privateZoneName: zone.name,
resourceGroupName: rsGroup.name,
//The location here must be 'global'
location: 'global',
registrationEnabled: false,
virtualNetwork: { id: v },
},
{ dependsOn: zone }
)
);
return zone;
};
Developing the CloudPC Stack
Our objective is to establish a private Virtual Network (VNet) for CloudPC and Azure DevOps agents using Pulumi, enabling us to provision the necessary Azure resources effectively.
- Firewall Policy: Implement security policies to manage egress traffic for CloudPC and DevOps agents.
- VNet and Peering: Develop the primary network infrastructure, including subnets for CloudPC and the Azure DevOps agent, with necessary VNet peering.
- Disk Encryption Set: Integrate a disk encryption set to secure virtual machine data at rest.
- Deploy a Linux VM: Provision a Linux virtual machine to host the Azure DevOps agent. The agent installation process will be covered in the following topic.
View code:
import { getGroupName, StackReference } from '@az-commons';
import * as azure from '@pulumi/azure-native';
import * as pulumi from '@pulumi/pulumi';
import * as config from '../config';
import FirewallRule from './CloudPcFirewallRules';
import DiskEncryptionSet from './DiskEncryptionSet';
import PrivateDNS from './PrivateDNS';
import VM from './VM';
import VNet from './VNet';
//Reference to the output of `az-01-shared` and `az-02-hub-vnet`.
const sharedStack = StackReference<config.SharedStackOutput>('az-01-shared');
const hubVnetStack = StackReference<config.HubVnetOutput>('az-02-hub-vnet');
//Apply Firewall Rules
FirewallRule(config.azGroups.cloudPC, {
resourceGroupName: hubVnetStack.rsGroup.name,
name: hubVnetStack.firewallPolicy.name,
});
//The vault Info from the shared project
const vault = {
id: sharedStack.vault.id,
vaultName: sharedStack.vault.name,
resourceGroupName: sharedStack.rsGroup.name,
readOnlyGroupId: sharedStack.vault.readOnlyGroupId,
};
// Create Shared Resource Group
const rsGroup = new azure.resources.ResourceGroup(
getGroupName(config.azGroups.cloudPC)
);
// Create Virtual Network with Subnets
const vnet = VNet(config.azGroups.cloudPC, {
rsGroup,
subnets: [
{
name: 'cloudpc',
addressPrefix: config.subnetSpaces.cloudPC,
},
{
name: 'devops',
addressPrefix: config.subnetSpaces.devOps,
},
],
//allows vnet to firewall's private IP
securityRules: [
{
name: `allows-vnet-to-hub-firewall`,
description: 'Allows Vnet to hub firewall Outbound',
priority: 300,
protocol: '*',
access: 'Allow',
direction: 'Outbound',
sourceAddressPrefix: hubVnetStack.firewall.address.apply(
(ip) => `${ip}/32`
),
sourcePortRange: '*',
destinationAddressPrefix: 'VirtualNetwork',
destinationPortRange: '*',
},
{
name: `allows-aks-to-devops`,
description: 'Allows aks to devops Inbound',
priority: 301,
protocol: '*',
access: 'Allow',
direction: 'Inbound',
sourceAddressPrefix: config.subnetSpaces.aks,
sourcePortRange: '*',
destinationAddressPrefix: config.subnetSpaces.devOps,
destinationPortRange: '*',
},
],
//route all requests to firewall's private IP
routes: [
{
name: 'route-vnet-to-firewall',
addressPrefix: '0.0.0.0/0',
nextHopIpAddress: hubVnetStack.firewall.address,
nextHopType: 'VirtualAppliance',
},
],
//peering to hub vnet
peeringVnet: {
name: hubVnetStack.hubVnet.name,
id: hubVnetStack.hubVnet.id,
resourceGroupName: hubVnetStack.rsGroup.name,
},
});
//Create Disk Encryption. This shall be able to share to multi VMs
const diskEncryptionSet = DiskEncryptionSet(config.azGroups.cloudPC, {
rsGroup,
vault,
});
//Create DevOps Agent 01 VM
const vm = VM('devops-agent-01', {
diskEncryptionSet,
rsGroup,
vmSize: 'Standard_B2s',
vault,
vnet,
});
//Create Private DNS Zone
const zone = PrivateDNS('drunkcoding.net', {
rsGroup,
privateIpAddress: '192.168.31.250',
vnetIds: [vnet.id, hubVnetStack.hubVnet.id],
});
// Export the information that will be used in the other projects
export default {
rsGroup: { name: rsGroup.name, id: rsGroup.id },
cloudPcVnet: { name: vnet.name, id: vnet.id },
devopsAgent01: { name: vm.name, id: vm.id },
privateDNS: { name: zone.name, id: zone.id },
};
Deployment and Cleanup
Deploying the Stack
To deploy the stack, run the pnpm run up
command. This will provision all the necessary Azure resources, such as the Virtual Network (VNet), subnets, firewall, and private endpoints. Before executing the command, ensure you are logged into your Azure account using the Azure CLI and have configured Pulumi with the correct backend and credentials.
The deployed Azure resources
Cleaning Up the Stack
To remove all associated Azure resources and clean up the stack, execute the pnpm run destroy
command. This will help avoid unnecessary costs and ensure that all resources are properly deleted after testing or development.
References
- Azure Virtual Network Documentation
- Firewall Policies and Rule Collection Groups
- Azure Bastion Configuration
- TeamServicesAgentLinux Extension
- Network requirements For Windows 365 Enterprise
- AzureDevOps Allowed IP addresses and domain URLs
Next
Day 07: Setup Windows 365 Enterprise as a private VDI
In the next article, we will explore how to configure a CloudPC with Windows 365 Enterprise to establish a secure and efficient Virtual Desktop Infrastructure (VDI) for accessing a private AKS environment.
Thank You
Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨
Steven | GitHub