Deploy Azure Bastion Hosts

The azure-native:network:BastionHost resource, part of the Pulumi Azure Native provider, provisions an Azure Bastion Host that provides secure browser-based RDP and SSH access to virtual machines without exposing them to the public internet. This guide focuses on three capabilities: standard deployment with public access, private-only mode for network-isolated environments, and Developer SKU with IP-based access control.

Bastion Hosts require a virtual network with a subnet named exactly “BastionHostSubnet” and, for standard deployments, a public IP address. The examples are intentionally small. Combine them with your own virtual networks, subnets, and access policies.

Deploy a standard Bastion Host with public access

Most deployments use Bastion to enable browser-based VM access without assigning public IPs to target VMs. The standard configuration requires a public IP and a dedicated subnet.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";

const bastionHost = new azure_native.network.BastionHost("bastionHost", {
    bastionHostName: "bastionhosttenant",
    ipConfigurations: [{
        name: "bastionHostIpConfiguration",
        publicIPAddress: {
            id: "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName",
        },
        subnet: {
            id: "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
        },
    }],
    resourceGroupName: "rg1",
});
import pulumi
import pulumi_azure_native as azure_native

bastion_host = azure_native.network.BastionHost("bastionHost",
    bastion_host_name="bastionhosttenant",
    ip_configurations=[{
        "name": "bastionHostIpConfiguration",
        "public_ip_address": {
            "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName",
        },
        "subnet": {
            "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
        },
    }],
    resource_group_name="rg1")
package main

