Set up an Azure Linux VM with the Azure CLI, apply Network Security Group (NSG) hardening, configure Azure Bastion for SSH access, and install Apus.

What you will do

What you need

Configure deployment

Sign in to Azure CLI

```bash
az login
az extension add -n ssh
```

The `ssh` extension is required for Azure Bastion native SSH tunneling.

Register required resource providers (one time)

```bash
az provider register --namespace Microsoft.Compute
az provider register --namespace Microsoft.Network
```

Verify registration; wait until both show `Registered`.

```bash
az provider show --namespace Microsoft.Compute --query registrationState -o tsv
az provider show --namespace Microsoft.Network --query registrationState -o tsv
```

Set deployment variables

```bash
RG="rg-apus"
LOCATION="westus2"
VNET_NAME="vnet-apus"
VNET_PREFIX="10.40.0.0/16"
VM_SUBNET_NAME="snet-apus-vm"
VM_SUBNET_PREFIX="10.40.2.0/24"
BASTION_SUBNET_PREFIX="10.40.1.0/26"
NSG_NAME="nsg-apus-vm"
VM_NAME="vm-apus"
ADMIN_USERNAME="apus"
BASTION_NAME="bas-apus"
BASTION_PIP_NAME="pip-apus-bastion"
```

Adjust names and CIDR ranges to fit your environment. The Bastion subnet must be at least `/26`.

Select an SSH key

Use your existing public key if you have one:

```bash
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
```

Otherwise, generate one:

```bash
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519 -C "you@example.com"
SSH_PUB_KEY="$(cat ~/.ssh/id_ed25519.pub)"
```

Select VM size and OS disk size

```bash
VM_SIZE="Standard_B2as_v2"
OS_DISK_SIZE_GB=64
```

- Start smaller for light usage and scale up later.
- Use more vCPU/RAM/disk for heavier automation, more channels, or larger model/tool workloads.
- If a size is unavailable in your region or subscription quota, pick the closest available SKU.

List VM sizes available in your target region:

```bash
az vm list-skus --location "${LOCATION}" --resource-type virtualMachines -o table
```

Check your current vCPU and disk usage/quota:

```bash
az vm list-usage --location "${LOCATION}" -o table
```

Deploy Azure resources

Create the resource group

```bash
az group create -n "${RG}" -l "${LOCATION}"
```

Create the network security group

Create the NSG and add rules so only the Bastion subnet can SSH into the VM.

```bash
az network nsg create \
  -g "${RG}" -n "${NSG_NAME}" -l "${LOCATION}"

# Allow SSH from the Bastion subnet only
az network nsg rule create \
  -g "${RG}" --nsg-name "${NSG_NAME}" \
  -n AllowSshFromBastionSubnet --priority 100 \
  --access Allow --direction Inbound --protocol Tcp \
  --source-address-prefixes "${BASTION_SUBNET_PREFIX}" \
  --destination-port-ranges 22

# Deny SSH from the public internet
az network nsg rule create \
  -g "${RG}" --nsg-name "${NSG_NAME}" \
  -n DenyInternetSsh --priority 110 \
  --access Deny --direction Inbound --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-port-ranges 22

# Deny SSH from other VNet sources
az network nsg rule create \
  -g "${RG}" --nsg-name "${NSG_NAME}" \
  -n DenyVnetSsh --priority 120 \
  --access Deny --direction Inbound --protocol Tcp \
  --source-address-prefixes VirtualNetwork \
  --destination-port-ranges 22
```

Rules evaluate by priority, lowest number first: Bastion traffic is allowed at 100, then all other SSH is blocked at 110 and 120.

Create the virtual network and subnets

Create the VNet with the VM subnet (NSG attached), then add the Bastion subnet.

