# How to set up SSH bastion access into a private subnet

Source: https://docs.quake.ai/docs/network/how-to/ssh-bastion-access
Markdown: https://docs.quake.ai/docs/network/how-to/ssh-bastion-access.md

---

# How to set up SSH bastion access into a private subnet

Reach instances on a private subnet through a single hardened jump host, so the private instances never need a public address.



Quake AI does not offer a managed bastion, jump-host, or session-broker service. The bastion in this guide is a virtual machine you own: a small instance you create, patch, and secure like any other internet-facing host. Before you put it in front of production instances, work through the [security hardening checklist](/docs/security/hardening-checklist) and keep its security group scoped to your administrators.



In this guide you will:

1. Create two security groups: one for the bastion, one for the private instances
2. Launch the bastion on the private network and give it the single floating IP this pattern needs
3. Launch private instances with no floating IP
4. Configure SSH `ProxyJump` on your workstation to reach the private instances through the bastion
5. Verify the connection and tear the pattern down



If you have used AWS Systems Manager Session Manager, EC2 Instance Connect, Azure Bastion, or Google Cloud IAP TCP forwarding, the building blocks here are the parts those services manage for you: an instance, a security group, a floating IP, and your own SSH client configuration. You operate them directly.



<PrerequisiteBlock methods={["console", "cli"]}>

- An [SSH key pair](/docs/tools/add-ssh-key) uploaded to your account
- A private network, subnet, and router already in place. Follow [How to create a VM on a private network](/docs/compute/how-to/create-vm-private-network) to build the network, subnet, and router used below (`my-private-net`, `my-private-subnet`, attached to `PublicStatic`)

</PrerequisiteBlock>



This pattern allocates exactly one floating IP: the bastion's public entry point. The private instances get no floating IP, which is what keeps them off the public internet.



## Step 1. Create the security groups

Create two security groups. The bastion group allows SSH only from your administrators' source addresses. The private group allows SSH only from the bastion group, so the private instances accept connections from the jump host and nothing else.

Replace `YOUR_ADMIN_CIDR` with the public address range your administrators connect from, for example `203.0.113.10/32` for a single static address.

<MethodTabs>
<Method label="Console">

<NoMoreButton />

1. Go to **Network** > **Security Groups** > **Create Security Group**.
2. Name the first group `bastion-sg` and select **OK**.
3. On the `bastion-sg` row, open the **Settings** gear icon dropdown and select **Create Rule**.
4. Select **SSH** for the protocol (TCP port 22). Set **Source** to **CIDR** and enter your administrators' range in **CIDR** (for example `203.0.113.10/32`). Select **OK**.
5. Select **Create Security Group** again, name the second group `private-sg`, and select **OK**.
6. On the `private-sg` row, open the **Settings** gear icon dropdown and select **Create Rule**.
7. Select **SSH** for the protocol (TCP port 22). Set **Source** to **Security Group** and select `bastion-sg` in the **Security** selector that appears. Select **OK**.

</Method>
<Method label="CLI">

```bash
openstack security group create bastion-sg
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip YOUR_ADMIN_CIDR \
  bastion-sg

openstack security group create private-sg
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-group bastion-sg \
  private-sg
```

Confirm the rules landed:

```bash
openstack security group rule list bastion-sg
openstack security group rule list private-sg
```

Each list shows one ingress rule for TCP port 22, scoped to the administrators' range and to `bastion-sg` respectively.

</Method>
</MethodTabs>

## Step 2. Launch the bastion and assign one floating IP

Launch a small instance on the private network with the `bastion-sg` security group, then associate the single floating IP. A shared flavor such as `s1a.small` is enough: the bastion forwards connections, it does not run application load.

<MethodTabs>
<Method label="Console">

1. Go to **Compute** > **Instances** > **Create Instance**.
2. On **Base Config** (Step 1), name the instance `bastion`, select `us-east-1a`, and choose a small flavor such as `s1a.small` (under the **Shared Resources** tab).
3. Select **Ubuntu-22.04** as the image, set disk to **10 GiB**, and check **Deleted with the instance**.
4. Select **Next: Network Config** and choose `my-private-net`.
5. For subnets, select **Automatically Assigned Address**.
6. Select the `default` and `bastion-sg` security groups. Clear any group that allows broad inbound SSH.
7. Select **Next: System Config**, choose **Keypair**, and select your key.
8. Select **Next: Confirm Config** and confirm.
9. On the **Instances** list, open the `bastion` row's **Networking** icon menu and select **Associate Floating IP**.
10. Select the bastion's private IP address, allocate one new floating IP from `PublicStatic`, and select **OK**.

</Method>
<Method label="CLI">

```bash
openstack server create \
  --flavor s1a.small \
  --image Ubuntu-22.04 \
  --boot-from-volume 10 \
  --network my-private-net \
  --key-name my-keypair \
  --security-group default \
  --security-group bastion-sg \
  bastion
```

Wait for the instance to become active, then allocate and associate the one floating IP:

```bash
openstack server show bastion -c status -c addresses

openstack floating ip create PublicStatic
openstack server add floating ip bastion FLOATING_IP_ADDRESS
```

Record `FLOATING_IP_ADDRESS`: it is the public address you connect to in Step 4.

</Method>
</MethodTabs>

## Step 3. Launch the private instances