import (
	network "github.com/pulumi/pulumi-azure-native-sdk/network/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := network.NewBastionHost(ctx, "bastionHost", &network.BastionHostArgs{
			BastionHostName: pulumi.String("bastionhosttenant"),
			IpConfigurations: network.BastionHostIPConfigurationArray{
				&network.BastionHostIPConfigurationArgs{
					Name: pulumi.String("bastionHostIpConfiguration"),
					PublicIPAddress: &network.SubResourceArgs{
						Id: pulumi.String("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName"),
					},
					Subnet: &network.SubResourceArgs{
						Id: pulumi.String("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet"),
					},
				},
			},
			ResourceGroupName: pulumi.String("rg1"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;

return await Deployment.RunAsync(() => 
{
    var bastionHost = new AzureNative.Network.BastionHost("bastionHost", new()
    {
        BastionHostName = "bastionhosttenant",
        IpConfigurations = new[]
        {
            new AzureNative.Network.Inputs.BastionHostIPConfigurationArgs
            {
                Name = "bastionHostIpConfiguration",
                PublicIPAddress = new AzureNative.Network.Inputs.SubResourceArgs
                {
                    Id = "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName",
                },
                Subnet = new AzureNative.Network.Inputs.SubResourceArgs
                {
                    Id = "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
                },
            },
        },
        ResourceGroupName = "rg1",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.network.BastionHost;
import com.pulumi.azurenative.network.BastionHostArgs;
import com.pulumi.azurenative.network.inputs.BastionHostIPConfigurationArgs;
import com.pulumi.azurenative.network.inputs.SubResourceArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var bastionHost = new BastionHost("bastionHost", BastionHostArgs.builder()
            .bastionHostName("bastionhosttenant")
            .ipConfigurations(BastionHostIPConfigurationArgs.builder()
                .name("bastionHostIpConfiguration")
                .publicIPAddress(SubResourceArgs.builder()
                    .id("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName")
                    .build())
                .subnet(SubResourceArgs.builder()
                    .id("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet")
                    .build())
                .build())
            .resourceGroupName("rg1")
            .build());

    }
}
resources:
  bastionHost:
    type: azure-native:network:BastionHost
    properties:
      bastionHostName: bastionhosttenant
      ipConfigurations:
        - name: bastionHostIpConfiguration
          publicIPAddress:
            id: /subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pipName
          subnet:
            id: /subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet
      resourceGroupName: rg1

The ipConfigurations array defines how Bastion connects to your network. The publicIPAddress property provides the public endpoint users connect to, while the subnet property must reference a subnet named exactly “BastionHostSubnet”. Azure routes traffic from the Bastion Host to your VMs through this subnet.

Deploy a private-only Bastion Host

Organizations with strict network isolation can deploy Bastion without a public IP, restricting access to users already on the virtual network via VPN or ExpressRoute.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";

const bastionHost = new azure_native.network.BastionHost("bastionHost", {
    bastionHostName: "bastionhosttenant",
    enablePrivateOnlyBastion: true,
    ipConfigurations: [{
        name: "bastionHostIpConfiguration",
        subnet: {
            id: "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
        },
    }],
    resourceGroupName: "rg1",
});
import pulumi
import pulumi_azure_native as azure_native

bastion_host = azure_native.network.BastionHost("bastionHost",
    bastion_host_name="bastionhosttenant",
    enable_private_only_bastion=True,
    ip_configurations=[{
        "name": "bastionHostIpConfiguration",
        "subnet": {
            "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
        },
    }],
    resource_group_name="rg1")
package main

import (
	network "github.com/pulumi/pulumi-azure-native-sdk/network/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := network.NewBastionHost(ctx, "bastionHost", &network.BastionHostArgs{
			BastionHostName:          pulumi.String("bastionhosttenant"),
			EnablePrivateOnlyBastion: pulumi.Bool(true),
			IpConfigurations: network.BastionHostIPConfigurationArray{
				&network.BastionHostIPConfigurationArgs{
					Name: pulumi.String("bastionHostIpConfiguration"),
					Subnet: &network.SubResourceArgs{
						Id: pulumi.String("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet"),
					},
				},
			},
			ResourceGroupName: pulumi.String("rg1"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;

return await Deployment.RunAsync(() => 
{
    var bastionHost = new AzureNative.Network.BastionHost("bastionHost", new()
    {
        BastionHostName = "bastionhosttenant",
        EnablePrivateOnlyBastion = true,
        IpConfigurations = new[]
        {
            new AzureNative.Network.Inputs.BastionHostIPConfigurationArgs
            {
                Name = "bastionHostIpConfiguration",
                Subnet = new AzureNative.Network.Inputs.SubResourceArgs
                {
                    Id = "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet",
                },
            },
        },
        ResourceGroupName = "rg1",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.network.BastionHost;
import com.pulumi.azurenative.network.BastionHostArgs;
import com.pulumi.azurenative.network.inputs.BastionHostIPConfigurationArgs;
import com.pulumi.azurenative.network.inputs.SubResourceArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var bastionHost = new BastionHost("bastionHost", BastionHostArgs.builder()
            .bastionHostName("bastionhosttenant")
            .enablePrivateOnlyBastion(true)
            .ipConfigurations(BastionHostIPConfigurationArgs.builder()
                .name("bastionHostIpConfiguration")
                .subnet(SubResourceArgs.builder()
                    .id("/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet")
                    .build())
                .build())
            .resourceGroupName("rg1")
            .build());

    }
}
resources:
  bastionHost:
    type: azure-native:network:BastionHost
    properties:
      bastionHostName: bastionhosttenant
      enablePrivateOnlyBastion: true
      ipConfigurations:
        - name: bastionHostIpConfiguration
          subnet:
            id: /subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet2/subnets/BastionHostSubnet
      resourceGroupName: rg1

Setting enablePrivateOnlyBastion to true removes the public IP requirement. Users must have existing connectivity to the virtual network through VPN, ExpressRoute, or peering. The ipConfigurations array still requires the BastionHostSubnet reference, but omits the publicIPAddress property.

Deploy a Developer SKU Bastion with network ACLs

The Developer SKU provides a cost-effective option for development environments, with IP-based access control to limit connections.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";

const bastionHost = new azure_native.network.BastionHost("bastionHost", {
    bastionHostName: "bastionhostdeveloper",
    ipConfigurations: [],
    networkAcls: {
        ipRules: [{
            addressPrefix: "1.1.1.1/16",
        }],
    },
    resourceGroupName: "rg2",
    virtualNetwork: {
        id: "/subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2",
    },
});
import pulumi
import pulumi_azure_native as azure_native

bastion_host = azure_native.network.BastionHost("bastionHost",
    bastion_host_name="bastionhostdeveloper",
    ip_configurations=[],
    network_acls={
        "ip_rules": [{
            "address_prefix": "1.1.1.1/16",
        }],
    },
    resource_group_name="rg2",
    virtual_network={
        "id": "/subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2",
    })
package main

import (
	network "github.com/pulumi/pulumi-azure-native-sdk/network/v3"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := network.NewBastionHost(ctx, "bastionHost", &network.BastionHostArgs{
			BastionHostName:  pulumi.String("bastionhostdeveloper"),
			IpConfigurations: network.BastionHostIPConfigurationArray{},
			NetworkAcls: &network.BastionHostPropertiesFormatNetworkAclsArgs{
				IpRules: network.IPRuleArray{
					&network.IPRuleArgs{
						AddressPrefix: pulumi.String("1.1.1.1/16"),
					},
				},
			},
			ResourceGroupName: pulumi.String("rg2"),
			VirtualNetwork: &network.SubResourceArgs{
				Id: pulumi.String("/subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using AzureNative = Pulumi.AzureNative;

return await Deployment.RunAsync(() => 
{
    var bastionHost = new AzureNative.Network.BastionHost("bastionHost", new()
    {
        BastionHostName = "bastionhostdeveloper",
        IpConfigurations = new[] {},
        NetworkAcls = new AzureNative.Network.Inputs.BastionHostPropertiesFormatNetworkAclsArgs
        {
            IpRules = new[]
            {
                new AzureNative.Network.Inputs.IPRuleArgs
                {
                    AddressPrefix = "1.1.1.1/16",
                },
            },
        },
        ResourceGroupName = "rg2",
        VirtualNetwork = new AzureNative.Network.Inputs.SubResourceArgs
        {
            Id = "/subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2",
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.azurenative.network.BastionHost;
import com.pulumi.azurenative.network.BastionHostArgs;
import com.pulumi.azurenative.network.inputs.BastionHostPropertiesFormatNetworkAclsArgs;
import com.pulumi.azurenative.network.inputs.SubResourceArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var bastionHost = new BastionHost("bastionHost", BastionHostArgs.builder()
            .bastionHostName("bastionhostdeveloper")
            .ipConfigurations()
            .networkAcls(BastionHostPropertiesFormatNetworkAclsArgs.builder()
                .ipRules(IPRuleArgs.builder()
                    .addressPrefix("1.1.1.1/16")
                    .build())
                .build())
            .resourceGroupName("rg2")
            .virtualNetwork(SubResourceArgs.builder()
                .id("/subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2")
                .build())
            .build());

    }
}
resources:
  bastionHost:
    type: azure-native:network:BastionHost
    properties:
      bastionHostName: bastionhostdeveloper
      ipConfigurations: []
      networkAcls:
        ipRules:
          - addressPrefix: 1.1.1.1/16
      resourceGroupName: rg2
      virtualNetwork:
        id: /subscriptions/subid/resourceGroups/rg2/providers/Microsoft.Network/virtualNetworks/vnet2

The Developer SKU uses the virtualNetwork property instead of ipConfigurations, referencing the entire VNet rather than a specific subnet. The networkAcls property restricts access by source IP address; only clients matching the ipRules addressPrefix can connect. This configuration is useful for limiting Bastion access to corporate networks or specific developer IPs.

Beyond these examples

These snippets focus on specific Bastion Host features: standard and private-only deployment modes, and Developer SKU with network ACLs. They’re intentionally minimal rather than full network security solutions.

The examples reference pre-existing infrastructure such as virtual networks with BastionHostSubnet, and public IP addresses for standard mode. They focus on configuring the Bastion Host rather than provisioning the surrounding network.

To keep things focused, common Bastion patterns are omitted, including:

  • Feature flags (enableFileCopy, enableTunneling, enableShareableLink, etc.)
  • Scale units for capacity planning (scaleUnits)
  • Availability zones for high availability (zones)
  • SKU configuration (sku property)

These omissions are intentional: the goal is to illustrate how each Bastion feature is wired, not provide drop-in security modules. See the Bastion Host resource reference for all available configuration options.

Let's deploy Azure Bastion Hosts

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Configuration & Deployment
What's the difference between Standard and Developer Bastion Host?
Standard Bastion uses ipConfigurations with a subnet and public IP address. Developer Bastion uses virtualNetwork with empty ipConfigurations and networkAcls for IP-based access control.
How do I create a private-only Bastion Host?
Set enablePrivateOnlyBastion to true and configure ipConfigurations with only a subnet reference (omit publicIPAddress).
What features can I enable on a Bastion Host?
You can enable File Copy (enableFileCopy), IP Connect (enableIpConnect), Kerberos (enableKerberos), Session Recording (enableSessionRecording), Shareable Link (enableShareableLink), and Tunneling (enableTunneling). All default to false.
Networking & Connectivity
What subnet name is required for Bastion Host?
The subnet must be named BastionHostSubnet, as shown in all standard deployment examples.
Do I need a public IP address for Bastion Host?
Standard Bastion requires a publicIPAddress in ipConfigurations. Private-only Bastion (with enablePrivateOnlyBastion set to true) does not require a public IP.
How do I configure network access control for Developer Bastion?
Use the networkAcls property with ipRules to specify allowed IP address prefixes (e.g., 1.1.1.1/16).
Immutability & Limitations
Can I change the Bastion Host name after creation?
No, bastionHostName is immutable and cannot be changed after creation. Changing it forces resource replacement.
What happens if I change an immutable property?
Changing bastionHostName or resourceGroupName (both immutable) will force Pulumi to destroy and recreate the Bastion Host resource.
Scaling & Availability
How do I configure scale units for Bastion Host?
Use the scaleUnits property to specify the number of scale units for the Bastion Host resource.
Can I deploy Bastion Host across availability zones?
Yes, use the zones property to specify a list of availability zones where the resource should be deployed.

Using a different cloud?

Explore networking guides for other cloud providers: