# How to harden a Quake AI virtual machine

Source: https://docs.quake.ai/docs/compute/how-to/harden-production-vm
Markdown: https://docs.quake.ai/docs/compute/how-to/harden-production-vm.md

---

# How to harden a Quake AI virtual machine

Harden a freshly provisioned Linux virtual machine before it serves traffic. Restrict network access with security groups, reduce SSH exposure in the guest OS, add a host firewall, enable automatic security updates, and block repeated login attempts.



Quake AI enforces [security groups](/docs/network/concepts/security-groups) at the network edge. You configure `sshd`, the host firewall, OS packages, and user accounts inside the VM.



Start from a virtual machine you can SSH into. If you still need to create one, follow the [production virtual machine provisioning guide](/docs/compute/how-to/provision-production-vm) first.

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

- A running Linux virtual machine with working key-based SSH access
- The security group attached to the instance (note its name)
- Your admin workstation's public IP or CIDR (for SSH source scoping)
- Sudo on the VM

</PrerequisiteBlock>

## Step 1. Restrict the security group

Security groups filter traffic before it reaches the instance. Remove broad rules, then add the ports your workload needs.

Target posture for a typical web VM:

| Direction | Protocol | Port | Source | Purpose |
|---|---|---|---|---|
| Ingress | TCP | 22 | `ADMIN_CIDR` | SSH from your admin network |
| Ingress | TCP | 80 | `0.0.0.0/0`, reverse-proxy CIDR, or external edge CIDR | HTTP (if the VM serves web traffic) |
| Ingress | TCP | 443 | `0.0.0.0/0`, reverse-proxy CIDR, or external edge CIDR | HTTPS (if the VM serves web traffic) |
| Egress | Any | Any | `0.0.0.0/0` | Outbound updates and API calls (default on custom groups) |

Replace `ADMIN_CIDR` with your office or home `/32` (for example, `203.0.113.45/32`). Use `0.0.0.0/0` for SSH only when you cannot name a narrower range.

<MethodTabs>
<Method label="Console">

<NoMoreButton />

1. Open **Network** > **Security Groups** and select the group attached to your instance.
2. On the **Rules** tab, delete ingress rules that expose SSH or application ports to **All Traffic** (`0.0.0.0/0`) unless that exposure is intentional.
3. Select **Create Rule** and add SSH restricted to your admin CIDR. Use **Custom TCP Rule**, **Port** `22`, **Direction** **Ingress**, **Source** **CIDR**, and enter your admin range.
4. Add application ports the same way. See [How to create security group rules](/docs/network/how-to/create-security-group-rules) for preset and wildcard behavior.

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

List the group's rules. The CLI prints the protocol, port, direction, and source for each rule:

```bash
openstack security group rule list SECURITY_GROUP
```

Delete an overly broad SSH rule (replace `RULE_ID` with the rule UUID):

```bash
openstack security group rule delete RULE_ID
```

Add SSH from your admin CIDR:

```bash
openstack security group rule create \
  --protocol tcp --dst-port 22 \
  --remote-ip ADMIN_CIDR \
  SECURITY_GROUP
```

Add HTTP and HTTPS if the VM serves web traffic:

```bash
openstack security group rule create \
  --protocol tcp --dst-port 80 \
  --remote-ip 0.0.0.0/0 \
  SECURITY_GROUP

openstack security group rule create \
  --protocol tcp --dst-port 443 \
  --remote-ip 0.0.0.0/0 \
  SECURITY_GROUP
```

</Method>
</MethodTabs>

## Step 2. Create a non-root sudo user

Run day-to-day work as an unprivileged account. Keep the bootstrap user (often `ubuntu` or `cloud-user`) until the new account works.

Connect from your workstation with the bootstrap user. Keep this session open through the remaining steps:

```bash
ssh BOOTSTRAP_USER@VM_FLOATING_IP
```

The SSH prompt confirms that the guest OS is ready for the following commands.




```bash
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
```

Open a second SSH session and confirm key login as `deploy` before you disable root login in Step 3.




```bash
sudo useradd -m deploy
sudo usermod -aG wheel deploy
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
```

Open a second SSH session and confirm key login as `deploy` before you disable root login in Step 3.




For a repeatable baseline, see the [Configure a VM with Ansible](/resources/iac-templates/ansible-configure-vm) template.

## Step 3. Harden secure shell access

Edit `sshd_config`, validate the syntax, and reload the daemon. Keep an existing session open while you test changes.




```bash
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
echo 'MaxAuthTries 3' | sudo tee -a /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl reload ssh
```




```bash
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
echo 'MaxAuthTries 3' | sudo tee -a /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl reload sshd
```




Optional: change the SSH port only if your security group and host firewall rules already allow the new port. Update the security group before you change `Port` in `sshd_config`.

## Step 4. Enable a host firewall

A host firewall adds a second traffic filter behind the security group. A connection succeeds when both layers allow it.




```bash
sudo apt update
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
# Omit the next two rules when the VM does not serve web traffic.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
```




```bash
sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
# Omit the next two rules when the VM does not serve web traffic.
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
```




## Step 5. Enable automatic security updates

Configure the guest OS to install security patches automatically. This covers unattended security fixes; plan reboot windows when kernels update.




```bash
sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades
```

Confirm `/etc/apt/apt.conf.d/50unattended-upgrades` limits upgrades to security origins and that `/etc/apt/apt.conf.d/20auto-upgrades` contains:

```text
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
```




```bash
sudo dnf install -y dnf-automatic
sudo sed -i 's/apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.conf
sudo sed -i 's/upgrade_type = default/upgrade_type = security/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer
```




## Step 6. Install fail2ban for SSH

fail2ban watches authentication failures and temporarily blocks offending source IPs in the host firewall.




```bash
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
```




```bash
sudo dnf install -y fail2ban
sudo tee /etc/fail2ban/jail.local <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
```




## Step 7. Verify the hardening

Run these checks from your workstation and on the VM.

**Key-only SSH works for the sudo user:**

```bash
ssh deploy@VM_FLOATING_IP
```

**The SSH daemon refuses password login** (expect `Permission denied`):

```bash
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no deploy@VM_FLOATING_IP
```

**Intended ports respond from the internet.** From outside the VM, scan the floating IP:

```bash
nc -zv VM_FLOATING_IP 22
nc -zv VM_FLOATING_IP 80
nc -zv VM_FLOATING_IP 443
nc -zv VM_FLOATING_IP 3306
```

Port 3306 (or another port you did not open) should fail to connect.

**On the VM**, confirm services:

```bash
sudo ss -tlnp
sudo systemctl is-active fail2ban
```

## Related documentation

- [Production virtual machine provisioning guide](/docs/compute/how-to/provision-production-vm): launch workflow this page continues
- [How to patch and update a Quake AI virtual machine](/docs/compute/how-to/patch-and-update-vm): ongoing OS updates after hardening
- [Security hardening checklist](/docs/security/hardening-checklist): project-wide audit list
- [Security groups concept](/docs/network/concepts/security-groups): how the Network service filters instance traffic
- [How to create security group rules](/docs/network/how-to/create-security-group-rules): rule fields and Console presets
- [Configure a VM with Ansible](/resources/iac-templates/ansible-configure-vm): idempotent hardening playbook