Launch each application instance on the same private network with the `private-sg` security group and no floating IP. With no floating IP and a security group that only admits the bastion, these instances are reachable from the bastion and not from the public internet.

<MethodTabs>
<Method label="Console">

1. Go to **Compute** > **Instances** > **Create Instance**.
2. On **Base Config** (Step 1), name the instance `app-1`, select `us-east-1a`, and choose a flavor for your workload.
3. Select an image, set the disk size, and check **Deleted with the instance**.
4. Select **Next: Network Config** and choose `my-private-net`.
5. For subnets, select **Automatically Assigned Address**.
6. Select the `default` and `private-sg` security groups.
7. Select **Next: System Config**, choose **Keypair**, and select your key.
8. Select **Next: Confirm Config** and confirm.
9. Do not associate a floating IP with this instance.

</Method>
<Method label="CLI">

```bash
openstack server create \
  --flavor c2a.large \
  --image Ubuntu-22.04 \
  --boot-from-volume 20 \
  --network my-private-net \
  --key-name my-keypair \
  --security-group default \
  --security-group private-sg \
  app-1
```

Note the private address for use in Step 4:

```bash
openstack server show app-1 -c addresses
```

</Method>
</MethodTabs>

## Step 4. Configure SSH ProxyJump on your workstation

`ProxyJump` tells your SSH client to open a connection to the bastion first, then tunnel through it to the private instance in a single command. Your private key stays on your workstation: the bastion forwards an encrypted channel and never sees the key material.

Add a block to your `~/.ssh/config`, replacing the floating IP and private address with the values from Steps 2 and 3:

```text
Host bastion
    HostName FLOATING_IP_ADDRESS
    User ubuntu
    IdentityFile ~/.ssh/my-keypair

Host app-1
    HostName 192.168.200.0
    User ubuntu
    IdentityFile ~/.ssh/my-keypair
    ProxyJump bastion
```

Set `HostName` for `app-1` to the private address that `openstack server show app-1` reported. With this config, one command reaches the private instance through the bastion:

```bash
ssh app-1
```

To connect without editing `~/.ssh/config`, pass the jump host inline with `-J`:

```bash
ssh -J ubuntu@FLOATING_IP_ADDRESS ubuntu@192.168.200.0
```



`ProxyJump` keeps your private key on your workstation, so it is the safer default. Agent forwarding (`ssh -A` or `ForwardAgent yes`) exposes your local SSH agent socket to the bastion: anyone with root on the bastion can use your loaded keys while you are connected. Do not copy private keys onto the bastion either. If you must forward an agent for a specific task, scope it to a single host and confirm the bastion is hardened first.



## Verify the connection

Connect to the private instance through the bastion:

```bash
ssh app-1
```

On first connection your SSH client prompts you to accept the host key for both the bastion and the private instance. Type `yes` for each. Your prompt changes to `ubuntu@app-1`, which confirms the session terminated on the private instance after transiting the bastion.

Confirm the private instance is not reachable directly. A connection straight to its private address from your workstation, without the jump, times out because no floating IP and no public route exist:

```bash
ssh -o ConnectTimeout=5 ubuntu@192.168.200.0
```

Type `logout` to end the session.

## Harden the bastion

The bastion is the one host in this pattern exposed to the internet, so treat it as a security boundary. Beyond the full [security hardening checklist](/docs/security/hardening-checklist), apply these bastion-specific controls:

- Keep `bastion-sg` scoped to your administrators' source range. Do not widen port 22 to `0.0.0.0/0`.
- Disable password authentication and root login in `/etc/ssh/sshd_config` (`PasswordAuthentication no`, `PermitRootLogin no`), then reload `sshd`.
- Restrict who can log in with `AllowUsers` or `AllowGroups` so only operator accounts reach the bastion.
- Apply operating system security updates on a schedule. A jump host that lags on patches becomes the weakest link.
- Run nothing else on the bastion. A single-purpose host has a smaller attack surface than one that also serves an application.

## Tear down

Remove the pattern in reverse order. The single floating IP is released in the second step.

<MethodTabs>
<Method label="Console">

1. Delete each private instance (`app-1` and any others) under **Compute** > **Instances**.
2. On the `bastion` row, open the **Networking** icon menu and select **Disassociate Floating IP**, then release it under **Network** > **Floating IPs** > **Release**.
3. Delete the `bastion` instance.
4. Delete the `private-sg` and `bastion-sg` security groups under **Network** > **Security Groups**.

</Method>
<Method label="CLI">

```bash
openstack server delete --wait app-1

openstack server remove floating ip bastion FLOATING_IP_ADDRESS
openstack floating ip delete FLOATING_IP_ADDRESS
openstack server delete --wait bastion

openstack security group delete private-sg
openstack security group delete bastion-sg
```

</Method>
</MethodTabs>

Leave the private network, subnet, and router in place if other workloads use them. To remove them as well, follow the teardown steps in [How to create a VM on a private network](/docs/compute/how-to/create-vm-private-network).

## See also

- [How to create a VM on a private network](/docs/compute/how-to/create-vm-private-network)
- [How to create a security group](/docs/network/how-to/create-security-group)
- [How to create security group rules](/docs/network/how-to/create-security-group-rules)
- [How to allocate floating IP addresses](/docs/network/how-to/allocate-floating-ips)
- [Add an SSH key pair to your account](/docs/tools/add-ssh-key)
- [Security hardening checklist](/docs/security/hardening-checklist)
