# Build a private network with two virtual machines

Source: https://docs.quake.ai/resources/deployments/build-private-network-two-vms
Markdown: https://docs.quake.ai/resources/deployments/build-private-network-two-vms.md

---

# Build a private network with two virtual machines

Stand up a private subnet with two Ubuntu instances on Quake AI: a bastion reachable through a floating IP and an internal instance reachable only over the private network. You operate the network and instances yourself; this is a kept-running bastion layout, not a managed VPC product.

<PricingCompanion
  components={[
    { kind: "primitive", required: true, label: "Bastion and internal instances", vm: { flavor: "m2a.large", count: 2 } },
    { kind: "primitive", required: true, label: "Bastion floating IP", publicIPs: 1 },
  ]}
/>

<Figure caption="Private subnet with bastion pattern: your laptop reaches the bastion through a floating IP; the bastion reaches the internal instance over the private subnet">

```d2
direction: right

laptop: Your laptop {shape: person}
public: PublicStatic\nexternal network {shape: cloud}
fip: Floating IP\n203.0.113.x

cloud: Quake AI project {
  router: Router\ntutorial-router

  subnet: Private subnet\n192.168.100.0/24 {
    bastion: Bastion VM\ntutorial-bastion\n192.168.100.10
    internal: Internal VM\ntutorial-internal\n192.168.100.20
  }
}

laptop -> fip: SSH 22
fip -> public
public -> cloud.router
cloud.router -> cloud.subnet.bastion
cloud.subnet.bastion -> cloud.subnet.internal: SSH 22
```

</Figure>

## Prerequisites

You need:

- An SSH key pair uploaded to your project, or follow Step 1 to create and upload one
- An SSH client on your local machine (built into macOS and Linux; Windows users should [set up a Linux CLI environment](/docs/tools/windows-cli-environment))
- Familiarity with SSH; see [Launch your first server](/docs/quickstart/launch-your-first-server) if you have not connected to an instance before

Use the Console for network and instance steps and your local terminal for SSH.

## Step 1: Generate and upload an SSH key pair

If you already have an SSH key pair uploaded to your project, skip the generation and the upload, and move to Step 2.

<SshKeygen />

Now upload the public key to your Quake AI project so the platform injects it into the instances you launch later:

1. In the Console, go to **Compute** > **Key Pairs** > **Import Key Pair**.
2. Enter a name like `tutorial-key`.
3. Display your public key with `cat ~/.ssh/id_ed25519.pub` and paste the contents into the **Public Key** field.
4. Select **OK**.

The key pair appears in your key pair list. You select it during the instance launch step.

## Step 2: Create the private network and subnet

The private network holds the IP addresses your instances use to talk to each other. The subnet defines the address range, DNS servers, and gateway.

1. Go to **Network** > **Networks** > **Create Network**.
2. Set the **Network Name** to `tutorial-network`.
3. Leave the **Create Subnet** toggle on (the default).
4. Fill the subnet fields:
    - **Subnet Name**: `tutorial-subnet`
    - **IP Version**: `ipv4` (the default)
    - **CIDR**: `192.168.100.0/24`
    - **DNS**: `1.1.1.1` and `1.0.0.1`
5. Select **OK** to create the network and subnet.

The new network appears in **Network** > **Networks**. The platform allocates `192.168.100.2` through `192.168.100.254` as the DHCP pool for this subnet. Your two virtual machines pick addresses from that pool when they boot.

## Step 3: Create a router and attach it to the network

The router connects `tutorial-network` to `PublicStatic`, the external network that holds public IP addresses. Without the router, instances on the private subnet cannot reach the internet and floating IPs cannot reach the instances.

1. Go to **Network** > **Routers** > **Create Router**.
2. Set the **Name** to `tutorial-router`.
3. Optionally enter a description like `Tutorial router for private subnet to PublicStatic`.
4. Select **Attach to Public Network** and choose `PublicStatic` from the dropdown. An optional **Subnet** field appears when you need to constrain allocation to a specific subnet; leave it blank when you connect a private subnet with **Connect Private Network** afterward, as this deployment does.
5. Select **OK** to create the router.

Now connect the router to the private subnet:

1. In the **Routers** list, find `tutorial-router`.
2. On the router's row, select the **Networking** icon and choose **Connect Private Network**.
3. Select the row for `tutorial-subnet`.
4. Select **OK**.

The router now sits between `tutorial-subnet` and `PublicStatic`. Traffic from your floating IP reaches the router, the router forwards it to the bastion VM you launch in Step 5, and return traffic flows back the same way.

## Step 4: Create a security group with an SSH rule

The default security group blocks all inbound traffic to instance ports. You create a separate group named `tutorial-ssh` for the SSH rule so the rules stay organized and removable during cleanup without affecting the default group.

1. Go to **Network** > **Security Groups** > **Create Security Group**.
2. Set the **Name** to `tutorial-ssh`.
3. Enter a description like `Allow SSH ingress for the private network deployment`.
4. Select **OK** to create the group.

Now add the SSH rule:

1. On the `tutorial-ssh` row, open the **Settings** gear icon dropdown and select **Create Rule**.
2. In the **Protocol** dropdown, select **SSH**. The Console auto-fills the port (`22`) and direction (**Ingress**).
3. Set **Source** to **CIDR** with the value `0.0.0.0/0`.
4. Select **OK** to create the rule.

The rule allows inbound TCP traffic on port 22 from any source IP. In production, narrow the **Source** CIDR to your office or VPN range. For this deployment, `0.0.0.0/0` keeps SSH working regardless of which network you connect from.

## Step 5: Launch the bastion virtual machine

The bastion is the public-facing VM. It carries the floating IP and accepts SSH connections from your laptop.

In the Console, go to **Compute** > **Instances** > **Create Instance**. Name the instance `tutorial-bastion` when the wizard asks. Then walk the rest of the Create Instance wizard with these settings:

<CreateVmConsole
  detailed
  image="Ubuntu-24.04"
  flavor="m2a.large"
  securityGroups={["default", "tutorial-ssh"]}
  availabilityZone="us-east-1a"
  diskSize="20 GiB"
  deleteWithInstance
/>

When the wizard reaches **Network Config**, select `tutorial-network` and `tutorial-subnet` (the network and subnet you created in Step 2). At the **System Config** step, select the `tutorial-key` key pair you uploaded in Step 1.

After you confirm, the bastion takes 30 to 60 seconds to reach **Active** status. Note the **IP Addresses** value on the instance row; this is the bastion's private address on `tutorial-subnet` (for example, `192.168.100.10`).

## Step 6: Launch the second virtual machine

The internal VM never receives a floating IP. Your laptop cannot reach it directly. You log in by SSHing first to the bastion, then from the bastion to the internal VM's private address.

Go to **Compute** > **Instances** > **Create Instance** again. Name this one `tutorial-internal`. Walk the same wizard with the same settings:

<CreateVmConsole
  detailed
  image="Ubuntu-24.04"
  flavor="m2a.large"
  securityGroups={["default", "tutorial-ssh"]}
  availabilityZone="us-east-1a"
  diskSize="20 GiB"
  deleteWithInstance
  floatingIp={false}
/>

Select `tutorial-network` and `tutorial-subnet` at the **Network Config** step and `tutorial-key` at the **System Config** step, identical to Step 5.

Once `tutorial-internal` reaches **Active**, note its private address (for example, `192.168.100.20`). You use that address from the bastion in Step 8.



Both VMs use the `default` security group plus `tutorial-ssh`. The `default` group allows traffic between members of the same group and all outbound traffic; combined with `tutorial-ssh`, the bastion can reach the internal VM over the private subnet and you can SSH into the bastion from your laptop.



## Step 7: Allocate a floating IP and associate it with the bastion

A floating IP is a public IP address you allocate from `PublicStatic` and bind to one instance at a time. Binding the IP to `tutorial-bastion` makes the bastion reachable from the public internet.

1. Go to **Network** > **Floating IPs** > **Allocate IP**.
2. In the **Network** dropdown, select `PublicStatic`. An optional **Owned Subnet** dropdown appears when you need to constrain allocation to a specific subnet; leave it blank for the default pool.
3. Leave **Batch Allocate** off.
4. Optionally enter a description like `Tutorial floating IP for bastion`.
5. Select **OK** to allocate the IP.

The new floating IP appears in the **Floating IPs** list, unassociated. Now associate it with the bastion:

1. Go to **Compute** > **Instances** and find `tutorial-bastion`.
2. On the instance row, open the **Networking** icon menu and select **Associate Floating IP**.
3. For **Instance IP**, select the private address shown for the bastion.
4. For **IP Address**, select the floating IP you allocated.
5. Select **OK**.

You can also associate from **Network** > **Floating IPs**: on the unassociated row, open the **Settings** gear icon and select **Associate**, then pick the bastion under **Instance IP**.