```bash
az network vnet create \
  -g "${RG}" -n "${VNET_NAME}" -l "${LOCATION}" \
  --address-prefixes "${VNET_PREFIX}" \
  --subnet-name "${VM_SUBNET_NAME}" \
  --subnet-prefixes "${VM_SUBNET_PREFIX}"

# Attach the NSG to the VM subnet
az network vnet subnet update \
  -g "${RG}" --vnet-name "${VNET_NAME}" \
  -n "${VM_SUBNET_NAME}" --nsg "${NSG_NAME}"

# AzureBastionSubnet: this exact name is required by Azure
az network vnet subnet create \
  -g "${RG}" --vnet-name "${VNET_NAME}" \
  -n AzureBastionSubnet \
  --address-prefixes "${BASTION_SUBNET_PREFIX}"
```

Create the VM

The VM gets no public IP. SSH access goes exclusively through Azure Bastion.

```bash
az vm create \
  -g "${RG}" -n "${VM_NAME}" -l "${LOCATION}" \
  --image "Canonical:ubuntu-24_04-lts:server:latest" \
  --size "${VM_SIZE}" \
  --os-disk-size-gb "${OS_DISK_SIZE_GB}" \
  --storage-sku StandardSSD_LRS \
  --admin-username "${ADMIN_USERNAME}" \
  --ssh-key-values "${SSH_PUB_KEY}" \
  --vnet-name "${VNET_NAME}" \
  --subnet "${VM_SUBNET_NAME}" \
  --public-ip-address "" \
  --nsg ""
```

`--public-ip-address ""` prevents a public IP from being assigned. `--nsg ""` skips a per-NIC NSG since the subnet-level NSG already handles security.

To pin a specific Ubuntu image version instead of `latest`, list available versions first:

```bash
az vm image list \
  --publisher Canonical --offer ubuntu-24_04-lts \
  --sku server --all -o table
```

Create Azure Bastion

Azure Bastion gives managed SSH access without exposing a public IP on the VM. The Standard SKU with tunneling enabled is required for CLI-based `az network bastion ssh`.

```bash
az network public-ip create \
  -g "${RG}" -n "${BASTION_PIP_NAME}" -l "${LOCATION}" \
  --sku Standard --allocation-method Static

az network bastion create \
  -g "${RG}" -n "${BASTION_NAME}" -l "${LOCATION}" \
  --vnet-name "${VNET_NAME}" \
  --public-ip-address "${BASTION_PIP_NAME}" \
  --sku Standard --enable-tunneling true
```

Bastion provisioning typically takes 5-10 minutes, but can take up to 15-30 minutes in some regions.

Install Apus

SSH into the VM through Azure Bastion

```bash
VM_ID="$(az vm show -g "${RG}" -n "${VM_NAME}" --query id -o tsv)"

az network bastion ssh \
  --name "${BASTION_NAME}" \
  --resource-group "${RG}" \
  --target-resource-id "${VM_ID}" \
  --auth-type ssh-key \
  --username "${ADMIN_USERNAME}" \
  --ssh-key ~/.ssh/id_ed25519
```

Install Apus (in the VM shell)

```bash
curl -fsSL https://ami-docs.dev.apuscorp.com/install.sh -o /tmp/install.sh
bash /tmp/install.sh
rm -f /tmp/install.sh
```

The installer installs Node and dependencies if not already present, installs Apus, and launches onboarding. See [Install](/install) for details.

Verify the gateway

After onboarding completes:

```bash
apus doctor --json
apus gateway status
```

If your organization already has GitHub Copilot licenses, you can choose the GitHub Copilot provider during onboarding instead of a separate model API key. See [GitHub Copilot provider](/providers/github-copilot).

Cost considerations

Approximate monthly costs (verify current pricing in the Azure Pricing Calculator, since rates vary by region and change over time):

To reduce costs:

Cleanup

Delete all resources created by this guide:

az group delete -n "${RG}" --yes --no-wait

This removes the resource group and everything inside it (VM, VNet, NSG, Bastion, public IP).

Next steps

Related