The instance row now shows both the private address and the floating IP. Copy the floating IP for the next step.

## Step 8: Connect via SSH and reach the internal virtual machine

Open your local terminal and SSH to the floating IP:

```bash
ssh ubuntu@YOUR_FLOATING_IP
```

Replace `YOUR_FLOATING_IP` with the address you copied in Step 7. The first time you connect, SSH prompts you to verify the host key fingerprint:

```
The authenticity of host 'YOUR_FLOATING_IP' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
```

Type `yes` and press **Enter**. You arrive at the bastion's shell:

```
ubuntu@tutorial-bastion:~$
```

Confirm the bastion's private address on the subnet:

```bash
ip -4 addr show ens3
```

Expected output (the address ends in `.10` in this example; yours may differ):

```
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.100.10/24 brd 192.168.100.255 scope global dynamic ens3
       valid_lft 86342sec preferred_lft 86342sec
```

Now reach the internal VM. Test connectivity first with a ping to the internal VM's private address:

```bash
ping -c 3 192.168.100.20
```

Replace `192.168.100.20` with the actual private address of `tutorial-internal` from Step 6. Expected output:

```
PING 192.168.100.20 (192.168.100.20) 56(84) bytes of data.
64 bytes from 192.168.100.20: icmp_seq=1 ttl=64 time=0.412 ms
64 bytes from 192.168.100.20: icmp_seq=2 ttl=64 time=0.298 ms
64 bytes from 192.168.100.20: icmp_seq=3 ttl=64 time=0.301 ms
```

The ping confirms the two VMs sit on the same subnet and can talk over the private network.

SSH from the bastion to the internal VM. The private key for `tutorial-key` lives on your laptop, not on the bastion, so add the `-A` flag to your original `ssh` connection if you have not already, or copy the key to the bastion. The simplest path is to enable agent forwarding on the connection from your laptop. Disconnect from the bastion (`exit`), then reconnect with agent forwarding:

```bash
ssh -A ubuntu@YOUR_FLOATING_IP
```

Now from the bastion, SSH to the internal VM's private address:

```bash
ssh ubuntu@192.168.100.20
```

You arrive at the internal VM's shell:

```
ubuntu@tutorial-internal:~$
```

Confirm you are on the internal VM:

```bash
hostname
```

Expected output:

```
tutorial-internal
```

You have two instances on a private network: one reachable from the internet through a floating IP and one reachable only through the bastion.

## Next steps

Now that you have a working private network with a bastion, deepen these areas:

- [How to allocate floating IP addresses](/docs/network/how-to/allocate-floating-ips): allocate, associate, and disassociate floating IPs on existing instances
- [How to create a security group](/docs/network/how-to/create-security-group): the canonical reference for security group creation across Console, CLI, API, and Terraform
- [Routers concepts](/docs/network/concepts/routers): how routers route between private subnets and external networks, including SNAT and DNAT behavior
- [Floating IPs concepts](/docs/network/concepts/floating-ips): the relationship between floating IPs, ports, and instances
- [How to create a VM on a private network](/docs/compute/how-to/create-vm-private-network): Console, CLI, and API tabs for the same layout

For an automated version of this architecture, see [Infrastructure as Code with OpenTofu](/docs/automation/how-to/getting-started-iac).

## Clean up

To avoid ongoing resource usage, delete the resources you created. Delete from instances outward so the resources release cleanly:

1. **Disassociate and release the floating IP.** Go to **Network** > **Floating IPs**, find the floating IP, and select **Disassociate**, then **Release IP**.
2. **Delete the instances.** Go to **Compute** > **Instances**, select `tutorial-bastion` and `tutorial-internal`, and select **Delete**. Confirm the deletion.
3. **Disconnect the router from the subnet.** Go to **Network** > **Routers**, find `tutorial-router`, open the **Networking** icon menu, and select **Disconnect Private Network**.
4. **Delete the router.** Select `tutorial-router` and select **Delete**.
5. **Delete the network.** Go to **Network** > **Networks**, select `tutorial-network`, and select **Delete**. The subnet deletes with the network.
6. **Delete the security group.** Go to **Network** > **Security Groups**, select `tutorial-ssh`, and select **Delete Security Group**.
7. **Optionally remove the key pair.** If you uploaded `tutorial-key` only for this deployment, go to **Compute** > **Key Pairs**, select it, and select **Delete Key Pair**. Keep it if you plan to use it for future instances.

All resources release immediately.
