<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://proxmoxpulse.com</id>
    <title>Proxmox Pulse</title>
    <updated>2026-05-06T01:47:11.942Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Proxmox Pulse</name>
        <email>hello@proxmoxpulse.com</email>
        <uri>https://proxmoxpulse.com</uri>
    </author>
    <link rel="alternate" href="https://proxmoxpulse.com"/>
    <subtitle>In-depth Proxmox VE tutorials, tips, and best practices for homelab enthusiasts and system administrators. Covers installation, VMs, LXC containers, storage, networking, and more.</subtitle>
    <logo>https://proxmoxpulse.com/images/og-default.png</logo>
    <icon>https://proxmoxpulse.com/favicon-32x32.png</icon>
    <rights>© 2026 Proxmox Pulse</rights>
    <entry>
        <title type="html"><![CDATA[Proxmox VE LDAP and Active Directory Authentication]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-ldap-active-directory-authentication/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-ldap-active-directory-authentication/"/>
        <updated>2026-05-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Configure Proxmox VE to authenticate users via LDAP or Active Directory. Step-by-step realm setup, group sync, role mapping, and troubleshooting tips included.]]></summary>
        <content type="html"><![CDATA[
Connecting Proxmox VE to your existing LDAP directory or Active Directory domain means every admin logs in with their corporate credentials — no separate Proxmox password to juggle, no shared `root@pam` account floating around, and a proper audit trail in `/var/log/auth.log` showing exactly who authenticated and when. By the end of this guide, you'll have a working LDAP realm in Proxmox VE 9.1, users synchronized from your directory, and AD security groups mapped directly to Proxmox roles.

## Key Takeaways

- **Realm types**: Proxmox supports PAM, PVE, LDAP, Active Directory, and OIDC — each with different trust models and sync capabilities.
- **Sync is optional but useful**: You can authenticate against LDAP without pre-syncing users, but syncing lets you assign roles via the GUI and see usernames in the audit log.
- **Groups map to roles**: Assign an entire AD group to a Proxmox role once; every member inherits the permission at the specified resource path.
- **Keep a local escape hatch**: Always maintain a tested local `root@pam` or `admin@pve` account — if LDAP becomes unreachable, you need a way back in.
- **TLS is non-negotiable**: Use LDAPS (port 636) or STARTTLS; plain LDAP on port 389 sends credentials in cleartext.

## Why Centralized Auth Beats Local Proxmox Users

The default Proxmox setup gives you `root@pam` backed by Linux PAM and local users backed by Proxmox's own PVE database. Both work fine for a single-node homelab where you're the only admin. Scale to a team of three, or expand into a [multi-node Proxmox private cloud](/articles/build-private-cloud-home-proxmox-ve/), and the cracks show fast:

- Password rotation means touching every node and every user manually
- You have no idea which "admin" shut down a production VM at 2 AM
- Onboarding and offboarding requires logging into Proxmox specifically, not your IdP

LDAP integration solves all three. It also lets you reuse existing group structures — your "Infrastructure Admins" AD group becomes a Proxmox Administrator role assignment in about 60 seconds.

**Active Directory vs plain LDAP**: The Proxmox realm type labeled "Active Directory" in the GUI is still LDAP under the hood, but it pre-fills sane defaults for Microsoft's schema (`sAMAccountName` attribute, `DC=` base DN format, Kerberos realm field). If you're running OpenLDAP, FreeIPA, or Authentik with an LDAP backend, use the generic "LDAP" realm type instead.

## Prerequisites: What You Need Before You Start

Before touching the Proxmox GUI, gather these details:

- **LDAP server hostname** — a domain controller FQDN for AD, or your OpenLDAP server address. Use the FQDN, not an IP — it matters for TLS certificate validation.
- **Base DN** — e.g., `DC=corp,DC=example,DC=com` for AD, or `dc=example,dc=org` for OpenLDAP.
- **Bind account** — a read-only service account in AD (e.g., `svc-proxmox`) with permission to read users and groups. Do not use a Domain Admin for this.
- **Bind account DN or UPN** — e.g., `svc-proxmox@corp.example.com` (UPN format works cleanly for AD).
- **Bind password** — stored encrypted by Proxmox under `/etc/pve/priv/`, but still: use a long, random password for this service account.
- **CA certificate** — if you're using LDAPS with a private CA, you need the certificate chain in PEM format.

Create the bind account on the AD side first:

```powershell
# Run on a Windows Server domain controller
New-ADUser -Name "svc-proxmox" `
  -SamAccountName "svc-proxmox" `
  -UserPrincipalName "svc-proxmox@corp.example.com" `
  -Path "OU=Service Accounts,DC=corp,DC=example,DC=com" `
  -AccountPassword (ConvertTo-SecureString "YourLongRandomPassword!" -AsPlainText -Force) `
  -PasswordNeverExpires $true `
  -Enabled $true
```

By default, Domain Users can read user and group objects in AD, so `svc-proxmox` being a Domain User is enough — no special delegation required.

## How to Add an LDAP Realm in Proxmox VE

### Open the Realm Configuration

In the Proxmox web UI, navigate to **Datacenter → Permissions → Authentication**. You'll see the two default realms (`pve` and `pam`). Click **Add** and choose **Active Directory Server** or **LDAP Server** depending on your directory type.

### Fill in Server and Bind Details

For Active Directory, the form fields map like this:

| Field | Example Value | Notes |
|-------|--------------|-------|
| Realm | `corp` | Short name used at login: `user@corp` |
| Base Domain Name | `corp.example.com` | Proxmox derives the base DN automatically |
| Server | `dc01.corp.example.com` | Use FQDN — required for TLS |
| Port | `636` | LDAPS; use `389` + STARTTLS if LDAPS is unavailable |
| User Attribute | `sAMAccountName` | For AD; use `uid` for OpenLDAP or FreeIPA |
| Domain | `corp.example.com` | Kerberos realm field (AD only) |
| Bind User | `svc-proxmox@corp.example.com` | UPN format works cleanly for AD |
| Bind Password | `YourLongRandomPassword!` | Stored encrypted in `/etc/pve/priv/` |

For OpenLDAP or FreeIPA, the equivalent settings:

```yaml
# OpenLDAP / FreeIPA realm values
Base DN:       dc=example,dc=org
Server:        ldap.example.org
Port:          636
User Attr:     uid
Bind DN:       cn=svc-proxmox,ou=serviceaccounts,dc=example,dc=org
Bind Password: YourLongRandomPassword!
```

### Import the CA Certificate for TLS

If your LDAP server uses a certificate signed by a private CA — which is almost always the case in enterprise Active Directory environments — import that CA certificate into Proxmox's trust store before saving the realm:

```bash
# Copy your CA cert (PEM format) to the system trust store
cp /path/to/corp-ca.pem /usr/local/share/ca-certificates/corp-ca.crt
update-ca-certificates
```

After importing, Proxmox will verify the LDAPS certificate against the system trust store. Without this step, you're forced to disable certificate verification — acceptable on a homelab, never in production.

**Gotcha**: If your AD uses an intermediate CA, you need the full chain, not just the root. Export it from Active Directory Certificate Services:

```powershell
# On a domain controller — exports the full issuing chain
certutil -ca.cert corp-ca-chain.crt
```

SCP that file to your Proxmox node and run `update-ca-certificates` again.

### Configure Sync Attributes

In the realm editor, switch to the **Sync** tab and set:

- **Sync Attributes**: `cn,mail,sAMAccountName` for AD (or `cn,mail,uid` for OpenLDAP)
- **User Classes**: `user` for AD (or `inetOrgPerson` for OpenLDAP)
- **Group Classes**: `group` for AD (or `groupOfNames` / `posixGroup` for OpenLDAP)
- **Group DN**: the OU where your infra groups live, e.g., `OU=Infra Groups,DC=corp,DC=example,DC=com`

Save the realm. If Proxmox reports a bind error immediately, double-check the bind account UPN and password — don't proceed until the realm saves cleanly.

## How to Sync Users and Groups from Active Directory

### Running the First Sync

Trigger a manual sync from the CLI rather than the GUI — the CLI output is more informative:

```bash
# Sync both users and groups from the 'corp' realm
pveum realm sync corp --enable-new true --purge false --scope both
```

`--scope both` pulls users and groups. `--enable-new true` marks newly synced users as enabled in Proxmox. `--purge false` leaves existing Proxmox users untouched if they no longer appear in LDAP — safer for a first run.

Expected output:

```
syncing realm 'corp'...
synced 47 users
synced 12 groups
done
```

Verify in the GUI under **Datacenter → Permissions → Users** — your AD users appear with the `@corp` suffix, and groups appear under **Datacenter → Permissions → Groups**.

### Scheduling Automatic Syncs

Don't rely on manual syncs for production. Add a cron job to keep Proxmox in sync with your directory:

```bash
# /etc/cron.d/proxmox-ldap-sync
# Sync the corp realm every 4 hours
0 */4 * * * root /usr/bin/pveum realm sync corp --enable-new true --purge false --scope both >> /var/log/proxmox-ldap-sync.log 2>&1
```

With `--purge false`, users removed from AD won't lose Proxmox access until you enable purge or manually disable them. In environments with strict offboarding requirements, run a weekly purge job as well:

```bash
# Weekly purge — removes Proxmox users no longer in LDAP
0 5 * * 0 root /usr/bin/pveum realm sync corp --purge true --scope users >> /var/log/proxmox-ldap-sync.log 2>&1
```

Pair this with [SSH hardening and fail2ban](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) to make sure stale accounts left behind by a delayed purge cycle can't be exploited.

## Mapping AD Groups to Proxmox Roles

This is where centralized auth pays off. Instead of assigning roles to individual users, assign them to groups — Proxmox respects LDAP group membership for permission decisions.

First, confirm groups appeared after the sync:

```bash
pveum group list
```

Then assign roles to groups at the appropriate resource path:

```bash
# 'infra-admins' AD group gets full Administrator access cluster-wide
pveum acl modify / --group 'infra-admins' --role Administrator

# 'dev-team' gets VM admin rights only in the dev resource pool
pveum acl modify /pool/dev-pool --group 'dev-team' --role PVEVMAdmin

# Read-only auditors can view everything, change nothing
pveum acl modify / --group 'proxmox-readonly' --role PVEAuditor
```

The built-in roles worth knowing:

| Role | What It Allows |
|------|---------------|
| `Administrator` | Full cluster access — treat like root for Proxmox operations |
| `PVEAdmin` | Manage VMs, storage, networks — no user or realm management |
| `PVEVMAdmin` | Full VM lifecycle — no node or storage administration |
| `PVEVMUser` | Start, stop, and open console — no config changes |
| `PVEAuditor` | Read-only view of the entire cluster |
| `PVEDatastoreAdmin` | Manage backups and storage pools — useful for dedicated backup operators |

If none of these fit, create a custom role:

```bash
pveum role add StorageOperator \
  --privs "Datastore.Audit,Datastore.AllocateSpace,Datastore.AllocateTemplate"
```

## Testing and Verifying Authentication End-to-End

Before announcing the change to your team, verify the full chain works from the Proxmox host itself:

```bash
# Test the LDAP bind directly (install if missing: apt-get install -y ldap-utils)
ldapsearch -H ldaps://dc01.corp.example.com \
  -D "svc-proxmox@corp.example.com" \
  -w "YourLongRandomPassword!" \
  -b "DC=corp,DC=example,DC=com" \
  "(sAMAccountName=testuser)" cn mail
```

A successful bind returns `cn` and `mail` attributes for the test user. Common errors:

- `ldap_bind: Invalid credentials (49)` — wrong bind DN or password
- `Can't contact LDAP server` — firewall blocking port 636 or DNS failure; test reachability with:

```bash
nc -zv dc01.corp.example.com 636
```

- `TLS: hostname does not match` — the server cert CN or SAN doesn't match the hostname you configured; use the FQDN that matches the certificate

Then test interactively: log out of the Proxmox GUI, select the `corp` realm in the login dropdown, and authenticate as a known AD user. If you're using [Ansible playbooks to manage your Proxmox cluster](/articles/automate-proxmox-ansible-vm-playbooks/), add an LDAP connectivity check task to your pre-upgrade playbook — LDAP failures discovered mid-upgrade are painful.

If realm config changes aren't taking effect, restart `pvedaemon`:

```bash
systemctl restart pvedaemon
```

## What to Do When LDAP Auth Breaks

The most common failure scenario: the LDAP server becomes unreachable, or the bind account password expires, and suddenly nobody can log in. This is exactly why you keep a local admin account active and tested.

```bash
# Log in as root@pam via console, iDRAC, iLO, or IPMI
# Then check configured realms
pveum realm list

# Disable the broken realm while you investigate
# (existing sessions continue; new logins to this realm fail with a clean error)
pveum realm modify corp --disable true

# Re-enable once the LDAP issue is resolved
pveum realm modify corp --disable false
```

Common root causes when LDAP breaks suddenly:

1. **Bind account password expired** — check in AD:
```powershell
Search-ADAccount -PasswordExpired -UsersOnly | Select-Object Name, PasswordExpiredAt
```

2. **Certificate expired** — check the LDAPS cert expiry from the Proxmox host:
```bash
echo | openssl s_client -connect dc01.corp.example.com:636 2>/dev/null \
  | openssl x509 -noout -dates
```

3. **Domain controller unreachable** — DNS change, firewall rule update, or DC maintenance window

4. **Realm config corrupted after a Proxmox upgrade** — inspect `/etc/pve/domains.cfg` directly for garbled entries; the file is plain text and editable

Active sessions are unaffected when LDAP goes down — Proxmox session tokens aren't re-validated against LDAP on every request. Anyone already logged in continues working until their session expires (default timeout is 2 hours). New logins fail.

## Conclusion

Proxmox VE's LDAP and Active Directory integration eliminates the overhead of managing Proxmox-specific passwords for every admin — the setup takes under 30 minutes once you have the bind account and base DN in hand. The discipline that makes it reliable over the long term is three things: a tested local admin account as a fallback, LDAPS with verified certificates in production, and scheduled sync jobs rather than ad-hoc manual runs. Once AD groups map to Proxmox roles, onboarding a new team member is as simple as adding them to the right security group — Proxmox picks it up on the next sync. To close down the remaining attack surface after enabling centralized auth, work through [SSH hardening, fail2ban, and the Proxmox firewall](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) as your next step.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="ldap"/>
        <category label="active-directory"/>
        <category label="authentication"/>
        <category label="access-control"/>
        <category label="proxmox-ve"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox API Tokens for Secure Automation Without Root]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-api-tokens-secure-automation/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-api-tokens-secure-automation/"/>
        <updated>2026-05-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Stop putting root credentials in Terraform and Ansible. Learn to create scoped Proxmox API tokens with least-privilege ACLs you can revoke in seconds.]]></summary>
        <content type="html"><![CDATA[
If you are using `root@pam` credentials in your Terraform provider, Ansible inventory, or curl scripts against the Proxmox API, you have a ticking time bomb buried in your config files. Proxmox VE 7+ introduced scoped API tokens that let you grant exactly the permissions an automation tool needs — and revoke them instantly if a secret leaks. By the end of this guide you will have a dedicated service user, a scoped token with its own ACL assignments, and a tested workflow that keeps root credentials out of every script, pipeline, and `.env` file.

## Key Takeaways

- **Token format**: Token IDs always follow `user@realm!token-name`; the secret is shown once at creation and never again
- **Three CLI commands**: `pveum user add`, `pveum acl modify`, and `pveum user token add` is all it takes from the Proxmox shell
- **Privilege separation**: Enable `--privsep 1` so the token's ACLs are scoped independently from the user's — narrower blast radius on leak
- **PVE realm only**: Use `automation@pve` over `automation@pam` for service accounts — no Linux shadow entry means no pivot path to shell
- **Revocation is instant**: `pveum user token remove` kills the token immediately with no grace period and no other credentials affected

## Why Root Credentials in Automation Are a Real Risk

Most quick-start tutorials reach for `root@pam` plus a password because it works immediately. Then that credential ends up in a Terraform `tfvars` file, a `.env` committed to a private repo, or an Ansible vault that three team members share. When you need to rotate the root password — or when someone leaves the team — you break every integration simultaneously.

The safer pattern is a dedicated service account with an API token scoped to exactly what the automation needs. If a token leaks, you run one command to revoke it. The root account and every other integration is untouched.

With Proxmox VE 9.1 running on most production and homelab nodes now, the per-token ACL system is mature and well-tested. This pairs naturally with the network and host-level controls covered in the [Proxmox firewall, fail2ban, and SSH hardening guide](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) — API token hygiene is the equivalent layer for the management plane.

## Understanding the Proxmox Permission Model

Before creating tokens, understand the three-layer structure that controls what any principal can do:

- **Users** — authenticated identities such as `root@pam` or `automation@pve`
- **Roles** — named permission bundles such as `PVEVMAdmin`, `PVEAuditor`, or `Administrator`
- **ACLs** — bindings of `(path, principal, role)` that express "this user has this role at this path"

Paths are hierarchical. `/` covers the entire cluster, `/vms` covers all VMs and containers, `/vms/100` covers only VM 100, `/nodes/pve` covers only the node named `pve`, and `/storage/local-lvm` covers only that pool.

API tokens layer on top of users. By default a token inherits all of its owning user's ACLs. If you enable **Privilege Separation**, the token's effective permissions become the intersection of the user's ACLs and the token's own ACLs — so you can restrict a token below what the user has, but never grant it more.

| Privilege Separation | Token ACL | Effective Permission |
|---|---|---|
| Disabled | — | Identical to owning user |
| Enabled | PVEAuditor on `/` | User's ACLs ∩ PVEAuditor on `/` |
| Enabled | PVEVMAdmin on `/vms` | User's ACLs ∩ PVEVMAdmin on `/vms` |
| Enabled | No ACL assigned | Zero permissions |

The intersection model means you must assign ACLs to both the user and the token when privilege separation is on.

## How to Create a Dedicated Service User

Create the account from the Proxmox shell. Using the `pve` realm keeps it internal to Proxmox — no PAM entry, no shadow file, no SSH login path:

```bash
# Create a local PVE user — no password needed, token-only auth
pveum user add automation@pve --comment "Terraform/Ansible service account"
```

For multi-system setups, create one user per automation system: `terraform@pve`, `ansible@pve`, `monitoring@pve`. That way revoking the Terraform token does not affect Ansible, and you can audit API activity per-system in the Proxmox task log.

## Assign Roles with Least Privilege

Choose roles that match what the automation actually does — not the widest role that makes it work:

| Role | Grants |
|---|---|
| `PVEVMAdmin` | Full VM/container lifecycle: create, configure, start, stop, delete |
| `PVEVMUser` | Start, stop, and console only — no create or delete |
| `PVEDatastoreAdmin` | Manage storage, backups, snapshots, upload ISOs |
| `PVEDatastoreUser` | Allocate disk space for VM disks only |
| `PVEAuditor` | Read-only across all paths |
| `PVESysAdmin` | Node-level operations: certs, services, network config |

For a Terraform workflow that provisions and destroys VMs:

```bash
# VM lifecycle access across all VMs
pveum acl modify /vms --users automation@pve --roles PVEVMAdmin

# Disk allocation on the target storage pool
pveum acl modify /storage/local-lvm --users automation@pve --roles PVEDatastoreUser

# ISO and template upload if Terraform manages cloud-init images
pveum acl modify /storage/local --users automation@pve --roles PVEDatastoreAdmin
```

For an Ansible read-only audit pass before [automating full VM provisioning playbooks with Ansible](/articles/automate-proxmox-ansible-vm-playbooks/):

```bash
pveum acl modify / --users automation@pve --roles PVEAuditor
```

For a [K3s cluster on Proxmox VMs](/articles/k3s-kubernetes-cluster-proxmox-vms/) where Terraform manages both nodes and their storage:

```bash
pveum acl modify /vms --users automation@pve --roles PVEVMAdmin
pveum acl modify /storage/local-lvm --users automation@pve --roles PVEDatastoreAdmin
```

## Create the API Token

Create the token with privilege separation enabled:

```bash
pveum user token add automation@pve terraform --privsep 1 --comment "Terraform provider 2026-05"
```

Proxmox prints a table with the token ID and secret:

```
┌──────────────────┬──────────────────────────────────────┐
│ key              │ value                                │
╞══════════════════╪══════════════════════════════════════╡
│ full-tokenid     │ automation@pve!terraform             │
│ info             │ {"privsep":"1"}                      │
│ value            │ a1b2c3d4-e5f6-7890-abcd-ef1234567890 │
└──────────────────┴──────────────────────────────────────┘
```

The `value` field is the token secret. It is displayed **exactly once**. Store it in your secrets manager — HashiCorp Vault, Bitwarden Secrets, a GitHub Actions secret, or a locally-encrypted `.env` file — before closing the terminal. If you lose it, delete the token and create a new one.

Now assign ACLs directly to the token. With privilege separation enabled, the token has zero permissions until you do this:

```bash
# Grant token access to VM management
pveum acl modify /vms \
  --users automation@pve \
  --tokens automation@pve!terraform \
  --roles PVEVMAdmin

# Grant token access to the storage pool
pveum acl modify /storage/local-lvm \
  --users automation@pve \
  --tokens automation@pve!terraform \
  --roles PVEDatastoreUser
```

You can scope the token more narrowly than the user. The user might have `/vms` admin access, but a specific token can be restricted to `/vms/100` through individual VM ACL assignments — useful for a deployment token that should only ever touch its own VMs.

## Test the Token Before Wiring It Into Anything

Verify the token works with a raw API call before touching Terraform or Ansible:

```bash
TOKEN_ID="automation@pve!terraform"
TOKEN_SECRET="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
PROXMOX_HOST="192.168.1.10"

curl -s -k \
  -H "Authorization: PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}" \
  "https://${PROXMOX_HOST}:8006/api2/json/nodes" \
  | python3 -m json.tool
```

A successful response returns a JSON array of cluster nodes. A 401 means the token ID or secret is wrong. A 403 means the token authenticated but lacks permissions at that path — recheck your `pveum acl modify` commands and confirm the ACL was applied to the token ID, not just the user.

To list all current ACLs for debugging:

```bash
pveum acl list
```

## Using the Token in Terraform

The `bpg/proxmox` Terraform provider accepts API tokens natively. Configure your provider block:

```hcl
terraform {
  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "~> 0.66"
    }
  }
}

provider "proxmox" {
  endpoint  = "https://192.168.1.10:8006/"
  api_token = var.proxmox_api_token
  insecure  = false
}
```

In `terraform.tfvars` — add this file to `.gitignore` before your first commit:

```hcl
proxmox_api_token = "automation@pve!terraform=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```

Or export it as an environment variable so it never touches the filesystem at all:

```bash
export TF_VAR_proxmox_api_token="automation@pve!terraform=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```

## Using the Token in Ansible

The `community.general.proxmox_kvm` module accepts token credentials directly:

```yaml
- name: Provision a VM
  community.general.proxmox_kvm:
    api_host: 192.168.1.10
    api_user: automation@pve
    api_token_id: terraform
    api_token_secret: "{{ proxmox_token_secret }}"
    node: pve
    name: my-vm
    cores: 2
    memory: 2048
    state: present
```

Store `proxmox_token_secret` in an Ansible Vault file, not in a plaintext `group_vars` file. Use `ansible-vault encrypt_string` to inline-encrypt the value if you prefer a single-file setup over a separate vault.

## Rotating and Revoking Tokens

List all tokens for a user:

```bash
pveum user token list automation@pve
```

Revoke a token immediately — effective in seconds, no grace period:

```bash
pveum user token remove automation@pve terraform
```

For a zero-downtime rotation, create the replacement first, then remove the old one:

```bash
# 1. Create replacement token
pveum user token add automation@pve terraform-v2 --privsep 1 --comment "Rotated 2026-05-05"

# 2. Assign ACLs to the new token
pveum acl modify /vms \
  --users automation@pve \
  --tokens automation@pve!terraform-v2 \
  --roles PVEVMAdmin
pveum acl modify /storage/local-lvm \
  --users automation@pve \
  --tokens automation@pve!terraform-v2 \
  --roles PVEDatastoreUser

# 3. Update secrets manager, test new token, update all consumers
# 4. Remove old token
pveum user token remove automation@pve terraform
```

The gotcha here: token ACLs are attached to the token ID, not the user. When you delete and recreate a token — even with the identical name — all ACLs are gone and must be reassigned from scratch. Keep a documented list of which ACLs each token needs, or script the reassignment as part of your rotation runbook so you do not rediscover this at 2am.

## Why the PVE Realm Is Safer for Service Accounts

A `automation@pve` user has no Linux system account. There is no `/etc/passwd` entry, no shadow file entry, and no ability to SSH into the Proxmox host using those credentials directly. If the API token is compromised, the attacker has API access scoped to the token's ACLs — they cannot pivot to an interactive shell on the hypervisor.

A `automation@pam` user maps to a real Linux account that does have a shadow entry. Depending on your SSH configuration, that user may be able to authenticate to the host directly. The [LOLPROX analysis of Proxmox hypervisor exploit paths](/articles/lolprox-protecting-proxmox-from-hypervisor-exploits/) makes this point clearly: reducing the identity attack surface at the API layer is one of the highest-leverage hardening steps available.

Use the PVE realm for every service account. Reserve PAM accounts for humans who need both shell access and web UI access.

## Conclusion

Replacing root credentials with scoped API tokens is a 20-minute change that pays back every time you rotate a secret, offboard a team member, or recover from a credential leak without touching your root account. Create `automation@pve`, assign narrowly-scoped ACLs, enable privilege separation on the token, store the secret in a proper secrets manager, and document the ACL assignments for your rotation runbook. The natural next step is putting this token to work in a structured Ansible workflow — the [Ansible VM automation guide](/articles/automate-proxmox-ansible-vm-playbooks/) walks through a production-ready inventory approach that pairs directly with what you set up here.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="api-tokens"/>
        <category label="security"/>
        <category label="automation"/>
        <category label="proxmox-api"/>
        <category label="least-privilege"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox LXC Resource Limits: CPU, Memory, and Disk I/O]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-lxc-resource-limits/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-lxc-resource-limits/"/>
        <updated>2026-05-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set CPU, memory, and disk I/O limits on Proxmox LXC containers using cgroups v2. Real pct commands, hook scripts, and hard-learned pitfalls — most apply live without a restart.]]></summary>
        <content type="html"><![CDATA[
Setting resource limits on Proxmox LXC containers is one of those tasks that pays dividends the first time a bulk backup job, a Nextcloud sync, or a rogue cron script saturates your host. By the end of this guide you'll know exactly which `pct` options map to which cgroup v2 controls, how to apply most of them live without a container restart, and which edge cases catch people off guard on Proxmox VE 9.1.

## Key Takeaways

- **CPU limit vs. CPU units**: `--cpulimit` caps absolute CPU time (2.0 = max 2 physical core-equivalents); `--cpuunits` controls relative scheduling priority between containers under contention.
- **Memory is a hard wall**: When a container hits its `--memory` ceiling, the OOM-killer fires — not graceful throttling. Set limits with headroom.
- **Live application**: `--cpulimit`, `--memory`, and `--swap` take effect immediately with `pct set` — no container restart needed.
- **cgroups v2 paths changed**: Proxmox VE 8+ uses the unified cgroups v2 hierarchy. I/O throttling uses `io.max`, not the v1 `blkio.throttle.*` paths you'll find in older tutorials.
- **Measure before you cap**: `pct monitor <ctid>` shows real-time consumption; tune from data, not guesses.

## How Proxmox LXC Resource Controls Work Under the Hood

LXC containers on Proxmox are namespaced processes sharing the host kernel — no hypervisor overhead, but also no hardware isolation. Resource limits are enforced entirely by Linux cgroup v2. When you run `pct set 101 --cpulimit 2`, Proxmox writes to `/sys/fs/cgroup/lxc/101/cpu.max` and the kernel scheduler does the rest.

Each container gets its own cgroup slice. On Proxmox VE 9.1 you can inspect the full hierarchy:

```bash
# Inspect the cgroup tree for container 101
systemd-cgls /sys/fs/cgroup/lxc/101
```

Three resource classes matter for most workloads:

1. **CPU** — absolute quota and relative scheduling weight
2. **Memory** — hard ceiling, swap budget, and soft pressure hints
3. **Block I/O** — bandwidth and IOPS caps per device

All three can be configured persistently via `pct set`. Changes survive reboots because Proxmox writes them back to `/etc/pve/lxc/<ctid>.conf`. The one exception is I/O throttling — direct cgroup writes don't survive container restarts, which is why hook scripts matter.

## How to Set CPU Limits on LXC Containers

### The Difference Between Cores, CPU Limit, and CPU Units

These three settings look similar in the Proxmox UI but control completely different scheduler knobs:

| Option | cgroup v2 knob | What it actually does |
|---|---|---|
| `--cores N` | `cpuset.cpus` | Container sees N vCPU threads |
| `--cpulimit N` | `cpu.max` | Hard cap: N × 100% of one physical core |
| `--cpuunits N` | `cpu.weight` | Relative scheduling priority (default: 1024) |

`--cpulimit` is the ceiling that actually prevents CPU saturation. Setting it to `2.0` means the container can consume at most 200% CPU — two full core-equivalents of wall-clock time — regardless of how many cores the host has or how many the container can see via `--cores`.

```bash
# Cap container 101 to 1.5 cores worth of CPU — applies immediately
pct set 101 --cpulimit 1.5

# Lower scheduling priority of a bulk-processing background container
pct set 102 --cpuunits 256

# Pin a latency-sensitive container to 2 threads with a matching hard cap
pct set 103 --cores 2 --cpulimit 2
```

**Gotcha from experience**: On a 16-core host with four containers each set to `--cores 4`, all four can simultaneously peg all their cores if no `--cpulimit` is configured. I watched a Jellyfin container doing 4K transcodes bring a database container to its knees this way. Always pair `--cores` with `--cpulimit` for workloads you don't fully trust.

### Verifying the CPU Limit Took Effect

```bash
# cpu.max format is: quota period (in microseconds)
# 150000 100000 = 150ms per 100ms window = 1.5 cores
cat /sys/fs/cgroup/lxc/101/cpu.max

# Or read directly from the Proxmox config
pct config 101 | grep cpu
```

The `cpu.stat` file shows cumulative throttled time — useful for confirming a container is actually hitting its limit:

```bash
cat /sys/fs/cgroup/lxc/101/cpu.stat | grep throttled
# throttled_usec 14230891  ← non-zero means the cap is being enforced
```

## Configuring Memory and Swap in Proxmox LXC

Memory configuration uses two knobs that work together:

```bash
# Give container 105 2 GB RAM and 512 MB swap
pct set 105 --memory 2048 --swap 512
```

The part that trips people up: `--swap` is **additive**, not a total budget. The container above gets 2048 MB RAM **plus** 512 MB of swap space on top — 2560 MB of total virtual memory. If you set `--memory 2048 --swap 0`, the container has exactly 2048 MB and no swap at all.

For latency-sensitive workloads like databases, disable swap entirely:

```bash
# Database container: 4 GB hard limit, no swap, no latency spikes from swapping
pct set 106 --memory 4096 --swap 0
```

### Why the OOM-Killer Fires Instead of Throttling

When a container hits its memory ceiling, the Linux kernel doesn't pause it or throttle allocations — it runs the OOM-killer and terminates the process with the highest `oom_score` inside that cgroup. For stateless services (nginx, Redis with `maxmemory` set, Prometheus), this is usually survivable. For PostgreSQL or any workload with a write-ahead log, it can corrupt data mid-write.

The practical rule: set `--memory` at least 20-30% above your measured working set, and monitor `memory.current` before tightening:

```bash
# Current RSS for container 106, human-readable
cat /sys/fs/cgroup/lxc/106/memory.current | numfmt --to=iec
# Example output: 1.8G
```

### Soft Memory Pressure with memory.low

Proxmox doesn't expose a soft memory limit in the UI, but cgroup v2's `memory.low` knob is available directly. Writing to it tells the kernel to evict other containers' pages before touching this container's working set under host memory pressure:

```bash
# Protect container 105's working set below 1.5 GB from host reclaim
echo $((1536 * 1024 * 1024)) > /sys/fs/cgroup/lxc/105/memory.low
```

This is a hint, not a guarantee — but it meaningfully improves behavior when you're running a mix of critical services and background batch jobs on the same host.

## Disk I/O Throttling: The Feature the Proxmox UI Skips

Per-container I/O throttling is absent from the Proxmox VE 9.1 web interface, but cgroup v2's `io.max` interface is fully functional. Without it, a container running a bulk `rsync` or a backup agent can saturate your storage bus and cause latency spikes in every other container and VM — the kind of thing that's hard to diagnose after the fact.

```bash
# Find the major:minor device number of your storage pool's block device
lsblk -no MAJ:MIN /dev/nvme0n1
# Example output: 259:0

# Cap container 101 to 50 MB/s read, 30 MB/s write, 3000 read IOPS, 1000 write IOPS
echo "259:0 rbps=52428800 wbps=31457280 riops=3000 wiops=1000" \
    > /sys/fs/cgroup/lxc/101/io.max
```

Direct cgroup writes vanish on container restart. Persist them with a hook script:

```bash
# Add this line to /etc/pve/lxc/101.conf
hookscript: local:snippets/iolimit-101.sh
```

```bash
#!/bin/bash
# /var/lib/vz/snippets/iolimit-101.sh
CTID=$1
PHASE=$2

if [ "$PHASE" = "post-start" ]; then
    sleep 1  # cgroup needs a moment to initialize
    DEVNO=$(lsblk -no MAJ:MIN /dev/pve/vm-${CTID}-disk-0 2>/dev/null || echo "259:0")
    echo "${DEVNO} rbps=52428800 wbps=31457280 riops=3000 wiops=1000" \
        > /sys/fs/cgroup/lxc/${CTID}/io.max
fi
```

```bash
chmod +x /var/lib/vz/snippets/iolimit-101.sh
```

**Important gotcha**: `io.max` applies to all block I/O the container generates — including reads and writes that go through bind mounts from the host filesystem. If you're running [Docker inside an LXC container on Proxmox](/articles/docker-inside-lxc-containers-proxmox/), all Docker layer pulls and container writes count against the same limit. A 30 MB/s write cap will throttle your Docker image builds too. Size your limits with that in mind.

**Older tutorials use the wrong paths**: cgroups v1 used `blkio.throttle.read_bps_device` and `blkio.throttle.write_bps_device`. Those paths don't exist on Proxmox VE 9.x. If a guide shows those paths, it was written for Proxmox VE 7 or older.

## Monitoring Real-Time Resource Usage

Before setting any limits, spend a few minutes under real workload watching actual consumption. The Proxmox web UI averages over 60-second windows and will miss short bursts entirely.

```bash
# Real-time stats for container 101, refreshes every second
pct monitor 101
```

For a host-wide snapshot of all running containers:

```bash
for CTID in $(pct list | awk 'NR>1 && $2=="running" {print $1}'); do
    echo "=== CT ${CTID} ==="
    printf "  CPU throttled_usec: "
    awk '/throttled_usec/ {print $2}' /sys/fs/cgroup/lxc/${CTID}/cpu.stat 2>/dev/null
    printf "  Memory current: "
    cat /sys/fs/cgroup/lxc/${CTID}/memory.current 2>/dev/null | numfmt --to=iec
done
```

For I/O accounting, `io.stat` gives cumulative bytes and operations per device:

```bash
cat /sys/fs/cgroup/lxc/101/io.stat
# 259:0 rbytes=2048000000 wbytes=512000000 rios=150000 wios=40000 ...
```

A non-zero and growing `throttled_usec` in `cpu.stat` confirms a CPU limit is actively being enforced. If you're not seeing throttling but the container still feels slow, the bottleneck is elsewhere — check I/O wait with `iostat -x 1` on the host.

## Applying Limits in Bulk via the Proxmox API

Managing limits one container at a time with `pct set` is fine for a handful of containers. For a homelab with a dozen LXCs, or a production cluster with many more, `pvesh` handles it cleanly:

```bash
# Apply a "low-priority background" profile to containers 200 through 209
for CTID in $(seq 200 209); do
    pvesh set /nodes/pve/lxc/${CTID}/config \
        --cpulimit 0.5 \
        --cpuunits 256 \
        --memory 512 \
        --swap 256
done
```

`pvesh` takes the same parameters as `pct set` and works identically — the difference is that `pvesh` talks to the Proxmox REST API directly, so you can run it remotely or embed it in CI pipelines. If your infrastructure is already managed as code with [Ansible playbooks for Proxmox](/articles/automate-proxmox-ansible-vm-playbooks/), the `community.general.proxmox` module accepts `cpus`, `cpuunits`, `memory`, and `swap` as task parameters — same idempotent workflow, no custom scripting required.

## Common Pitfalls When Setting LXC Resource Limits

**`--cpulimit` throttles even on an idle host.** Once set, the kernel enforces the CPU cap regardless of whether other containers are competing. If you have a weekly report generator that needs to run fast, use `--cpuunits` to raise its priority instead — that only activates under contention, not when the host is idle.

**Swap on ZFS volumes doubles ARC pressure.** If your LXC root disk is a zvol on a ZFS pool, container swap I/O goes through ZFS, which creates its own ARC churn. For ZFS-backed containers, `--swap 0` is almost always correct — compensate with sufficient `--memory` instead.

**Limits don't cover in-kernel work done on behalf of the container.** If your container generates heavy NFS traffic or triggers ZFS prefetch, those kernel threads run outside the container's cgroup. You can see a container's CPU limit enforced at 1.0 while the host shows high `%sys` from ksoftirqd or nfsd. This is expected kernel behavior with no simple workaround — it's a characteristic of the container model, not a bug in your configuration.

**Resource limits are also a security boundary.** An unprivileged LXC with no CPU or memory cap can run a trivial fork bomb and degrade every other tenant on the host. Setting conservative defaults — even for containers you trust — is a meaningful layer of defense that works alongside the network and access controls covered in [Hardening Proxmox VE: Firewall, fail2ban, and SSH Security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/). This is worth it any time you run more than two or three containers on a host.

## Recommended Baseline Settings by Workload

Start from these values and adjust after a week of `pct monitor` observation:

| Workload | `--cpulimit` | `--cpuunits` | `--memory` | `--swap` |
|---|---|---|---|---|
| Web server (nginx/Caddy) | 1.0 | 1024 | 512 MB | 256 MB |
| Database (Postgres/MariaDB) | 2.0 | 2048 | 2048 MB | 0 |
| Monitoring (Prometheus) | 1.0 | 768 | 1024 MB | 512 MB |
| Media server (Jellyfin) | 4.0 | 512 | 2048 MB | 1024 MB |
| Backup agent (restic/borgmatic) | 0.5 | 128 | 256 MB | 256 MB |
| Dev/test (low priority) | 0.5 | 256 | 512 MB | 512 MB |

The `--cpuunits` values are relative — only their ratios matter. A container at 2048 gets twice the scheduler slices of one at 1024 during CPU contention. On an idle host, both run unrestricted regardless of their `--cpuunits` value.

The backup agent row deserves special attention: backup containers are the most common culprit for host-wide slowdowns. Capping a restic or borgmatic container to 0.5 cores and slow I/O means [your automated Proxmox Backup Server jobs](/articles/automated-backups-proxmox-backup-server/) finish a bit later — but your production containers stay responsive throughout the backup window.

## Conclusion

With CPU limits, memory ceilings, and I/O throttling in place, LXC containers become proper tenants on your Proxmox host rather than free-range processes competing for the same resources. The combination of `pct set` for persistent CPU and memory configuration and hook scripts for I/O throttling covers everything the web UI doesn't expose — and most of it applies live without touching a running workload. The logical next step is measuring the impact over time: wire up per-container cgroup metrics in your monitoring stack so you can see when a container is chronically hitting its CPU quota and needs its limit raised, or when it's consistently well under and the headroom can be reclaimed.

]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="lxc"/>
        <category label="cgroups"/>
        <category label="resource-limits"/>
        <category label="proxmox"/>
        <category label="performance"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automate Proxmox VE with Ansible Full VM Playbooks]]></title>
        <id>https://proxmoxpulse.com/articles/automate-proxmox-ansible-vm-playbooks/</id>
        <link href="https://proxmoxpulse.com/articles/automate-proxmox-ansible-vm-playbooks/"/>
        <updated>2026-05-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Provision Proxmox VMs and LXC containers with Ansible using community.general and API tokens. Get repeatable, zero-touch VM deployments in under 90 seconds.]]></summary>
        <content type="html"><![CDATA[
Ansible turns a Proxmox node — or a full three-node cluster — into reproducible, version-controlled infrastructure. By the end of this guide you'll have working playbooks that provision VMs and LXC containers on Proxmox VE 9.x using the `community.general` Ansible collection, a node-level configuration play you can run on every new host, and a structure you can commit to git and replay after a disaster.

## Key Takeaways

- **Collection to use**: `community.general` 8.x ships `proxmox_kvm` and `proxmox` modules covering full VM and LXC lifecycle
- **Auth method**: API tokens (not root password) are the correct approach — scoped, revocable, and logged in the Proxmox audit trail
- **Idempotency**: Both modules are idempotent; re-running a playbook will not clone duplicate VMs
- **Cloud-Init VMs**: Combine `proxmox_kvm` with a Cloud-Init template for zero-touch VM deployment in under 90 seconds on NVMe storage
- **Node config**: A separate OS-level play handles repo configuration, user accounts, and sysctl tuning on every new host automatically

## Why Ansible Beats Clicking Through the Proxmox UI

The Proxmox web UI is excellent for one-off tasks — but the moment you're standing up a third K3s node or rebuilding a host after drive failure, manual UI work becomes a liability. You miss a CPU setting, forget to enable the QEMU guest agent, or pick the wrong storage pool. Ansible makes that impossible by turning your infrastructure into a YAML file you commit to git.

The practical payoff: I rebuilt an entire three-node homelab from scratch in about 20 minutes after a botched ZFS experiment, running a single `ansible-playbook site.yml`. Every VM came up with the right CPU topology, the right network bridge, and Cloud-Init pre-populated with my SSH keys.

That said, Ansible for Proxmox is not Terraform. It doesn't maintain remote state, so if you delete a VM manually and re-run the playbook, Ansible will try to create it again. For homelab scale this is fine. For production, combining Ansible with Terraform's Proxmox provider gives you both declarative provisioning and state tracking.

## Prerequisites: Modules, API Tokens, and Inventory Setup

### Installing the community.general Collection

```bash
ansible-galaxy collection install community.general
```

You need community.general 8.0.0 or later for the `proxmox_kvm` and `proxmox` (LXC) modules. Check your installed version:

```bash
ansible-galaxy collection list | grep community.general
```

Install the Python dependency on the machine running Ansible — not the Proxmox host itself:

```bash
pip install proxmoxer requests
```

### Creating a Proxmox API Token

Never store your root password in a playbook. Create a dedicated API token instead:

```bash
pveum user add ansible@pve --comment "Ansible automation"
pveum role add AnsibleRole --privs "Datastore.AllocateSpace,Datastore.Audit,Pool.Allocate,Sys.Audit,Sys.Console,Sys.Modify,VM.Allocate,VM.Audit,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Migrate,VM.Monitor,VM.PowerMgmt,SDN.Use"
pveum aclmod / -user ansible@pve -role AnsibleRole
pveum user token add ansible@pve automation --privsep 0
```

That last command outputs the token secret — copy it now, you won't see it again. Store it in Ansible Vault immediately:

```bash
ansible-vault create group_vars/all/vault.yml
```

```yaml
# vault.yml (encrypted at rest)
vault_proxmox_token_id: "ansible@pve!automation"
vault_proxmox_token_secret: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```

### Inventory Structure

A minimal inventory for a single-node setup:

```ini
[proxmox]
pve01 ansible_host=192.168.1.10

[proxmox:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_ed25519
```

For the API-based Proxmox modules, `ansible_host` is used as `api_host`. Ansible calls the Proxmox REST API from your workstation — it does not SSH into the node to run `proxmox_kvm` tasks. SSH is only used for OS-level configuration plays.

## How to Provision VMs with the proxmox_kvm Module

This playbook creates a Ubuntu 24.04 VM from a Cloud-Init template. It assumes you have a Cloud-Init-ready template at VMID 9000 — [setting up that base template is part of building a full private cloud on Proxmox](/articles/build-private-cloud-home-proxmox-ve/).

```yaml
# playbooks/create_vm.yml
---
- name: Provision Ubuntu VM on Proxmox
  hosts: localhost
  gather_facts: false
  vars:
    api_host: "192.168.1.10"
    api_token_id: "{{ vault_proxmox_token_id }}"
    api_token_secret: "{{ vault_proxmox_token_secret }}"
    node: "pve01"
    template_vmid: 9000
    new_vmid: 101
    vm_name: "ubuntu-worker-01"
    vm_memory: 4096
    vm_cores: 2
    storage: "local-lvm"
    ipconfig: "ip=192.168.1.101/24,gw=192.168.1.1"
    ssh_keys: "ssh-ed25519 AAAAC3Nz your@key"

  tasks:
    - name: Clone VM from Cloud-Init template
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        name: "{{ vm_name }}"
        vmid: "{{ new_vmid }}"
        clone: "{{ template_vmid }}"
        full: true
        storage: "{{ storage }}"
        timeout: 300
        state: present

    - name: Configure VM hardware and Cloud-Init
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        vmid: "{{ new_vmid }}"
        memory: "{{ vm_memory }}"
        cores: "{{ vm_cores }}"
        ipconfig:
          ipconfig0: "{{ ipconfig }}"
        sshkeys: "{{ ssh_keys }}"
        ciuser: "ubuntu"
        update: true

    - name: Start VM
      community.general.proxmox_kvm:
        api_host: "{{ api_host }}"
        api_token_id: "{{ api_token_id }}"
        api_token_secret: "{{ api_token_secret }}"
        node: "{{ node }}"
        vmid: "{{ new_vmid }}"
        state: started
```

Run it:

```bash
ansible-playbook playbooks/create_vm.yml --vault-password-file ~/.vault_pass
```

On NVMe-to-NVMe (local-lvm to local-lvm on the same node), a full 32 GB clone completes in under 90 seconds. On HDD-backed storage, plan for 3-5 minutes and set `timeout` accordingly.

### Deploying Multiple VMs with a Loop

The real payoff comes when you define your fleet in a variables file and loop over it:

```yaml
# vars/vms.yml
vms:
  - name: k3s-master-01
    vmid: 101
    ip: "192.168.1.101"
    memory: 4096
    cores: 2
  - name: k3s-worker-01
    vmid: 102
    ip: "192.168.1.102"
    memory: 8192
    cores: 4
  - name: k3s-worker-02
    vmid: 103
    ip: "192.168.1.103"
    memory: 8192
    cores: 4
```

```yaml
- name: Clone and configure all VMs
  community.general.proxmox_kvm:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    name: "{{ item.name }}"
    vmid: "{{ item.vmid }}"
    clone: "{{ template_vmid }}"
    full: true
    storage: "{{ storage }}"
    memory: "{{ item.memory }}"
    cores: "{{ item.cores }}"
    ipconfig:
      ipconfig0: "ip={{ item.ip }}/24,gw=192.168.1.1"
    sshkeys: "{{ ssh_keys }}"
    ciuser: ubuntu
    state: present
    timeout: 300
  loop: "{{ vms }}"
```

Three K3s nodes provisioned with one loop. Once they're up, [the K3s Kubernetes cluster setup guide on Proxmox](/articles/k3s-kubernetes-cluster-proxmox-vms/) picks up exactly where this leaves off.

## Provisioning LXC Containers with the proxmox Module

The `community.general.proxmox` module handles LXC lifecycle. The interface differs from `proxmox_kvm` — you specify a template from a storage pool rather than cloning a VMID.

First, download the template on the Proxmox node:

```bash
pveam update
pveam available | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
```

Then the Ansible tasks:

```yaml
- name: Create Debian 12 LXC container
  community.general.proxmox:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    vmid: 200
    hostname: "monitoring-01"
    ostemplate: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
    storage: local-lvm
    disk: 8
    memory: 1024
    swap: 512
    cores: 2
    netif:
      net0: "name=eth0,bridge=vmbr0,ip=192.168.1.200/24,gw=192.168.1.1"
    password: "{{ vault_lxc_root_password }}"
    pubkey: "{{ ssh_keys }}"
    unprivileged: true
    features:
      - nesting=1
    state: present

- name: Start LXC container
  community.general.proxmox:
    api_host: "{{ api_host }}"
    api_token_id: "{{ api_token_id }}"
    api_token_secret: "{{ api_token_secret }}"
    node: "{{ node }}"
    vmid: 200
    state: started
```

Setting `unprivileged: true` and `nesting=1` is the right default for containers that will run Docker — [running Docker inside LXC containers on Proxmox](/articles/docker-inside-lxc-containers-proxmox/) covers the additional `lxc.apparmor.profile` and keyctl settings you'll apply after the container first starts.

**Gotcha**: If your Proxmox node uses a self-signed certificate, `proxmoxer` will refuse the API connection with a verification error. Install a proper TLS certificate or pass `validate_certs: false` in each module call for automation running entirely on a trusted internal network.

## Automating Node-Level Configuration Over SSH

Ansible shines at the OS layer too. This play handles the common Proxmox post-install tasks every node needs:

```yaml
# playbooks/configure_node.yml
---
- name: Configure Proxmox node base settings
  hosts: proxmox
  become: false

  tasks:
    - name: Disable enterprise repo
      ansible.builtin.copy:
        dest: /etc/apt/sources.list.d/pve-enterprise.list
        content: |
          # deb https://enterprise.proxmox.com/debian/pve bookworm pve-enterprise

    - name: Add no-subscription repo
      ansible.builtin.apt_repository:
        repo: "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription"
        state: present
        filename: pve-no-subscription

    - name: Update all packages
      ansible.builtin.apt:
        update_cache: true
        upgrade: dist

    - name: Set swappiness for VM host
      ansible.posix.sysctl:
        name: vm.swappiness
        value: "10"
        sysctl_file: /etc/sysctl.d/99-proxmox.conf
        reload: true

    - name: Install QEMU guest agent
      ansible.builtin.apt:
        name: qemu-guest-agent
        state: present
```

For SSH hardening, fail2ban, and Proxmox firewall rules, keep a separate `harden_node.yml`. Running it via Ansible means every new node gets an identical security baseline automatically — exactly the defense-in-depth approach that [hardening Proxmox with firewall, fail2ban, and SSH config](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) describes.

## Structuring a site.yml That Ties Everything Together

Once you have individual playbooks, a top-level `site.yml` composes them in the correct order:

```yaml
# site.yml
---
- import_playbook: playbooks/configure_node.yml
- import_playbook: playbooks/harden_node.yml
- import_playbook: playbooks/create_vms.yml
- import_playbook: playbooks/create_lxcs.yml
```

Run order matters. Configure and harden the node before creating workloads. If the node play reconfigures network bridges, a VM created before that step completes will start with no network interface.

```bash
# Dry run with diff output first
ansible-playbook site.yml --check --diff --vault-password-file ~/.vault_pass

# Apply
ansible-playbook site.yml --vault-password-file ~/.vault_pass
```

## Common Pitfalls to Avoid

**VMID conflicts**: If a playbook targets a VMID already in use, `proxmox_kvm` fails with a confusing API error rather than a clear message. Check `pvesh get /nodes/pve01/qemu` before assigning VMIDs in automation, or reserve a dedicated range above 200 exclusively for Ansible-managed workloads.

**Clone timeout on slow storage**: The default `timeout` for `proxmox_kvm` is 30 seconds. A full clone to HDD-backed storage will time out and leave a partial VM. Set `timeout: 300` as a minimum — even NVMe-to-NVMe can push past 90 seconds for a 100 GB disk.

**SSH host key collisions**: When a VM is rebuilt with the same IP, Ansible will refuse SSH because the host key changed. Add this to `ansible.cfg`:

```ini
[defaults]
host_key_checking = False
```

Or use the `known_hosts` module to explicitly clear stale entries before connecting.

**API privilege scope**: The role above is broad — intentionally so for a homelab. For production, the minimum set for VM and LXC management is: `VM.Allocate`, `VM.Config.CPU`, `VM.Config.Memory`, `VM.Config.Disk`, `VM.Config.Network`, `VM.Config.Cloudinit`, `VM.PowerMgmt`, `Datastore.AllocateSpace`, `Datastore.Audit`, `SDN.Use`.

**Proxmox VE 9.1 and community.general 9.0**: The `proxmox_kvm` module gained `scsi_discard` support and improved Cloud-Init disk handling in community.general 9.0. If you're on Proxmox VE 9.1 and see unexpected disk configuration behavior, upgrade the collection before debugging your playbook.

## Conclusion

With these playbooks committed to git, standing up a new VM or LXC container on Proxmox takes one command and under two minutes — no UI clicks, no config drift, no forgotten settings. The logical next step is adding VLAN and bridge configuration to your node-level play (Proxmox's `ifupdown2` config in `/etc/network/interfaces` maps cleanly to Ansible's `template` module), then tagging your VMs with Proxmox pools so you can filter by environment in the dashboard. From there, your infrastructure is a pull request.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="ansible"/>
        <category label="automation"/>
        <category label="infrastructure-as-code"/>
        <category label="vm-management"/>
        <category label="proxmox"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox Backup Server 4.2 S3 Storage Backend Setup]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-backup-server-s3-storage-backend/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-backup-server-s3-storage-backend/"/>
        <updated>2026-05-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Configure PBS 4.2's S3 storage backend to sync backups to Backblaze B2, Wasabi, or Cloudflare R2. Covers sync jobs, retention propagation, encryption, and cost per TB.]]></summary>
        <content type="html"><![CDATA[
Proxmox Backup Server 4.2 adds native S3-compatible object storage as a remote sync target, eliminating the need for a second PBS instance just to get backups off-site. Configure a remote once, point it at Backblaze B2, Wasabi, Cloudflare R2, or your own MinIO, and PBS handles chunk-level sync with full deduplication awareness. By the end of this guide you will have your backups replicating to object storage on a schedule with retention enforced at the S3 end, no extra hardware required.

## Key Takeaways

- **New remote type**: PBS 4.2 introduces a native `S3` backend under Remotes — no relay server needed
- **Chunk-aware sync**: Only new 4 MB chunks transfer after the first run; unchanged data never re-uploads
- **Any S3-compatible endpoint**: Backblaze B2, Wasabi, Cloudflare R2, MinIO, and AWS S3 all work with the same config
- **Storage overhead**: Budget 15-25% more S3 usage than your local datastore due to manifests and index metadata
- **Cost**: 1 TB of offsite retention runs $6-7/month on B2 or Wasabi; R2 charges zero egress fees

## S3 Sync vs Running a Second PBS Instance

Before PBS 4.2, the standard offsite approach was pulling backups to a second PBS node via the built-in replication protocol. That works well — but it means a second machine, potentially a second enterprise subscription, and another piece of infrastructure to patch and monitor.

S3 sync trades hardware cost for a monthly per-GB fee. Whether that is the right tradeoff depends on your datastore size:

| | Second PBS Node | S3 Sync (PBS 4.2) |
|---|---|---|
| Hardware cost | $150–500+ one-time | $0 |
| Monthly operating cost | Electricity (~$10–15) | $6–7/TB |
| Restore speed | Full PBS API, fast | Pull chunks from S3 first |
| Deduplication awareness | Full (native) | Chunk-level (native in 4.2) |
| Disaster recovery | Needs second machine up | S3 is always available |
| Break-even point | ~2–3 TB stored | Under 2 TB stored |

For homelab setups under 2 TB — and any small-business scenario where the second PBS machine sits idle most of the time — S3 sync is cheaper and simpler. Above 2 TB, the monthly S3 cost starts approaching the electricity cost of a dedicated machine.

If you are setting up PBS for the first time, the [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) guide covers the datastore and backup job fundamentals before you layer on S3 sync.

## Prerequisites

You will need:

- **PBS 4.2 or later** — run `proxmox-backup-manager version` to check; if on 4.1 or earlier, upgrade first:

```bash
apt update && apt full-upgrade
```

- An account with Backblaze B2, Wasabi, Cloudflare R2, MinIO, or AWS S3
- A bucket created in your chosen provider (covered below)
- Network access from your PBS host to the S3 endpoint — no NAT hairpins, no intercepting proxies without `HTTP_PROXY` configured

PBS 4.2 requires Debian 12 Bookworm as the base OS. If your PBS runs on Bullseye, the upgrade path requires a full OS upgrade before you can reach PBS 4.2.

## Creating a Bucket and Access Keys

### Backblaze B2

B2 is the most common homelab choice at $6/TB/month with no minimum storage term and free egress up to 3x your stored data per day.

1. Log into your B2 account and go to **Buckets → Create a Bucket**
2. Name the bucket (e.g., `pbs-offsite-2026`) — this string appears in your endpoint URL
3. Set **Files in Bucket** to **Private**
4. Go to **App Keys → Add a New Application Key**
5. Scope it to your bucket, enable **Read and Write**, and save the `keyID` and `applicationKey`

B2's S3-compatible endpoint format — find your exact region on the bucket detail page:

```
https://s3.us-west-004.backblazeb2.com
```

### Cloudflare R2

R2 charges zero egress fees, making it the right pick if you do frequent restores from S3. The first 10 GB of storage per month is free.

1. In the Cloudflare dashboard go to **R2 → Create bucket**
2. Choose a location hint near your PBS host
3. Go to **R2 → Manage R2 API Tokens → Create API Token**, grant Object Read and Write scoped to your bucket
4. Note your **Account ID** from the R2 overview page

R2 endpoint format — substitute your 32-character account ID:

```
https://<ACCOUNT_ID>.r2.cloudflarestorage.com
```

### Wasabi

Wasabi matches B2 at $6.99/TB/month but enforces a **90-day minimum storage policy**. Deleting objects stored less than 90 days still bills for the full 90 days. Do not use Wasabi with prune schedules shorter than 90 days or you will pay for data you no longer hold.

Wasabi regional endpoint format:

```
https://s3.us-east-1.wasabisys.com
```

## How to Add an S3 Remote in PBS 4.2

### Via the Web UI

1. Open the PBS web UI at `https://<pbs-ip>:8007`
2. Navigate to **Configuration → Remotes**
3. Click **Add** and select **Type: S3**
4. Fill in the fields:
   - **ID**: a short name (e.g., `b2-offsite`)
   - **Endpoint**: your full S3 endpoint URL
   - **Bucket**: your bucket name
   - **Region**: the bucket's region string
   - **Access Key**: your `keyID`
   - **Secret Key**: your `applicationKey`
5. Click **Test Connection** — PBS runs a list operation against the bucket and surfaces auth errors immediately before you save

### Via CLI

```bash
proxmox-backup-manager remote create b2-offsite \
  --type s3 \
  --endpoint "https://s3.us-west-004.backblazeb2.com" \
  --bucket "pbs-offsite-2026" \
  --region "us-west-004" \
  --access-key "your-keyID-here" \
  --secret-key "your-applicationKey-here"
```

Verify the remote saved correctly:

```bash
proxmox-backup-manager remote list
```

Expected output:

```
┌─────────────┬──────┬───────────────────────────────────────────────┐
│ name        │ type │ endpoint                                      │
╞═════════════╪══════╪═══════════════════════════════════════════════╡
│ b2-offsite  │ s3   │ https://s3.us-west-004.backblazeb2.com        │
└─────────────┴──────┴───────────────────────────────────────────────┘
```

## Setting Up Sync Jobs and Retention

### Creating the Sync Job

Via the web UI:

1. Go to **Datastore → \<your-datastore\> → Sync Jobs**
2. Click **Add Sync Job**
3. Set **Remote** to your S3 remote
4. **Remote Store**: the namespace path to use in the S3 bucket — use your datastore name (e.g., `backups`)
5. **Schedule**: `daily` or a cron expression like `0 2 * * *` for 2 AM daily
6. **Remove Vanished**: enable this — it deletes S3 objects for snapshots that have been pruned locally

Via CLI:

```bash
proxmox-backup-manager sync-job create daily-s3-sync \
  --store backups \
  --remote b2-offsite \
  --remote-store backups \
  --schedule "0 2 * * *" \
  --remove-vanished true
```

Trigger the first sync manually before relying on the schedule:

```bash
proxmox-backup-manager sync-job run daily-s3-sync
```

Watch progress under **Administration → Task History**. A 500 GB datastore over a 100 Mbit uplink expects the initial upload to complete in 2–4 hours. Incremental syncs after that transfer only new chunks — typically 2–8 GB/day for a homelab with 3–4 VMs running daily backups.

### How Retention Propagates to S3

PBS does not enforce retention directly on S3. Pruning happens locally first, then the `remove-vanished` flag propagates deletions on the next sync cycle. The sequence:

1. Local prune job runs and removes old snapshots from the local datastore
2. Next sync job runs with `remove-vanished: true`
3. PBS compares the S3 object list against local state and deletes orphaned chunk files

Your S3 bucket will lag local retention by up to one sync cycle — 24 hours at a daily schedule. On Wasabi specifically: a snapshot pruned at day 89 still triggers the full 90-day minimum charge because Wasabi sees an early deletion. Set your Wasabi prune schedule to keep at least 90 days minimum.

## What PBS Actually Uploads to S3

This is where the most common misconception lives. PBS stores backups as content-addressed 4 MB chunk files. When syncing to S3, it uploads:

- **Chunk files** (`.blob`) — deduplicated backup data, already compressed
- **Snapshot manifests** (`.manifest`) — links each snapshot to its chunk list
- **Index files** (`.fidx`, `.didx`) — mapping tables for file-level and block-device backups

It does **not** sync the task log or the searchable catalog. This means you can restore data from S3 on a fresh PBS instance by registering the S3 remote as a source datastore, but you will need to rebuild the catalog afterward:

```bash
proxmox-backup-client catalog rebuild --repository <user>@<pbs-host>:backups
```

The 15–25% storage overhead estimate comes from these metadata files. A local datastore holding 800 GB of deduplicated backup data will occupy roughly 920 GB to 1 TB on S3.

## Provider Cost Comparison

| Provider | Storage/TB/month | Egress | Min. term | Best for |
|---|---|---|---|---|
| Backblaze B2 | $6.00 | Free (3x stored/day) | None | Low-restore-frequency homelabs |
| Wasabi | $6.99 | Free | 90 days | Long-retention cold storage |
| Cloudflare R2 | $15.00 | Free | None | Frequent restores, zero surprise bills |
| MinIO (self-hosted) | Hardware only | Free | None | Air-gapped or LAN backup copies |
| AWS S3 Standard | $23.00 | $0.09/TB | None | Avoid for homelab-scale volumes |

For a homelab with 2 TB stored, daily incrementals, and 30-day retention: B2 costs ~$12/month, Wasabi ~$14/month, R2 ~$30/month. R2's zero-egress advantage only makes financial sense if you are pulling hundreds of GB in restores per month. Most homelabs run few enough restores that B2 wins on total cost.

## Securing Credentials and Encrypting the Datastore

PBS stores S3 credentials in `/etc/proxmox-backup/remotes.cfg`, readable only by root. For most setups that is sufficient. If your PBS runs as a VM on a shared Proxmox host with other administrators, enabling datastore-level encryption means data pushed to S3 is client-side encrypted before it leaves your network — your S3 provider stores only ciphertext.

Enable encryption in PBS: **Datastore → \<name\> → Encryption → Generate Key**. Store the key file somewhere other than the PBS host itself — a password manager or a dedicated secrets store. Lose the key and your S3 backups become permanently unrecoverable, so treat it with the same discipline as your SSH private keys.

The [Build a Private Cloud at Home with Proxmox VE](/articles/build-private-cloud-home-proxmox-ve/) guide covers the broader architecture behind layered backup strategies on Proxmox, including where PBS fits alongside Proxmox's native VM snapshot tooling. For the host-level hardening that protects PBS credentials in the first place, the [Hardening Proxmox VE: Firewall, fail2ban, and SSH Security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) guide is the companion read.

## Troubleshooting Common Sync Errors

**`AuthorizationHeaderMalformed`**
The region string does not match what the provider expects. B2 regions look like `us-west-004`, not `us-west-1`. Copy the exact region string directly from the bucket detail page in the B2 console.

**`403 Forbidden` on test connection**
Your API key lacks list permission on the bucket. For B2, verify the application key includes `listBuckets`, `readFiles`, and `writeFiles` capabilities. For R2, confirm the API token is scoped to the correct bucket.

**Sync shows 0 bytes transferred**
Normal if no new backups have been created since the last sync. PBS only transfers chunks absent from the remote. Confirm backups are running:

```bash
proxmox-backup-client snapshots --repository <user>@<pbs-host>:backups
```

**Initial sync is stuck at very low throughput**
Check for a rate limit under **Configuration → Bandwidth Limits** in the PBS web UI. Also check whether B2's free API tier cap (2,500 Class B operations per day) has been hit — the client applies exponential backoff when rate-limited. For large datastores, upgrade to a paid B2 API tier or spread the initial sync over several days using bandwidth throttling.

## Conclusion

PBS 4.2's S3 backend turns any S3-compatible bucket into a fault-tolerant off-site backup copy for $6–7/month per TB with no additional hardware. Set up the remote, create a daily sync job with `remove-vanished` enabled, and your local prune policy propagates to S3 automatically. The step most people skip: do a test restore from S3 before you need it — register your S3 bucket as a source on a temporary PBS instance or spare VM, browse the snapshots, and pull one back. That is the only confirmation that your offsite copy actually works.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="proxmox-backup-server"/>
        <category label="s3"/>
        <category label="backup"/>
        <category label="object-storage"/>
        <category label="offsite-backup"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox VE for Small Business: A Free VMware Alternative]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-ve-small-business-vmware-alternative/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-ve-small-business-vmware-alternative/"/>
        <updated>2026-05-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Proxmox VE 9.1 replaces VMware vSphere for small business at zero cost. Compare features, configure HA, RBAC, and backups with step-by-step commands.]]></summary>
        <content type="html"><![CDATA[
If your small business runs on VMware vSphere and you're watching the renewal invoice climb past what the infrastructure actually costs to run, Proxmox VE 9.1 is a credible path out. This guide maps the VMware features your ops team depends on to their Proxmox equivalents, then walks through the four configuration areas that matter most in a production environment: role-based access control, high availability, backups, and network segmentation. By the end, you'll know exactly what Proxmox delivers — and the two real gaps you need to plan around.

## Key Takeaways

- **Zero licensing cost**: Proxmox VE is free (AGPL-3.0); the optional enterprise subscription adds stable repos and Bugzilla support, not features.
- **HA is built in**: Proxmox HA uses Corosync + fencing on commodity hardware — three nodes is the minimum for a stable quorum.
- **Feature parity**: vMotion maps to live migration, vCenter maps to the PVE web UI, vSAN maps to Ceph, and vDS port groups map to VLAN-aware bridges or SDN VNets.
- **Backup is first-party**: Proxmox Backup Server handles incremental, deduplicated backups without a third-party tool.
- **Real gap**: There is no equivalent to VMware's Distributed Resource Scheduler (DRS) — load balancing is manual.

## Why Small Businesses Are Leaving VMware

Broadcom's February 2024 licensing overhaul eliminated perpetual vSphere licenses and moved everything to subscription bundles. The smallest tier — vSphere Foundation — starts at approximately $250 per core per year, with a 16-core minimum per CPU. Two sockets on a single server means 32 licensed cores: that's $8,000 per host per year before support. A five-server cluster that ran on a one-time $15,000 perpetual purchase now costs $40,000+ annually.

Proxmox VE's pricing is the inverse. The software is free. The enterprise repository subscription — which gives access to the stable `pve-enterprise` apt repo and Proxmox's bug tracker — costs €134 per socket per year. Most small businesses either pay this for their production nodes, or run the `pve-no-subscription` repo and accept a slightly less conservative update cadence. Either way, the licensing argument is decisive.

## How Proxmox VE Compares to VMware vSphere Feature by Feature

This is the honest map, not the marketing version.

| VMware Feature | Proxmox Equivalent | Notes |
|---|---|---|
| ESXi hypervisor | KVM/QEMU (Proxmox VE 9.1) | Full parity for Linux and Windows guests |
| vCenter Server | Proxmox web UI + pvesh REST API | No Windows dependency |
| vMotion (live migration) | `qm migrate --online` | Works without shared storage via NBD |
| HA / FT | Proxmox HA Manager + Corosync | No Fault Tolerance (zero-downtime mirroring) |
| vDS / NSX-T | Linux bridges + SDN VNets | SDN needs Open vSwitch for advanced routing |
| vSAN | Ceph (built-in since PVE 5) | Requires 3+ nodes, 3+ OSDs per node |
| VMFS / NFS datastores | LVM-thin, ZFS, NFS, iSCSI, Ceph RBD | All first-class in the storage panel |
| vROps / DRS | No equivalent | Workload balancing is manual |
| RBAC | pveum + realm-based permissions | AD/LDAP integration included |
| VMware Tools | QEMU Guest Agent | Must be installed manually per guest |

The absence of DRS is the most meaningful gap for larger clusters. For 3-5 hosts with predictable workloads, manual migration is fine. For 15+ hosts with spiky load profiles, you will feel it.

## What You Need Before You Start

Hardware minimums for a production cluster:

- **Three physical servers** — Corosync quorum requires an odd number of votes; two-node clusters need an external quorum device and are fragile under any failure scenario.
- **Dedicated cluster network** — A separate 1 GbE NIC for Corosync heartbeats keeps cluster traffic off your VM network. Use 10 GbE if you're running Ceph.
- **Shared or replicated storage** — Ceph for high-availability storage across nodes, or ZFS replication paired with PBS for nodes with local NVMe.
- **IPMI or iDRAC access** — Proxmox HA fencing needs a way to power-cycle unresponsive nodes. Without it, HA will not restart VMs from a failed host.

If you're starting from scratch rather than migrating, [installing Proxmox VE on any hardware](/articles/install-proxmox-ve-on-any-hardware/) covers ISO prep, BIOS/UEFI settings, and whether to use ext4 or ZFS for the root disk.

## How to Set Up Role-Based Access Control

Proxmox ships with 15 built-in roles. For a small business, these four cover most scenarios:

| Role | What It Can Do |
|---|---|
| Administrator | Full cluster access |
| PVEVMAdmin | Create, configure, and delete VMs — no host management |
| PVEVMUser | Start, stop, and access VM consoles only |
| PVEAuditor | Read-only view of everything |

Create a group for VM operators, assign it a role scoped to `/vms`, and add users:

```bash
# Create the group
pveum group add vmops --comment "VM Operators"

# Grant PVEVMAdmin on all VMs
pveum acl modify /vms --group vmops --role PVEVMAdmin

# Create a local user and add to the group
pveum user add jsmith@pve --comment "Jane Smith"
pveum group member add vmops jsmith@pve
```

If your company has Active Directory, connect it as an authentication realm:

```bash
pveum realm add corp-ad \
  --type ad \
  --domain corp.local \
  --server 192.168.1.10 \
  --default 0 \
  --comment "Corporate Active Directory"
```

AD users log in as `username@corp-ad`. Assign them to the same groups with the same `pveum` commands — no separate role system to learn.

## How to Configure Proxmox High Availability

### Build the Cluster First

Create the cluster on your first node:

```bash
pvecm create corp-cluster
```

Join the remaining nodes (run on each additional server):

```bash
pvecm add 10.0.1.101
```

Verify cluster health before doing anything else:

```bash
pvecm status
```

You need to see `Quorate: Yes`. A non-quorate cluster will not execute HA operations — adding VMs to HA on a broken cluster creates confusion, not safety.

### Configure Fencing

Fencing is non-negotiable for HA. Without it, Proxmox will not restart a VM from a node it can't reach — correctly, because that VM might still be running and a second start would corrupt shared storage. For servers with IPMI or iDRAC:

```bash
pvesh set /nodes/pve2/config \
  --ipmi_address 10.0.1.212 \
  --ipmi_user admin \
  --ipmi_password 'fencepassword'
```

For servers without IPMI, use the kernel watchdog. Add this line to `/etc/pve/datacenter.cfg`:

```ini
fencing: watchdog-mux
```

### Add VMs to HA

Create an HA group defining which nodes can run the workload:

```bash
ha-manager groupadd prod-vms --nodes pve1,pve2,pve3 --restricted 0
```

Add a VM to HA management:

```bash
ha-manager add vm:100 \
  --group prod-vms \
  --max_restart 3 \
  --max_relocate 1 \
  --state started
```

With `max_restart 3` and `max_relocate 1`, Proxmox attempts three in-place restarts, then one migration to another node, then marks the service failed. Expect a 2-3 minute total fence-and-restart cycle for a 50 GB VM on shared NFS. Ceph with NVMe OSDs cuts this to under 90 seconds.

## Backup Strategy with Proxmox Backup Server

VMware's native backup story has always required third-party tools — Veeam, Nakivo, or Commvault. Proxmox Backup Server is a first-party solution with client-server deduplication that achieves 3:1 to 5:1 ratios on typical mixed-workload VMs. Run it on a dedicated machine or a separate VM on your cluster.

On the PBS machine, create a datastore:

```bash
proxmox-backup-manager datastore create corp-backups /mnt/backup-disk
```

Create a service account for Proxmox to authenticate against:

```bash
proxmox-backup-manager user create pvebackup@pbs --password 'StrongBackupPass'
proxmox-backup-manager acl update /datastore/corp-backups \
  --auth-id pvebackup@pbs \
  --role DatastoreBackup
```

Get the PBS server's TLS fingerprint — you'll need it when adding the storage in the Proxmox web UI:

```bash
proxmox-backup-manager fingerprint
```

In the Proxmox web UI, go to Datacenter → Storage → Add → Proxmox Backup Server. Provide the PBS IP, fingerprint, datastore name, and the `pvebackup@pbs` credentials.

Schedule nightly backups at 02:00 with 14-day retention:

```bash
pvesh create /cluster/backup \
  --vmid 100,101,102,103 \
  --storage pbs-corp \
  --schedule "0 2 * * *" \
  --maxfiles 14 \
  --compress zstd \
  --mode snapshot
```

For the full setup including verification jobs and retention policies, [automated backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) covers everything from initial PBS install through verifying backup integrity on a schedule.

## Network Segmentation for Department Isolation

Small business networks typically need at minimum four segments: management, production, backup traffic, and DMZ. The cleanest way to handle this in Proxmox is a VLAN-aware bridge on each host.

Edit `/etc/network/interfaces` on each node:

```bash
auto vmbr0
iface vmbr0 inet static
    address 10.0.1.101/24
    gateway 10.0.1.1
    bridge-ports eno1
    bridge-stp off
    bridge-fd 0
    bridge-vlan-aware yes
    bridge-vids 2-4094
```

Apply without rebooting:

```bash
ifreload -a
```

Assign VMs to their department VLANs:

```bash
# Finance on VLAN 20
qm set 101 --net0 virtio,bridge=vmbr0,tag=20

# Production app server on VLAN 30
qm set 102 --net0 virtio,bridge=vmbr0,tag=30

# DMZ on VLAN 40
qm set 103 --net0 virtio,bridge=vmbr0,tag=40
```

Your upstream switch must present the Proxmox-facing port as a trunk carrying all relevant VLANs. The complete bridge configuration — including trunk port setup and routing between segments via a firewall VM — is in [configuring VLANs on Proxmox with Linux bridges](/articles/configure-vlans-proxmox-linux-bridges/).

## Common Gotchas Before You Go Live

**QEMU Guest Agent is not installed automatically.** Without it, VM shutdowns from the UI rely on ACPI signals alone — expect 30-60 seconds of waiting, and snapshot quiescing will not work on running VMs.

```bash
# Debian / Ubuntu guests
apt install qemu-guest-agent
systemctl enable --now qemu-guest-agent
```

For Windows guests, install from the VirtIO ISO and select the Guest Agent component during setup.

**Windows 11 needs explicit EFI and TPM config.** VMware handles Secure Boot silently through vCenter policies. In Proxmox, you add the EFI disk and virtual TPM 2.0 manually:

```bash
qm set 110 \
  --bios ovmf \
  --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=1 \
  --tpmstate0 local-lvm:1,version=v2.0
```

Skip this and you'll hit a `Windows requires a TPM 2.0` block mid-installation.

**Two-node clusters need an external quorum device.** If you only have two servers at launch, add a quorum device on a third machine — a Raspberry Pi 4 works fine:

```bash
pvecm qdevice setup 10.0.1.250
```

Without it, losing one node takes down quorum and the surviving node fences itself.

**The default install leaves security gaps.** The Proxmox installer enables root login and exposes the web UI on port 8006 with no rate limiting. Before connecting to production networks, work through the [Proxmox firewall, fail2ban, and SSH hardening guide](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) to lock down admin access, configure two-factor authentication, and restrict API tokens to specific paths.

## When Proxmox Is Not the Right Answer

Be honest about these cases before committing:

- **You need VMware Fault Tolerance** — zero-RPO, sub-second mirrored failover. Proxmox HA has a 2-3 minute restart window. There is no FT equivalent.
- **You have VMware-certified enterprise apps** — some Oracle and SAP configurations have support contracts that specify VMware. Running on KVM may void those agreements.
- **Your team is VMware-certified and retraining costs are real** — the Proxmox CLI and permission model take about a week of hands-on time to internalize. For very small teams, that cost can flip the math.

Outside these specific constraints, Proxmox VE handles general-purpose production workloads cleanly and without ongoing licensing overhead.

## Conclusion

Proxmox VE 9.1 gives small businesses HA, RBAC, first-party backups, and VLAN segmentation at zero licensing cost — the migration effort is real, but the operational model is straightforward once you know the `pveum`, `pvecm`, and `ha-manager` tools. Plan a week of parallel testing before cutting over production workloads. If you're building the cluster fresh, start with [installing Proxmox VE on any hardware](/articles/install-proxmox-ve-on-any-hardware/), then return here to stand up the business-critical configuration.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="proxmox"/>
        <category label="vmware"/>
        <category label="high-availability"/>
        <category label="rbac"/>
        <category label="proxmox-backup-server"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox Open vSwitch Setup for Advanced VM Networking]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-open-vswitch-vm-networking/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-open-vswitch-vm-networking/"/>
        <updated>2026-04-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up Open vSwitch on Proxmox VE 9.1 for per-port VLAN access mode, port mirroring, and VXLAN overlay tunnels — step-by-step with real CLI commands.]]></summary>
        <content type="html"><![CDATA[
Open vSwitch (OVS) gives Proxmox three capabilities the default Linux bridge cannot match: per-port VLAN access mode, port mirroring to a dedicated monitoring VM, and VXLAN overlay tunnels for multi-node flat networks. By the end of this guide you will have a working OVS bridge on Proxmox VE 9.1 with at least one VM assigned to a VLAN access port — and a clear picture of exactly when the added complexity pays off versus when to stay with Linux bridges.

## Key Takeaways

- **OVS advantage**: Per-port VLAN assignment, port mirroring, and VXLAN tunnels that Linux bridges do not support natively.
- **Version**: Open vSwitch 3.3 ships in Proxmox VE 9.1's Debian 13 base — no third-party repo required.
- **Top gotcha**: Always open a serial console (IPMI/iDRAC/iLO) before editing `/etc/network/interfaces` on a live node — misconfiguration kills SSH access instantly.
- **SDN conflict**: Proxmox SDN and manual OVS configuration conflict on the same bridge — pick one approach per node.
- **Simpler path**: For basic VLAN trunking only, [configuring VLANs on Proxmox with Linux bridges](/articles/configure-vlans-proxmox-linux-bridges/) is lower-risk and fully sufficient.

## When OVS Is Worth the Complexity

Linux bridges handle VLAN trunking and basic isolation well. Switch to OVS when you need at least one of these:

- **Access-port VLAN assignment** — the virtual switch port drops traffic into a specific VLAN; the guest sees plain untagged Ethernet and needs no in-guest VLAN configuration
- **Port mirroring** — copy all frames from a production VM's tap interface to an IDS or monitoring VM (Zeek, Suricata inline mode) without touching the production guest
- **VXLAN between nodes** — L2 overlay tunnels for VMs on separate physical hosts without a full Ceph or shared storage fabric
- **QoS policing** — rate-limit a specific VM's uplink at the virtual switch layer, not inside the guest

If none of those scenarios apply, stay with Linux bridges. OVS misconfiguration is the fastest way to lock yourself out of a remote node with no graceful recovery path short of a serial console or physical keyboard.

## Installing Open vSwitch on Proxmox VE 9.1

OVS 3.3 is in the Debian 13 main repository — no extra sources needed:

```bash
apt update
apt install openvswitch-switch -y
```

Verify the daemon is running:

```bash
systemctl status ovs-vswitchd
```

Check the exact version:

```bash
ovs-vsctl --version
# ovs-vsctl (Open vSwitch) 3.3.x
```

The `ovs-vsctl` tool is your control plane for all bridge and port configuration. Unlike `brctl`, it writes to `ovsdb-server`, a persistent database that survives `ovs-vswitchd` restarts. Think of `ovsdb-server` as the single source of truth for your virtual switch topology.

## How to Configure the OVS Bridge in /etc/network/interfaces

**Open a serial console before you touch anything.** IPMI, iDRAC, iLO — whatever your hardware provides. A single typo in `/etc/network/interfaces` will take down the management IP and leave you with no SSH path back in. This is not a theoretical risk; it is how most OVS-on-Proxmox incidents start.

Here is the minimal working configuration: one physical NIC (`enp3s0`) uplinked into an OVS bridge (`vmbr0`), with the Proxmox host management IP on the bridge itself.

```ini
auto lo
iface lo inet loopback

auto enp3s0
iface enp3s0 inet manual
    ovs_type OVSPort
    ovs_bridge vmbr0

auto vmbr0
iface vmbr0 inet static
    address 192.168.1.100/24
    gateway 192.168.1.1
    dns-nameservers 1.1.1.1
    ovs_type OVSBridge
    ovs_ports enp3s0
```

Apply without rebooting:

```bash
ifreload -a
```

If `ifreload` is not available on your node:

```bash
ifdown vmbr0 enp3s0 && ifup enp3s0 vmbr0
```

Verify the bridge is up and the physical port is attached:

```bash
ovs-vsctl show
```

Expected output:

```
Bridge vmbr0
    Port enp3s0
        Interface enp3s0
    Port vmbr0
        Interface vmbr0
            type: internal
```

The `type: internal` port is how the Proxmox host IP lives on the bridge — it is an in-kernel virtual port, not a separate tap device. If you see it, your management IP is on the OVS bridge and SSH should work normally.

## Assigning VMs to VLAN Access Ports

This is where OVS earns its complexity overhead. Assign a specific VLAN tag to a VM's tap interface and the guest sees completely untagged Ethernet — zero guest-side VLAN configuration needed.

In the Proxmox GUI, create or edit the VM and attach its NIC to bridge `vmbr0` with no VLAN tag set. Then from the host shell:

```bash
# VM 100, first NIC creates tap interface tap100i0
ovs-vsctl set port tap100i0 tag=20
```

Confirm the assignment:

```bash
ovs-vsctl list port tap100i0 | grep tag
# tag                 : 20
```

For a firewall or router VM that needs to receive multiple tagged VLANs — pfSense, OPNsense — configure a trunk port instead:

```bash
ovs-vsctl set port tap200i0 trunks=10,20,30
```

### Persisting VLAN Assignments Across Reboots

OVS database entries survive `ovs-vswitchd` restarts but not full host reboots unless you persist them. The simplest approach for a homelab is `up` hooks in `/etc/network/interfaces`:

```ini
auto vmbr0
iface vmbr0 inet static
    address 192.168.1.100/24
    gateway 192.168.1.1
    ovs_type OVSBridge
    ovs_ports enp3s0
    up ovs-vsctl set port tap100i0 tag=20 || true
    up ovs-vsctl set port tap200i0 trunks=10,20,30 || true
```

The `|| true` prevents the bridge bring-up from failing when a tap interface does not yet exist at boot (it will not — VMs start after networking). For larger setups with many VMs, a systemd oneshot service running after `pve-guests.target` is more reliable than per-interface hooks.

## How to Mirror VM Traffic to a Monitoring VM

Scenario: VM 100 is a production web server, VM 300 runs Zeek for network traffic analysis. You want all of VM 100's traffic mirrored to VM 300's NIC without touching the web server.

```bash
ovs-vsctl \
  -- --id=@src get port tap100i0 \
  -- --id=@dst get port tap300i0 \
  -- --id=@mirror create mirror name=web-mirror \
       select-src-port=@src select-dst-port=@src \
       output-port=@dst \
  -- add bridge vmbr0 mirrors @mirror
```

Verify the mirror is active:

```bash
ovs-vsctl list mirror
```

Inside VM 300, put the NIC in promiscuous mode and point Zeek at it. The mirrored frames arrive unmodified — no VLAN stripping, no encapsulation. Expect roughly 5–8% CPU overhead on the host under sustained traffic due to the frame duplication path.

To remove the mirror when done:

```bash
ovs-vsctl clear bridge vmbr0 mirrors
```

## Setting Up a VXLAN Overlay Between Two Proxmox Nodes

VXLAN creates a virtual L2 segment over an existing L3 connection, letting VMs on two separate physical hosts share a broadcast domain. This is the lightweight alternative to a full Ceph fabric when you are [building a private cloud at home with Proxmox](/articles/build-private-cloud-home-proxmox-ve/) and want VM-to-VM flat networking without shared storage dependencies.

**Configuration:**
- Node A management IP: `192.168.1.100`
- Node B management IP: `192.168.1.101`
- VNI (VXLAN Network Identifier): `100`
- Overlay bridge name: `vxbr0`

**On Node A:**

```bash
ovs-vsctl add-br vxbr0
ovs-vsctl add-port vxbr0 vxlan0 \
  -- set interface vxlan0 type=vxlan \
     options:remote_ip=192.168.1.101 \
     options:key=100 \
     options:dst_port=4789
```

**On Node B:**

```bash
ovs-vsctl add-br vxbr0
ovs-vsctl add-port vxbr0 vxlan0 \
  -- set interface vxlan0 type=vxlan \
     options:remote_ip=192.168.1.100 \
     options:key=100 \
     options:dst_port=4789
```

VMs attached to `vxbr0` on either node are now on the same L2 segment. Ping between them to confirm. Expect 5–8% throughput loss versus native L2 on a 10 GbE link due to VXLAN encapsulation — acceptable for almost all services except high-frequency storage traffic.

Persist the overlay bridge in `/etc/network/interfaces` on each node:

```ini
auto vxbr0
allow-ovs vxbr0
iface vxbr0 inet manual
    ovs_type OVSBridge

allow-vxbr0 vxlan0
iface vxlan0 inet manual
    ovs_type OVSPort
    ovs_bridge vxbr0
    ovs_options type=vxlan options:remote_ip=192.168.1.101 options:key=100 options:dst_port=4789
```

## OVS vs Linux Bridge: Feature Comparison

| Feature | Linux Bridge | Open vSwitch |
|---|---|---|
| VLAN trunking | Yes | Yes |
| Per-port VLAN (access mode) | Requires SDN or manual `ip link` hacks | Native |
| Port mirroring | No | Yes |
| VXLAN tunnels | Partial (`ip link add ... type vxlan`) | Native, composable |
| QoS policing | Limited (`tc` only) | Built-in via OVS queue config |
| Proxmox GUI VM attachment | Full | Full |
| Port-level config | GUI | CLI only |
| Configuration file | `/etc/network/interfaces` | OVS DB + `/etc/network/interfaces` |
| Misconfiguration risk | Low | Higher — lockout possible |

The practical takeaway: Proxmox's GUI sees OVS bridges as ordinary bridges for VM attachment. You assign VMs normally in the GUI, then fine-tune port behavior from the CLI. That hybrid workflow is comfortable within a week of daily use.

## Troubleshooting Common OVS Issues on Proxmox

**OVS bridge does not come up after reboot.** Verify that `openvswitch-switch` starts before the `networking` service and is enabled:

```bash
systemctl status openvswitch-switch
systemctl enable openvswitch-switch
```

If the service starts too late relative to `networking.service`, add an explicit `After=openvswitch-switch.service` ordering to a networking drop-in under `/etc/systemd/system/networking.service.d/`.

**Tap interfaces missing from OVS after VM restart.** Proxmox creates tap devices on VM start and destroys them on VM stop. Your `/etc/network/interfaces` `up` hooks fired at boot when no taps existed yet. Use the `|| true` idiom above, or write a QEMU hook script at `/etc/pve/qemu-server/hooks/` to apply port config each time a specific VM starts.

**Management IP disappeared after switching to OVS.** The physical NIC stanza must be `inet manual` with the IP address only in the bridge stanza. Diagnose from the serial console:

```bash
ip addr show enp3s0
ip addr show vmbr0
```

If the IP is on the NIC instead of the bridge, edit `/etc/network/interfaces` from the serial console and run `ifreload -a`.

**Proxmox SDN module conflicts with manual OVS.** If you have used Proxmox SDN on this node, it may attempt to manage the same bridge names. Use SDN exclusively or disable the SDN controller for this node — mixing manual OVS configuration with SDN on the same bridge produces unpredictable results that are difficult to debug remotely.

## Hardening the OVS Configuration

By default, OVS enables STP on all bridges and is ready to accept an external OpenFlow controller connection. On a standalone homelab node you want neither:

```bash
# Disable STP on the management bridge
ovs-vsctl set bridge vmbr0 stp_enable=false

# Remove any external OpenFlow controller pairing
ovs-vsctl del-controller vmbr0

# Verify OVSDB is not listening on a network socket (empty output = correct)
ovs-vsctl get-manager
```

If `get-manager` returns a TCP address, remove it:

```bash
ovs-vsctl del-manager
```

For host-level nftables firewall rules and SSH hardening that complement OVS, see [Hardening Proxmox VE: Firewall, fail2ban, and SSH Security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) — the host firewall rules apply identically whether you use Linux bridges or OVS underneath. For a broader look at hypervisor attack surface including virtual NIC escape vectors, [LOLPROX: Protecting Proxmox from Hypervisor Exploits](/articles/lolprox-protecting-proxmox-from-hypervisor-exploits/) covers the threat model in detail.

## Conclusion

Open vSwitch on Proxmox VE 9.1 is the right tool when you need access-port VLAN assignment, port mirroring for an IDS VM, or VXLAN overlays between nodes — and it is overkill for everything else. The install is a single `apt install openvswitch-switch`, the configuration lives in the same `/etc/network/interfaces` file you already use, and the per-port CLI workflow becomes routine within an afternoon. The immediate next step: attach a firewall VM to `vmbr0` as a trunk port carrying VLANs 10, 20, and 30, then set your production workload VMs to access ports on their respective VLANs — that is where the isolation model fully clicks into place.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="open-vswitch"/>
        <category label="networking"/>
        <category label="vlans"/>
        <category label="vxlan"/>
        <category label="proxmox"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[6 Must-Have LXC Containers for Your Proxmox Homelab]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-homelab-essential-lxc-containers/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-homelab-essential-lxc-containers/"/>
        <updated>2026-04-29T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Six LXC containers that cover the majority of Proxmox homelab infrastructure needs, with exact RAM specs, pct commands, and real-world gotchas for each.]]></summary>
        <content type="html"><![CDATA[
If you're running Proxmox VE 9.1 and wondering what to put inside it, these six LXC containers cover the majority of homelab infrastructure needs: DNS filtering, SSL reverse proxying, password management, uptime monitoring, media serving, and identity management. Each runs on under 1 GB RAM, boots in seconds, and coexists on a single 8 GB node with plenty of headroom. By the end of this guide you'll have a homelab services stack that punches well above its weight.

## Key Takeaways

- **Lightweight**: Each container uses 128–512 MB RAM; all six together idle at around 1 GB combined.
- **Unprivileged by default**: All six run as unprivileged LXC containers — Jellyfin needs two extra cgroup lines for hardware transcoding, nothing more.
- **Order matters**: Deploy Pi-hole first so every subsequent container can use it for local DNS immediately.
- **Docker optional**: Four of the six run as native systemd services; only NPM and Authentik benefit from Docker Compose.
- **Minimum hardware**: A node with 8 GB RAM and 60 GB SSD handles the full stack comfortably with room for Proxmox backups.

## Why LXC Instead of Full VMs for These Workloads

LXC containers share the host kernel, which means sub-second boot times and near-zero overhead versus a full KVM VM. A Pi-hole VM running Debian idles at around 350 MB RAM just for the OS layer. The same workload in an LXC container uses 70 MB. For services that spend most of their life waiting — DNS resolvers, uptime checkers, password vaults — that gap is the difference between fitting six services on a 4 GB node or needing 16 GB.

The tradeoff: LXC containers share the host kernel, so a kernel-level exploit could theoretically affect other containers on the same host. For most homelab threat models this is acceptable. If you want the full isolation picture, the guide on [running Docker inside LXC containers on Proxmox](/articles/docker-inside-lxc-containers-proxmox/) covers exactly where the isolation boundary sits and when a full VM is warranted instead.

## How to Create LXC Containers from the CLI

All six containers follow the same creation pattern. Pull the Debian 12 template first:

```bash
pveam update
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
```

Then create and start with `pct`:

```bash
pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname pihole \
  --memory 256 \
  --cores 1 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.10/24,gw=192.168.1.1 \
  --storage local-lvm \
  --rootfs local-lvm:4 \
  --unprivileged 1 \
  --start 1
```

Adjust `--memory`, `--rootfs`, the CT ID, and the IP for each service. The specs below are from a production homelab running Proxmox VE 9.1 — not theoretical minimums.

## Container 1: Pi-hole — Network-Wide DNS Filtering

**CT ID**: 200 | **RAM**: 256 MB | **Disk**: 4 GB | **IP**: 192.168.1.10

Pi-hole on Debian 12 in an unprivileged LXC is the foundation of the entire stack. Every other container and LAN client points to it for DNS, so deploy this one first.

```bash
pct exec 200 -- bash -c "apt update && apt install -y curl"
pct exec 200 -- bash -c "curl -sSL https://install.pi-hole.net | bash"
```

After the installer exits, set the web admin password:

```bash
pct exec 200 -- pihole -a -p yourpassword
```

**Gotcha**: Pi-hole's installer configures `eth0` as the listening interface and complains if the container's DNS already resolves to localhost. Before running the installer, check `/etc/resolv.conf` inside the container and temporarily set an upstream like `1.1.1.1`. Switch it back to `127.0.0.1` after Pi-hole is running. The admin UI lands at `http://192.168.1.10/admin`.

## Container 2: Nginx Proxy Manager — SSL Reverse Proxy

**CT ID**: 201 | **RAM**: 512 MB | **Disk**: 8 GB | **IP**: 192.168.1.11 (needs ports 80 and 443)

Nginx Proxy Manager gives you a web GUI for SSL termination, Let's Encrypt auto-renewal, and subdomain routing to internal services. This is the one container in the list where Docker Compose pays off — the NPM image is significantly easier to update than a manual nginx + certbot setup. For a broader look at managing Docker workloads on Proxmox, the guide on [managing Docker on Proxmox with Portainer and Dockge](/articles/managing-docker-on-proxmox-with-portainer-and-dockge/) covers the tooling that complements NPM.

Inside the container:

```bash
apt update && apt install -y docker.io docker-compose-plugin
mkdir -p /opt/npm && cd /opt/npm
```

Create `/opt/npm/docker-compose.yml`:

```yaml
services:
  npm:
    image: jc21/nginx-proxy-manager:2.12.1
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
```

```bash
docker compose up -d
```

Default credentials: `admin@example.com` / `changeme`. Change both immediately on first login at `http://192.168.1.11:81`.

**Gotcha**: Pi-hole and NPM must be on different static IPs. They don't share ports, but assigning both to `192.168.1.10` is a common mistake that causes maddening DNS resolution failures. Give NPM its own IP from the start.

## Container 3: Vaultwarden — Self-Hosted Password Manager

**CT ID**: 202 | **RAM**: 256 MB | **Disk**: 4 GB | **IP**: 192.168.1.12

Vaultwarden is a Bitwarden-compatible server written in Rust. It handles the full Bitwarden client API — browser extensions, mobile apps, the desktop client — at under 25 MB resident memory at idle. The official Bitwarden server requires 2 GB RAM minimum; Vaultwarden replaces it entirely for personal or small-team use.

```bash
apt update && apt install -y docker.io
```

Generate an Argon2 admin token before starting the container:

```bash
docker run --rm -it vaultwarden/server:1.32.0 /vaultwarden hash --preset owasp
```

Copy the `$argon2id$...` output, then start the container:

```bash
docker run -d \
  --name vaultwarden \
  --restart unless-stopped \
  -e ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$YOURTOKEN' \
  -v /opt/vaultwarden/data:/data \
  -p 8080:80 \
  vaultwarden/server:1.32.0
```

**Gotcha**: Bitwarden clients require HTTPS. Vaultwarden over plain HTTP works only on localhost — mobile app syncs fail silently over HTTP on a LAN IP without any useful error message. You must proxy it through Nginx Proxy Manager with a valid Let's Encrypt cert before pointing any clients at it. Add a proxy host in NPM for `vault.yourdomain.com` → `192.168.1.12:8080` before importing any passwords.

**Timing**: From container creation to first successful sync with the Bitwarden browser extension takes under 90 seconds once DNS and SSL are configured.

## Container 4: Uptime Kuma — Service Monitoring Dashboard

**CT ID**: 203 | **RAM**: 256 MB | **Disk**: 4 GB | **IP**: 192.168.1.13

Uptime Kuma monitors HTTP endpoints, TCP ports, DNS records, and ping targets, then alerts you via Telegram, Discord, SMTP, or webhooks when something goes down. It also generates a public status page — useful if you're running services for family members or a small team.

```bash
apt update && apt install -y docker.io
docker run -d \
  --name uptime-kuma \
  --restart unless-stopped \
  -v /opt/uptime-kuma:/app/data \
  -p 3001:3001 \
  louislam/uptime-kuma:1.23.16
```

The web UI is at `http://192.168.1.13:3001`. Add monitors for Pi-hole, NPM, Vaultwarden, and anything else you've deployed — the whole point of this container is to catch failures before your users do.

**Gotcha**: Uptime Kuma stores everything in SQLite. If the 4 GB rootfs fills up — which happens if you enable verbose logging and forget about it — the database stops writing and you lose monitoring history with no obvious error in the UI. Check disk usage monthly with `df -h` inside the container and make sure Proxmox has a backup job scheduled for this CT.

## Container 5: Jellyfin — Self-Hosted Media Server

**CT ID**: 204 | **RAM**: 1024 MB | **Disk**: 8 GB root + media bind mount | **IP**: 192.168.1.14

Jellyfin is the only container in this list that needs more than 512 MB RAM — library scanning on large collections briefly spikes to 1.2 GB. The rootfs stays lean at 8 GB because the actual media lives on the host or NAS, mounted into the container as a bind mount.

Add the bind mount in `/etc/pve/lxc/204.conf` before starting the container:

```ini
mp0: /mnt/nas/media,mp=/media
```

Then install Jellyfin 10.10.x (current stable as of April 2026):

```bash
curl https://repo.jellyfin.org/install-debuntu.sh | bash
systemctl enable --now jellyfin
```

For **Intel Quick Sync hardware transcoding** in an unprivileged LXC, add these lines to `/etc/pve/lxc/204.conf`:

```ini
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir
```

Then add the `jellyfin` user to the `render` and `video` groups inside the container and restart:

```bash
usermod -aG render,video jellyfin
systemctl restart jellyfin
```

**Gotcha**: In an unprivileged LXC, the `jellyfin` user (UID 999 inside the container) maps to UID 100999 on the host. Your bind-mounted media directory needs to be readable by UID 100999. Fix it with `chown -R 100999:100999 /mnt/nas/media` on the host, or use ACLs if the mount is shared with other services.

## Container 6: Authentik — Self-Hosted Identity Provider

**CT ID**: 205 | **RAM**: 1024 MB | **Disk**: 10 GB | **IP**: 192.168.1.15

Authentik is a self-hosted identity provider that adds SSO, OAuth2, LDAP, and SAML to your homelab. Once running, Nginx Proxy Manager can forward authentication to Authentik before proxying any service — meaning Vaultwarden, Jellyfin, and Uptime Kuma all sit behind a single login page without modifying those applications at all.

Authentik requires PostgreSQL and Redis, making Docker Compose the only sane choice here:

```bash
apt update && apt install -y docker.io docker-compose-plugin
mkdir /opt/authentik && cd /opt/authentik
```

Download the official compose file from the Authentik documentation, then generate secrets:

```bash
echo "PG_PASS=$(openssl rand -base64 36 | tr -d '=+/')" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '=+/')" >> .env
echo "AUTHENTIK_ERROR_REPORTING__ENABLED=false" >> .env
```

```bash
docker compose pull && docker compose up -d
```

First startup takes 2–3 minutes while Authentik runs database migrations. Complete setup at `http://192.168.1.15:9000/if/flow/initial-setup/`.

**Gotcha**: The default Authentik compose file uses `latest` image tags. Pin to a specific release (e.g., `ghcr.io/goauthentik/server:2024.12.3`) before your first pull. Authentik occasionally ships breaking API changes between minor versions, and an unattended `docker compose pull` on the wrong day will break SSO for every proxied service simultaneously.

**Worth the complexity?** Only if you have five or more services to protect. For a two-container setup, HTTP Basic Auth through NPM is sufficient. Authentik pays off when you want audit logs, TOTP enforcement, and a single logout that propagates across all services at once.

## Resource Planning: Running All Six on One Node

| Container | RAM Allocated | RAM at Idle | vCPUs | Disk |
|---|---|---|---|---|
| Pi-hole | 256 MB | 70 MB | 1 | 4 GB |
| Nginx Proxy Manager | 512 MB | 180 MB | 1 | 8 GB |
| Vaultwarden | 256 MB | 25 MB | 1 | 4 GB |
| Uptime Kuma | 256 MB | 90 MB | 1 | 4 GB |
| Jellyfin | 1024 MB | 220 MB | 2 | 8 GB + media |
| Authentik | 1024 MB | 450 MB | 2 | 10 GB |
| **Total** | **3328 MB** | **~1035 MB** | **8** | **38 GB** |

An 8 GB node handles the full stack with headroom. On a 4 GB node, drop Authentik — it's the heaviest container and the least essential for a basic homelab. For guidance on node selection, storage layout, and networking to support this kind of infrastructure, the guide on [building a private cloud at home with Proxmox VE](/articles/build-private-cloud-home-proxmox-ve/) covers the hardware decisions that set the foundation.

## Security Hardening for the Container Stack

All six containers handle sensitive data. A few non-negotiable steps before you consider this stack production-ready:

- **Restrict admin ports**: Lock ports 81 (NPM admin), 3001 (Uptime Kuma), and 9000 (Authentik) to your LAN subnet using Proxmox firewall rules. The [Proxmox firewall and SSH hardening guide](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) covers datacenter-level rules that apply at the container level without touching iptables manually.
- **Schedule backups**: All six containers store persistent state on disk. Configure a Proxmox backup job for each CT in Datacenter → Backup, targeting PBS or an NFS share. Weekly retention of two backups is the minimum viable safety net.
- **Pin versions**: Vaultwarden and Authentik are security-critical. Never run `latest` tags — pin to a specific version and update deliberately after reading the changelog.
- **Enable TOTP**: Pi-hole, NPM, Uptime Kuma, and Authentik all support TOTP. Enable it on each admin account before exposing any service through NPM to the internet.

## Conclusion

These six LXC containers — Pi-hole, Nginx Proxy Manager, Vaultwarden, Uptime Kuma, Jellyfin, and Authentik — give you a complete homelab services layer that runs comfortably on a single 8 GB Proxmox VE 9.1 node. Deploy them in order: DNS first, reverse proxy second, then everything else behind it. Once Authentik is in place, the natural next step is wiring its forward-auth middleware into each NPM proxy host — a 10-minute configuration that replaces per-service login prompts with a single SSO portal covering your entire homelab.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="lxc"/>
        <category label="homelab"/>
        <category label="pihole"/>
        <category label="jellyfin"/>
        <category label="nginx-proxy-manager"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Migrate Bare-Metal TrueNAS to Proxmox Without Data Loss]]></title>
        <id>https://proxmoxpulse.com/articles/migrate-bare-metal-truenas-proxmox-zfs/</id>
        <link href="https://proxmoxpulse.com/articles/migrate-bare-metal-truenas-proxmox-zfs/"/>
        <updated>2026-04-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Move your TrueNAS bare-metal installation to a Proxmox VM without touching your ZFS pools. HBA passthrough, disk passthrough, and pool import — step by step.]]></summary>
        <content type="html"><![CDATA[
The safest way to move a running bare-metal TrueNAS machine to a Proxmox VM is to pass your storage controller directly to the guest — either an HBA via PCIe passthrough or individual drives via disk passthrough. Done right, TrueNAS imports its existing ZFS pools on first boot inside the VM, your data stays intact, and Proxmox never touches the pool metadata.

## Key Takeaways

- **HBA passthrough**: The cleanest path — pass the entire controller to the VM so Proxmox never sees the pool drives.
- **Disk passthrough**: Works when you can't pass the full HBA; always use `/dev/disk/by-id/` paths, never `/dev/sdX`.
- **Export pools first**: If Proxmox has auto-imported your ZFS pools on the host, export them before attaching drives to the VM.
- **TrueNAS SCALE 24.10**: Runs as a Proxmox VM with minor config tweaks; TrueNAS CORE works identically.
- **Risk window**: Data loss is most likely during the brief moment when drives are attached to both host and VM — don't let that happen.

## Why Virtualize TrueNAS Instead of Running It Bare Metal

Running TrueNAS on bare metal is fine until you want to share the server. A dedicated NAS machine locks up hardware that could also run VMs, containers, and backup jobs. Virtualizing on Proxmox gives you VM-level snapshots before TrueNAS updates, flexible resource allocation without touching hardware, and one unified management UI for your NAS and your [homelab VMs](/articles/build-private-cloud-home-proxmox-ve/).

The tradeoff: the storage controller must be either passed through to the VM or replaced with virtual block devices. If your pools live on a dedicated PCIe HBA, passthrough is straightforward. If they're on motherboard SATA ports, you'll need individual disk passthrough — which introduces IOMMU group constraints covered below.

## What You Need Before You Start

- CPU with VT-d (Intel) or AMD-Vi enabled in UEFI
- At least 16 GB RAM (TrueNAS SCALE 24.10 enforces this minimum)
- A separate boot drive for Proxmox — not one of your pool drives
- TrueNAS SCALE 24.10.2 installer ISO

Confirm IOMMU is active on the Proxmox host:

```bash
dmesg | grep -e DMAR -e IOMMU | head -20
```

If the output is empty, enable it in GRUB:

```bash
# /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"
# AMD: replace intel_iommu=on with amd_iommu=on
```

```bash
update-grub && reboot
```

The `iommu=pt` flag reduces overhead for devices the host isn't passing through — relevant when Proxmox itself is using NVMe while pool drives go straight to the TrueNAS VM.

## How to Stop Proxmox from Importing Your ZFS Pools

This is the most common gotcha. Proxmox boots with pool drives attached, ZFS auto-imports every pool it finds — including TrueNAS pools. If the host holds a pool while the VM tries to import it, you get a failed import or silent metadata corruption.

Check what's already imported:

```bash
zpool status
```

If your TrueNAS pool names appear here, clear them from the cache file and export them:

```bash
zpool set cachefile=none tank
zpool export tank
```

Run this for every pool being migrated. `cachefile=none` removes the pool from `/etc/zfs/zpool.cache` so it won't auto-import on the next reboot. Pass those drives to the TrueNAS VM immediately — if you reboot Proxmox first, ZFS will import them again.

## Option 1: Pass Through an HBA Controller (Recommended)

For drives connected to a dedicated PCIe HBA — any IT-mode card, LSI 9207-8i, LSI 9300-8i — this is the cleanest approach. The host never sees the drives.

Find the HBA's PCI address:

```bash
lspci -nn | grep -Ei "lsi|megaraid|sas|storage controller"
# Example: 02:00.0 Serial Attached SCSI controller [0107]: Broadcom / LSI SAS9207-8i [1000:0097]
```

Verify it's alone in its IOMMU group:

```bash
find /sys/kernel/iommu_groups/ -name "0000:02:00.0" 2>/dev/null
# Output: /sys/kernel/iommu_groups/14/devices/0000:02:00.0
ls /sys/kernel/iommu_groups/14/devices/
```

If the group contains only the HBA (a co-located PCIe root port is fine), add it to the VM after creation:

```bash
qm set 110 --hostpci0 02:00.0,pcie=1,rombar=0
```

`pcie=1` is required for modern HBAs. `rombar=0` prevents a boot hang seen with some LSI cards inside QEMU.

## Option 2: Pass Through Individual Disks

When the SATA controller is part of the motherboard chipset and shared with the Proxmox boot drive, pass individual drives instead. Never use `/dev/sdX` — those letters reassign at boot. Use stable by-id paths:

```bash
ls -la /dev/disk/by-id/ | grep -v part | grep ata
# ata-WDC_WD40EFRX-68N32N0_WD-WCC7K1234567 -> ../../sdb
# ata-WDC_WD40EFRX-68N32N0_WD-WCC7K7654321 -> ../../sdc
```

After exporting the pool, attach each drive:

```bash
qm set 110 --scsi1 /dev/disk/by-id/ata-WDC_WD40EFRX-68N32N0_WD-WCC7K1234567
qm set 110 --scsi2 /dev/disk/by-id/ata-WDC_WD40EFRX-68N32N0_WD-WCC7K7654321
```

Repeat for every drive. A 6-drive RAIDZ2 means six `qm set` commands. This is worth it only if you can't add a dedicated HBA. For long-term use, a used IT-mode card at $20–$40 on eBay is the cleaner investment.

## Create the TrueNAS VM

```bash
qm create 110 \
  --name truenas-scale \
  --memory 32768 \
  --balloon 0 \
  --cores 4 \
  --sockets 1 \
  --cpu host \
  --machine q35 \
  --bios ovmf \
  --net0 virtio,bridge=vmbr0 \
  --ostype l26
```

`--balloon 0` prevents Proxmox from reclaiming RAM that TrueNAS is actively using as ZFS ARC cache.

Add the supporting disks and installer:

```bash
# EFI disk for OVMF boot
qm set 110 --efidisk0 local-lvm:1,efitype=4m,pre-enroll-keys=0

# OS boot disk — TrueNAS uses a mirrored boot pool, 32 GB minimum
qm set 110 --scsi0 local-lvm:32,format=raw,iothread=1,ssd=1

# Installer ISO
qm set 110 --ide2 local:iso/TrueNAS-SCALE-24.10.2.iso,media=cdrom

# Boot order: ISO first, then OS disk
qm set 110 --boot order="ide2;scsi0"
```

The IOMMU mechanics are identical to [GPU passthrough on Proxmox](/articles/gpu-passthrough-proxmox-complete-guide/). If you've done GPU passthrough before, the `hostpci` setup will feel familiar — you're just passing storage instead of video. Now attach the HBA or individual disks from the previous sections.

## Install TrueNAS and Import Your Existing Pools

Boot the VM. The TrueNAS SCALE 24.10.2 installer completes in under five minutes on NVMe. Select the OS boot disk (`scsi0`) as the installation target — not the pool drives.

After TrueNAS reboots into the dashboard, go to **Storage → Import Pool**. TrueNAS scans the attached drives and surfaces your existing ZFS pool. Select it and click **Import**. For a 20 TB pool this finishes in under 30 seconds — no data moves, only pool metadata is recognized.

Verify from the TrueNAS shell or SSH:

```bash
zpool status
zfs list -r
zpool history tank | tail -20
```

All vdevs should show `ONLINE`. If your most recent scrub event appears in the history output, the pool imported from the correct state.

## Reconfigure Shares and Verify Before Decommissioning

The IP and interface name change because TrueNAS now has a `virtio` NIC. In the TrueNAS UI, go to **Network → Interfaces**, set a static IP, then update any SMB or NFS bindings that reference the old interface. Re-enable periodic scrub schedules under **Data Protection** — they don't carry over with a pool import.

If you have [Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) pointing at a TrueNAS share, update the storage definition with the new IP. The datastore contents are unchanged.

Before wiping the old bare-metal install, run a full scrub inside the VM:

```bash
zpool scrub tank
watch zpool status
```

Expect roughly 1 hour per 10 TB on spinning disks. Also check SMART data on all drives to confirm nothing new appeared during the migration:

```bash
for disk in /dev/sdb /dev/sdc /dev/sdd /dev/sde; do
  echo "=== $disk ==="
  smartctl -a "$disk" | grep -E "Reallocated|Pending|Uncorrectable"
done
```

Keep the bare-metal server powered off but intact for at least a week. If a missing share or misconfigured permission surfaces, you'll want that fallback.

## Conclusion

With an HBA or pool drives passed through directly, TrueNAS imports existing ZFS 2.2.x pools intact and your data never leaves the disks. The critical steps are exporting pools from the Proxmox host before attaching them to the VM, using `/dev/disk/by-id/` for individual disk passthrough, and running a post-import scrub to confirm clean pool state. Next, consider [hardening the Proxmox host with firewall rules and fail2ban](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) — the NAS is only as secure as the hypervisor it runs on.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="truenas"/>
        <category label="zfs"/>
        <category label="disk-passthrough"/>
        <category label="hba-passthrough"/>
        <category label="storage-migration"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[CPU Pinning on Proxmox for Low-Latency VM Workloads]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-cpu-pinning-low-latency-vms/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-cpu-pinning-low-latency-vms/"/>
        <updated>2026-04-27T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Pin VM cores on Proxmox VE 9 to eliminate scheduler jitter and cut latency. Covers isolcpus, cpuaffinity, NUMA alignment, and when pinning actually helps.]]></summary>
        <content type="html"><![CDATA[
CPU pinning on Proxmox VE 9 assigns each vCPU thread in a VM to a specific physical CPU core, bypassing the Linux scheduler's normal load-balancing across all available cores. The result is consistent, predictable latency — the kind that matters for a Windows gaming VM with GPU passthrough, a real-time audio workstation, or an API server where p99 latency is a hard SLA. By the end of this guide your VM will have dedicated cores, the host scheduler will leave them alone, and you'll have three commands to verify the pinning is actually working.

## Key Takeaways

- **cpuaffinity**: Run `qm set <vmid> --cpuaffinity 4-7` to bind a QEMU process and all its threads to specific physical cores
- **isolcpus matters**: Without kernel-level core isolation, other processes can still preempt your pinned VM threads and cause jitter
- **NUMA alignment**: On EPYC or multi-socket systems, pin memory and CPUs to the same NUMA node or every memory access crosses the interconnect
- **Live migration caveat**: A VM with `cpu: host` set will not migrate to nodes with a different CPU microarchitecture — design for this upfront
- **Not always worth it**: General-purpose VMs and idle containers get no benefit; reserve pinning for sustained latency-sensitive workloads

## What CPU Pinning Actually Does Under the Hood

Without pinning, the Linux Completely Fair Scheduler (CFS) migrates QEMU threads across physical cores continuously. This is efficient for throughput, but every thread migration causes L1 and L2 cache evictions on the core the thread just left and cold-cache misses on the core it lands on. For a 4K gaming session or a Postgres database handling sub-100 ms queries, those cache evictions translate to visible stutters or latency spikes.

`cpuaffinity` in Proxmox works by setting the cpuset cgroup for the entire QEMU process at VM start. Every thread QEMU spawns — vCPU threads, the I/O thread, device emulation threads — is constrained to the cores you specify. This is a process-level constraint rather than per-vCPU-thread pinning, but for the overwhelming majority of workloads it produces the same result with far less configuration overhead.

The kernel parameter `isolcpus` goes one step further: it removes specified cores from the scheduler's pool entirely. Nothing lands on an isolated core unless it explicitly requests affinity for that core. This is the difference between "I want my VM on cores 4-7" and "nothing else touches cores 4-7, ever."

## How to Check Your NUMA Topology First

Understanding your hardware layout before picking cores is non-negotiable. Pinning to sibling hyperthreads on a saturated physical core, or crossing NUMA nodes, will make latency *worse* than no pinning.

```bash
lscpu --extended
```

Look at the `CPU`, `CORE`, `SOCKET`, and `NODE` columns. On an Intel Core i9-13900K, the 8 P-cores appear in hyperthreaded pairs — logical CPUs 0 and 16 share physical core 0, logical CPUs 1 and 17 share physical core 1, and so on. Always pin both hyperthreads of the same physical core together.

For NUMA topology on EPYC, Threadripper, or any dual-socket system:

```bash
numactl --hardware
```

On a single-socket consumer CPU this shows one node. On an EPYC 7302P, memory latency between the two NUMA nodes differs by roughly 30 ns — enough to add measurable jitter to database workloads.

For the most detailed view, install `hwloc` and render the full topology tree:

```bash
apt install hwloc -y
lstopo --no-io
```

This shows L3 cache domains. On AMD CPUs each CCX shares its own L3, so keep all pinned cores within a single CCX. On a Ryzen 9 7950X with two 8-core CCDs, keeping the VM to one CCD means all 8 cores share the same 32 MB L3 — a meaningful cache advantage for workloads with hot working sets.

## How to Isolate Cores from the Host Kernel

This step is optional for lightly loaded hosts but strongly recommended for gaming or real-time workloads. Open the GRUB config:

```bash
nano /etc/default/grub
```

Append to `GRUB_CMDLINE_LINUX_DEFAULT`:

```ini
GRUB_CMDLINE_LINUX_DEFAULT="quiet isolcpus=4-7,12-15 nohz_full=4-7,12-15 rcu_nocbs=4-7,12-15"
```

`nohz_full` stops the kernel from sending periodic timer interrupts to isolated cores, reducing jitter from hundreds of microseconds down to under 5 µs. `rcu_nocbs` offloads RCU callbacks off those same cores. All three options must reference the same core list.

Apply and reboot:

```bash
update-grub && reboot
```

After reboot, confirm isolation is active:

```bash
cat /sys/devices/system/cpu/isolated
# Expected output: 4-7,12-15
```

Leave at least 2 physical cores (plus their hyperthreads) for the Proxmox host OS. On a 12-core i9-12900K I keep cores 0-3 for Proxmox — the web UI, storage daemons, PBS sync jobs, and any lightweight LXC containers all run there without touching the VM's dedicated P-cores.

## How to Configure cpuaffinity on a Proxmox VM

The `cpuaffinity` option accepts a comma-separated list of CPU IDs or ranges matching the logical CPU numbers from `lscpu`.

```bash
qm set 100 --cpuaffinity 4-7
```

Or edit the config file directly:

```bash
nano /etc/pve/qemu-server/100.conf
```

```ini
cpuaffinity: 4-7
```

The number of vCPUs in the VM does not need to exactly match the pinned range. If you pin to `4-7` (4 logical CPUs) and the VM has `cores: 4,sockets: 1`, each vCPU thread lands on one core. QEMU's I/O thread and device emulation threads are also constrained to that range, but they are lightweight and do not meaningfully compete with the vCPU threads.

For a gaming VM with 8 vCPUs across 4 physical P-cores with hyperthreading:

```bash
qm set 100 --cpuaffinity 4-7,12-15
qm set 100 --cores 8 --sockets 1
qm set 100 --cpu host
```

The `cpu: host` flag passes all physical CPU features directly to the guest, including AVX-512 and TSC invariant mode. Pair this with [the full GPU passthrough configuration](/articles/gpu-passthrough-proxmox-complete-guide/) to get consistent frame times from a dedicated gaming or AI VM.

The tradeoff: `cpu: host` makes the VM non-migratable to hosts with a different CPU microarchitecture. If you're running [a K3s Kubernetes cluster on Proxmox](/articles/k3s-kubernetes-cluster-proxmox-vms/) and need live migration across heterogeneous nodes, stick with `cpu: x86-64-v3` and skip `cpuaffinity` on those VMs — portability matters more than the marginal latency gain.

## NUMA Alignment: Bind Memory to the Right Node

On any multi-NUMA system, memory accesses that cross NUMA nodes add 20-40 ns per access. If your pinned cores are on NUMA node 0, bind the VM's RAM to node 0 as well.

Confirm which NUMA node owns your target cores:

```bash
numactl --hardware
# Look for "node 0 cpus:" and "node 1 cpus:" lines
```

Then add NUMA binding to the VM config:

```bash
nano /etc/pve/qemu-server/100.conf
```

```ini
cpuaffinity: 4-7
numa: 1
numa0: cpus=4-7,hostnodes=0,memory=16384,policy=bind
```

`policy=bind` hard-allocates guest RAM from NUMA node 0 only. If node 0 runs out of free memory the VM will fail to start rather than silently falling back to slower remote memory. `policy=preferred` is softer but can mask a memory budget mistake. Use `bind` so you know immediately if something is wrong.

On a properly aligned Proxmox EPYC system, STREAM TRIAD benchmark scores inside the guest improve 20-35% compared to unaligned placement. On real-world workloads like Postgres or ffmpeg encoding, expect 8-15% throughput improvement with p99 latency 30-50% lower.

## Verifying the Pinning Is Actually Working

Start the VM and locate the QEMU process ID:

```bash
qm start 100
pgrep -a qemu-system-x86 | grep " 100 "
```

Check process-level affinity:

```bash
taskset -cp <pid>
# pid 12345's current affinity list: 4-7,12-15
```

Inspect individual thread placement:

```bash
ps -eLo pid,tid,psr,comm | grep qemu | head -20
```

The `PSR` column shows which physical CPU each thread is currently scheduled on. Every thread should show a value within your pinned range. If any land outside it, the `isolcpus` parameter was not applied — run `cat /proc/cmdline` to confirm it appears in the active boot parameters.

For a latency measurement inside a Linux guest, install and run `cyclictest` from the `rt-tests` package:

```bash
apt install rt-tests -y
cyclictest --mlockall --smp --priority=80 --interval=200 --distance=0 --loops=10000
```

On a pinned, isolated 4-core setup with `nohz_full` on a 13th-gen Intel host, worst-case latency consistently lands under 80 µs. Without pinning on the same hardware under moderate host load, worst-case values hit 3-8 ms. That is a 40x difference at the p99.99 — the exact improvement that eliminates audio dropouts in a DAW or micro-stutters in a GPU passthrough gaming VM.

## When CPU Pinning Is Worth the Effort (and When It Isn't)

Worth pinning:

- **GPU passthrough gaming VMs**: frame time consistency depends on stable vCPU scheduling; even 2 ms jitter shows up as stutters at 144 Hz
- **Audio production workstations**: real-time audio at 48 kHz with a 128-sample buffer needs scheduling latency well under 3 ms
- **Low-latency databases**: Postgres, Redis, or ClickHouse under sustained query load benefit measurably from cache-warm cores
- **[Home Assistant with heavy integrations](/articles/home-assistant-os-on-proxmox-2026-setup-guide/)**: fast polling loops on a loaded host miss intervals without dedicated cores

Not worth pinning:

- **General-purpose web servers**: CFS handles bursty traffic better than static affinity; pinning forfeits burst headroom
- **LXC containers**: use `cpulimit` cgroup throttling instead — the overhead-to-benefit ratio for LXC pinning is rarely justified
- **Lightly loaded single-VM hosts**: CFS already gives a near-idle VM the run of all available cores
- **Batch compute VMs**: a VM that runs a 30-second ffmpeg transcode every few hours will not notice pinning

The gotcha I ran into: pinning a VM to cores 0-3 on a system where the Proxmox host was using core 0 for NVMe interrupt handling caused *more* latency variance than no pinning at all. Always check `cat /proc/irq/*/smp_affinity_list` after setting up pinning, and move storage and network IRQs off your isolated cores with `echo <cpu-mask> > /proc/irq/<n>/smp_affinity`.

## Conclusion

CPU pinning on Proxmox VE 9 is a 20-minute configuration change with immediate, measurable results for latency-sensitive workloads: `isolcpus` in GRUB, `cpuaffinity` in the VM config, and a `taskset` check to confirm. The logical next step is interrupt affinity — disable `irqbalance` and manually assign NVMe and PCIe interrupts to your reserved host cores, or run `irqbalance` with a `--banirq` list for your storage and network devices. That is where the last few microseconds of jitter typically hide on a well-tuned Proxmox host.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="cpu-pinning"/>
        <category label="numa"/>
        <category label="kvm"/>
        <category label="vm-performance"/>
        <category label="proxmox"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox LXC Bind Mounts: Share Host Paths with Containers]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-lxc-bind-mounts-host-storage/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-lxc-bind-mounts-host-storage/"/>
        <updated>2026-04-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Configure Proxmox LXC bind mounts to share host directories with containers, fix UID/GID mapping in unprivileged containers, and avoid permission pitfalls.]]></summary>
        <content type="html"><![CDATA[
Bind mounts let an LXC container read from and write to a directory that lives on the Proxmox host — same data, no copying, no NFS required. In under ten minutes you can have a container writing logs, media files, or database dumps directly to a host path you control. This guide covers the Proxmox VE 9.1 web UI method, the config-file method, and the UID/GID remapping issue that trips up almost everyone the first time they work with an unprivileged container.

## Key Takeaways

- **How it works**: A bind mount makes a host directory appear at a specific path inside the container — changes in either location are immediate and atomic.
- **UID/GID shift**: Unprivileged containers map container UID 0 → host UID 100000 by default; the host directory must be owned by the shifted UID or writes fail with permission errors.
- **Config syntax**: Bind mounts appear as `mp0`, `mp1`, etc. in `/etc/pve/lxc/<VMID>.conf` — for example `mp0: /host/path,mp=/container/path`.
- **Privileged containers skip the shift**: A privileged container avoids UID remapping but trades namespace isolation for convenience.
- **Exclude from backups**: Add `backup=0` to any large mount point entry to keep media libraries out of `vzdump` archives.

## What Are LXC Bind Mounts and When Do You Need Them

LXC containers share the Proxmox host kernel but are isolated by namespaces and cgroups. That makes them fast to start and cheap on RAM, and it also means they can safely access host directories if you configure mount points correctly.

You reach for a bind mount when:

- Multiple containers need access to the same dataset — a media library read by Jellyfin and written by Sonarr simultaneously
- You want application data outside the container rootfs so it survives `pct restore` or `pct destroy`
- You are running Docker inside an LXC container and want the Docker volume data on a host path you can snapshot — exactly the setup described in [Running Docker Inside LXC Containers on Proxmox](/articles/docker-inside-lxc-containers-proxmox/)
- You want to snapshot application data independently via ZFS without snapshotting the whole container disk image

A bind mount is **not** a copy. Write from the container, the host sees the change immediately. Delete a file from the host side, the container loses it. Plan your data layout before you start.

## How to Add a Bind Mount from the Proxmox UI

In Proxmox VE 9.1, mount points live in the container's **Resources** tab.

1. Select the container in the left pane, click **Resources** → **Add** → **Mount Point**.
2. Set **Storage** to `Directory` and enter the **Host Path** — an absolute path to an existing directory on the Proxmox host (e.g., `/mnt/data/media`).
3. Set **Mount Point** to the path where it should appear inside the container (e.g., `/media`).
4. Optionally tick **Read-only** to prevent container writes to the host path.
5. Click **Add**, then restart the container: **More** → **Reboot** or run `pct reboot <VMID>` from the shell.

> **Gotcha**: The UI will not create the host directory for you. If the path does not exist on disk, Proxmox will accept the config but the container will fail to start with a vague `lxc-start` error. Create it first:

```bash
mkdir -p /mnt/data/media
```

## Configuring Bind Mounts via the LXC Config File

For scripted deployments, editing the config directly is faster and easier to put under version control.

```bash
nano /etc/pve/lxc/101.conf
```

Add a mount point entry at the bottom:

```ini
mp0: /mnt/data/media,mp=/media
```

Multiple mount points use `mp0` through `mp9`:

```ini
mp0: /mnt/data/media,mp=/media
mp1: /mnt/data/config,mp=/config,ro=1
```

Full set of options supported in Proxmox VE 9.1:

| Option | Example | Effect |
|--------|---------|--------|
| `mp=` | `mp=/data` | Container-side mount path (required) |
| `ro=1` | `ro=1` | Mount read-only inside the container |
| `backup=0` | `backup=0` | Exclude this path from `vzdump` backups |
| `replicate=0` | `replicate=0` | Skip during ZFS or PBS replication |
| `shared=1` | `shared=1` | Mark as cluster-shared storage |

After editing the config, restart the container and verify the mount:

```bash
pct reboot 101
pct exec 101 -- df -h /media
```

## The UID/GID Remapping Problem in Unprivileged Containers

This is where almost everyone gets burned the first time. Unprivileged LXC containers use a UID/GID mapping defined in `/etc/subuid` and `/etc/subgid` on the host. Proxmox ships with:

```bash
cat /etc/subuid
# root:100000:65536
```

This means container UID 0 (root) maps to host UID 100000, container UID 1000 maps to host UID 101000, and so on. When a container process writes to a bind-mounted host directory, the host kernel sees host UID 101000, not UID 1000. The directory permission check happens against the shifted UID.

### Calculating the Mapped Host UID

```bash
# Inside the container, check the running user:
id
# uid=1000(ubuntu) gid=1000(ubuntu)

# On the host, that container UID maps to:
# host_uid = 100000 + container_uid = 101000
```

### Option 1: Chown the Host Directory to the Shifted UID

The cleanest fix — change ownership on the host to the mapped UID:

```bash
chown -R 101000:101000 /mnt/data/media
```

Container processes running as UID 1000 can now read and write the directory transparently, with no special config beyond the mount point entry itself.

### Option 2: Use POSIX ACLs for Shared Paths

ACLs are more surgical when multiple containers or host users need access to the same path:

```bash
# Install acl if not already present (Proxmox host is Debian-based)
apt install acl

# Grant the container's mapped UID read/write/execute access
setfacl -m u:101000:rwx /mnt/data/media

# Default ACL so new files and subdirectories inherit the rule
setfacl -d -m u:101000:rwx /mnt/data/media
```

### Option 3: Map Container Root to Host Root

For containers where the container's root user needs to own host files, add a custom UID map to the container config. This is a targeted override, not a global switch:

```ini
lxc.idmap: u 0 0 1
lxc.idmap: u 1 100001 65535
lxc.idmap: g 0 0 1
lxc.idmap: g 1 100001 65535
```

This maps container UID 0 to host UID 0 while keeping all other UIDs shifted. The host directory just needs to be owned by root:

```bash
chown root:root /mnt/data/special
chmod 755 /mnt/data/special
```

> **Security note**: Mapping container root to host root reduces namespace isolation — a compromised container root can affect any root-owned bind-mounted path on the host. This is acceptable for trusted internal workloads. For the broader security picture on LXC and Proxmox isolation, see [Hardening Proxmox VE: Firewall, fail2ban, and SSH Security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/).

## Bind Mounts in Privileged Containers

Privileged containers (`unprivileged: 0` in the config) have no UID remapping — container UID 1000 is host UID 1000. Standard Unix permissions apply directly:

```bash
chown -R 1000:1000 /mnt/data/media
```

Privileged containers are the simpler path when you are running Docker inside LXC. Docker's overlay2 storage driver needs real root access, so the LXC container must be privileged anyway. In that setup — like the Portainer and Dockge workflow described in [Managing Docker on Proxmox with Portainer and Dockge](/articles/managing-docker-on-proxmox-with-portainer-and-dockge/) — bind-mounting Docker volume directories from the host just works without any UID arithmetic.

## Real-World Configs That Work

### Media Library Shared Across Two Containers

Host ZFS dataset `/mnt/tank/media` mounted read-only into Jellyfin, read-write into Sonarr, with both excluded from `vzdump`:

```ini
# /etc/pve/lxc/200.conf  (Jellyfin — consumer)
mp0: /mnt/tank/media,mp=/media,ro=1,backup=0

# /etc/pve/lxc/201.conf  (Sonarr — writer)
mp0: /mnt/tank/media,mp=/media,backup=0
```

```bash
# Both containers run as UID 1000 internally
chown -R 101000:101000 /mnt/tank/media
```

### Docker Data Directory on a Host ZFS Dataset

Run Docker inside a privileged LXC but keep `/var/lib/docker` on a ZFS dataset you can snapshot independently:

```ini
# /etc/pve/lxc/300.conf  (privileged container: unprivileged: 0)
mp0: /mnt/ssd/docker-data,mp=/var/lib/docker
```

```bash
chown root:root /mnt/ssd/docker-data
chmod 710 /mnt/ssd/docker-data
```

Expect Docker to initialize its overlay2 storage driver the first time the container starts — about 10 to 15 seconds on a fresh ZFS dataset before the daemon comes up.

### Read-Only Config Injection

Manage application configs centrally on the host; containers pick up changes on next restart:

```ini
mp0: /mnt/configs/nginx,mp=/etc/nginx,ro=1
mp1: /mnt/configs/app,mp=/app/config,ro=1
```

This pattern works well for CI/CD pipelines where Ansible or a deploy script writes the host directory and container restarts pull in the new config.

## Troubleshooting Bind Mount Failures

**Container fails to start, errors in the journal**

```bash
journalctl -u pve-container@101.service --no-pager | tail -40
```

The most common cause is the host directory not existing or a typo in the config path. Verify:

```bash
ls -la /mnt/data/media
```

**Permission denied inside the container**

Check the effective ownership from the host:

```bash
ls -lan /mnt/data/media
# Owner UID should match 100000 + container_uid
```

Fix it:

```bash
chown -R 101000:101000 /mnt/data/media
```

**Files created inside the container have large UIDs when viewed from the host**

Expected behavior for unprivileged containers. UID 101000 on the host is UID 1000 inside the container. Use `chown` with the mapped UID when you need to manipulate these files from the host side.

**Mount not present after `pct restore`**

`pct restore` rebuilds the container config from the backup archive. Mount point entries added after the backup was taken are not included. Re-add the `mp` lines to the config manually, or ensure you capture the config file as part of your backup procedure. For a robust backup strategy that covers both containers and host datasets, [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) lays out a production-grade approach.

**`vzdump` backups are enormous**

A bind-mounted media library can balloon a container backup from 2 GB to 2 TB. Add `backup=0` to the mount point line:

```ini
mp0: /mnt/tank/media,mp=/media,ro=1,backup=0
```

Back up the host dataset separately via PBS or ZFS send.

## Conclusion

Bind mounts in Proxmox LXC are straightforward once you have the UID shift internalized: for unprivileged containers, `chown 101000:101000` on the host directory is the fix for nearly every permission error you will encounter. Add `mp0: /host/path,mp=/container/path` to the container config, restart, and verify with `pct exec`. The pattern scales cleanly to a dozen containers sharing the same ZFS datasets. Your next step: put those host directories on a ZFS dataset with hourly snapshots — five minutes of work that gives you point-in-time recovery for all your container data without touching the containers themselves.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="lxc"/>
        <category label="proxmox"/>
        <category label="bind-mounts"/>
        <category label="storage"/>
        <category label="containers"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Configure Proxmox Notifications for Email and Webhooks]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-notifications-email-webhooks/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-notifications-email-webhooks/"/>
        <updated>2026-04-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Configure Proxmox VE 8.1 notifications to route backup failures, HA events, and storage alerts to your inbox or webhooks like ntfy and Slack in 20 minutes.]]></summary>
        <content type="html"><![CDATA[
Proxmox VE 8.1 introduced a proper centralized notification framework — SMTP endpoints, webhook targets, and matcher-based routing all managed from one config file or the web UI. This guide walks you through setting up email alerts and HTTP webhooks so backup failures, HA events, and storage errors reach you the moment they happen, whether that's your inbox, Slack, or an ntfy topic on your phone.

## Key Takeaways

- **Requires PVE 8.1+**: The notification framework was introduced in Proxmox VE 8.1; earlier versions only support per-job email fields.
- **Two endpoint types**: SMTP for email and webhook for any HTTP POST target (ntfy, Gotify, Slack, Telegram).
- **Matchers control routing**: Filter by event type and severity — silence info noise and only page for actual errors.
- **Cluster-aware**: The config in `/etc/pve/notifications.cfg` replicates to all nodes automatically.
- **Always test first**: Fire a test notification before trusting backup alerts to actually land.

## How the Proxmox VE 8.1 Notification Framework Works

Before 8.1, notification setup meant copying an email address into every backup job and hoping sendmail worked. The new framework flips this: you define named **endpoints** (where to send) and **matchers** (what triggers a send), then the system routes events automatically.

Everything lives in `/etc/pve/notifications.cfg` on the Proxmox cluster filesystem. In a cluster, it replicates to all nodes the same way VM configs do. A skeleton config looks like this:

```ini
smtp: my-smtp
    server mail.example.com
    port 587
    mode starttls
    username alerts@example.com
    from-address alerts@example.com
    mailto admin@yourdomain.com

matcher: default-matcher
    target my-smtp
```

The framework ships with a built-in `mail-to-root` endpoint that uses the local `sendmail` binary. In most homelabs, Postfix isn't configured as an SMTP relay on the Proxmox host, so that target silently fails. The fix is replacing the default matcher's target with an explicit SMTP endpoint you control.

## Setting Up an SMTP Endpoint

Configure SMTP either in the web UI at **Datacenter → Notifications → Add → SMTP Endpoint**, or via `pvesh`:

```bash
pvesh create /cluster/notifications/endpoints/smtp \
  --name my-smtp \
  --server smtp.fastmail.com \
  --port 465 \
  --mode tls \
  --username me@fastmail.com \
  --password 'yourpassword' \
  --from-address proxmox@fastmail.com \
  --mailto admin@yourdomain.com
```

For Gmail, use `smtp.gmail.com`, port 587, `starttls` mode, and an **App Password** — Google has blocked regular credentials for SMTP since 2022, so your account password will not work here.

### TLS Mode Reference

| Mode | Port | Use When |
|------|------|----------|
| `starttls` | 587 | Standard submission; negotiates TLS after connect |
| `tls` | 465 | SMTPS; TLS from the first byte |
| `insecure` | 25 | Local relay only — never send credentials this way |

The `--mailto-user` flag accepts a Proxmox user ID such as `root@pam` and resolves the email address from that user's profile under **Datacenter → Permissions → Users**. The `--mailto` flag takes a raw email address directly. To notify multiple recipients, repeat the flag:

```bash
pvesh create /cluster/notifications/endpoints/smtp \
  --name team-smtp \
  --server smtp.example.com \
  --port 587 \
  --mode starttls \
  --username alerts@example.com \
  --password 'yourpassword' \
  --from-address proxmox@example.com \
  --mailto ops@company.com \
  --mailto oncall@company.com
```

If you run Proxmox Backup Server alongside Proxmox VE, PBS has its own notification framework configured separately in the PBS web UI. The concepts are identical but the config is a separate service — see [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) for the full PBS workflow including retention and offsite replication.

## Testing the SMTP Endpoint

Fire a test message before trusting this for production backup alerts:

```bash
pvesh create /cluster/notifications/endpoints/smtp/my-smtp/test
```

Expect the email within 60 seconds. If nothing arrives, check the log:

```bash
journalctl -u postfix --since "5 minutes ago"
```

The most common failure causes:

- **Port 25 blocked outbound** by your ISP or upstream router — use 587 or 465 instead
- **Wrong app password** — Gmail and Fastmail require a dedicated SMTP app password, not your account password
- **TLS mode mismatch** — flip between `starttls` and `tls` if you see TLS handshake errors in the log
- **Firewall blocking outbound SMTP** — if you run strict egress rules on the Proxmox host, you'll need to allow TCP 465 or 587 outbound; the [Hardening Proxmox VE: Firewall, fail2ban, and SSH Security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) guide covers how those egress rules are structured

## Adding a Webhook Endpoint

Webhook endpoints send an HTTP POST to any URL — ntfy, Gotify, Slack, Telegram bots, Mattermost, or a custom receiver. The webhook endpoint type has been available since Proxmox VE 8.1.

For **ntfy** (a lightweight push notification server that runs cleanly in an LXC container):

```bash
pvesh create /cluster/notifications/endpoints/webhook \
  --name ntfy-homelab \
  --url 'https://ntfy.example.com/proxmox-alerts' \
  --method POST \
  --header 'Authorization:Bearer your-ntfy-token' \
  --body '{"title":"{{title}}","message":"{{message}}"}'
```

For **Slack** via an incoming webhook URL:

```bash
pvesh create /cluster/notifications/endpoints/webhook \
  --name slack-ops \
  --url 'https://hooks.slack.com/services/T00000000/B00000000/XXXX' \
  --method POST \
  --body '{"text":"*{{title}}*\n{{message}}"}'
```

The body field supports Handlebars-style template variables:

- `{{title}}` — short event subject
- `{{message}}` — full event body
- `{{severity}}` — one of `info`, `notice`, `warning`, or `error`
- `{{timestamp}}` — Unix epoch seconds

## Configuring Matchers to Route Alerts

A matcher without filters is a catch-all — it sends every event to its target:

```bash
pvesh create /cluster/notifications/matchers \
  --name catch-all \
  --target my-smtp
```

Add `--filter-severity` to cut down on noise. This matcher sends only errors and warnings to the webhook:

```bash
pvesh create /cluster/notifications/matchers \
  --name critical-to-ntfy \
  --target ntfy-homelab \
  --filter-severity '["error","warning"]'
```

Filter by event type with `--filter-type` to catch only backup job results:

```bash
pvesh create /cluster/notifications/matchers \
  --name backup-alerts \
  --target my-smtp \
  --filter-type '["vzdump"]' \
  --filter-severity '["error","warning"]'
```

Multiple matchers are independent — an error event can match both a `filter-severity error` webhook matcher and a catch-all email matcher, and both will fire. There is no stop-processing rule.

## What Events Actually Generate Notifications

This catches people off guard: the notification framework only fires for system-initiated events, not manual UI actions.

| Event Type | What Triggers It | Severity |
|------------|-----------------|----------|
| `vzdump` | Backup job finishes (success, fail, or warning) | `info` / `error` / `warning` |
| `replication` | Storage replication job completes or fails | `info` or `error` |
| `package-updates` | Weekly update scan finds available packages | `notice` |
| `fencing` | HA manager fences a node | `error` |
| `cluster` | Node join/leave or quorum change | `warning` or `error` |

Manual operations — cloning a VM, live migration, snapshot creation, moving a disk between pools — do **not** generate notifications. Expecting a ping when you hand-migrate a VM and getting silence is the most common "is this broken?" moment for new users. It's not broken.

## A Complete Working Config

Here is the exact `/etc/pve/notifications.cfg` running on a three-node Proxmox VE 9.1 cluster. Email gets everything; ntfy only gets errors, so my phone buzzes for actual problems and not for 50 successful nightly backup completions:

```ini
smtp: fastmail
    server smtp.fastmail.com
    port 465
    mode tls
    username me@fastmail.com
    from-address proxmox@fastmail.com
    mailto me@fastmail.com
    comment Email catch-all

webhook: ntfy
    url https://ntfy.sh/my-private-topic-xxxxxx
    method POST
    header Authorization:Bearer ntfy-token-here
    body {"title":"{{title}}","message":"{{message}}"}
    comment Phone push for errors only

matcher: errors-to-phone
    target ntfy
    filter-severity error

matcher: everything-to-email
    target fastmail
    comment Catch-all
```

Edit this file directly for bulk changes. No service restart is needed — Proxmox reads it on demand. The file is plain text, and version-controlling it alongside your other infrastructure configs is straightforward.

### What About the Default mail-to-root Target?

Proxmox ships with a built-in `mail-to-root` SMTP endpoint and a `default-matcher` that points to it. If `sendmail` isn't configured on your Proxmox host — which is true for most homelab installations — this silently fails. Go to **Datacenter → Notifications**, edit the `default-matcher`, and point it to your named SMTP endpoint instead.

## Migrating from Per-Job Email to Matchers

If you set email addresses directly on vzdump jobs before Proxmox VE 8.1, those inline fields still work for backward compatibility. But to get matcher routing and webhooks, you need to clear them. Check what you have first:

```bash
grep -r "mailto" /etc/pve/jobs.cfg
```

For each job with an inline `mailto`, clear the field under **Datacenter → Backup → Edit**. For a large number of jobs, edit `/etc/pve/jobs.cfg` directly to strip `mailto` lines, then verify none remain:

```bash
pvesh get /cluster/jobs/vzdump
```

Confirm no returned jobs carry a `mailto` field before relying on matchers alone.

## Conclusion

With a named SMTP endpoint and a couple of matchers in `/etc/pve/notifications.cfg`, Proxmox VE 9.1 reliably delivers backup failures, HA fencing events, and replication errors to your inbox or phone — no per-job configuration needed. The twenty-minute setup pays off the first time a 3 AM backup failure lands in your inbox before your users notice. The logical refinement from here: add `filter-severity error` to your webhook matcher so only genuine failures wake you up, and let the daily info-level noise stay in email. If you haven't scheduled automated backup jobs yet, [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) covers everything from retention policies to off-site replication.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="proxmox-notifications"/>
        <category label="smtp"/>
        <category label="webhooks"/>
        <category label="ntfy"/>
        <category label="alerting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Add NFS Storage to Proxmox for VM Disks and Backups]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-nfs-storage-vm-disks-backups/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-nfs-storage-vm-disks-backups/"/>
        <updated>2026-04-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Add NFS storage to Proxmox VE for VM disk images, ISO libraries, and VZDump backups. Covers TrueNAS export config, mount options, and the root_squash permission fix.]]></summary>
        <content type="html"><![CDATA[
Adding NFS storage to Proxmox VE takes about five minutes from the web UI, and once attached, every node in your cluster can use the same share for VM disk images, ISO libraries, container templates, and backup archives — no per-node configuration needed. This guide walks through the complete process: exporting a share from TrueNAS SCALE or a Debian NFS server, attaching it in Proxmox VE 9.1, choosing mount options that actually improve performance, and avoiding the permission pitfalls that trip up almost everyone on first attempt.

## Key Takeaways

- **Cluster-wide access**: NFS storage added under Datacenter → Storage is mounted on all cluster nodes automatically — add it once, use it everywhere.
- **Best fit**: ISO libraries, container templates, and VZDump backup archives — not the primary disk for write-heavy database VMs.
- **root_squash is the biggest gotcha**: Most NAS devices enable root_squash by default, which blocks Proxmox from writing disk images as root. Disable it on the export.
- **Performance tip**: Adding `nconnect=4` to mount options delivers a 30–50% throughput increase on 10 GbE without any other changes.
- **Content types**: A single NFS share can simultaneously serve disk images, ISO files, LXC templates, backups, and cloud-init snippets.

## When NFS Makes Sense for Proxmox Storage

NFS is not the right answer for every storage use case. Being clear about this upfront saves you a painful storage migration later.

**Use NFS when:**
- You already have a TrueNAS, Synology, or QNAP NAS with spare capacity and a dedicated storage network or 10 GbE link
- You want a centralized ISO and template library shared across multiple Proxmox nodes without copying files manually to each
- You need a cost-effective backup target for VZDump archives
- Your workloads are sequential or low-IOPS — Home Assistant, lightweight web services, media libraries

**Avoid NFS for:**
- Databases (PostgreSQL, MySQL) or any workload with heavy random 4K I/O — network round-trip latency kills IOPS; use local NVMe or Ceph RBD instead
- VMs that need fast live migration — NFS migration works, but local NVMe-to-NVMe migration completes in under two minutes for a 50 GB disk; NFS over 1 GbE can take fifteen minutes for the same operation
- Workloads that depend on ZFS-native send/recv replication — once data is on NFS, those features belong to the NAS, not Proxmox

If you are designing a full homelab storage layout, [Build a Private Cloud at Home with Proxmox VE](/articles/build-private-cloud-home-proxmox-ve/) covers how to layer NFS alongside local ZFS pools for a balanced setup.

## Setting Up the NFS Export

### TrueNAS SCALE (Dragonfish 24.10 or later)

In the TrueNAS web UI:

1. Go to **Shares → NFS → Add**
2. Set the **Path** to your dataset (e.g., `/mnt/tank/proxmox-nfs`)
3. Under **Advanced Options**, add a network entry for your Proxmox subnet (e.g., `192.168.10.0/24`)
4. Uncheck **Enable Root Squash** — Proxmox must write as root for disk image operations
5. Save and confirm the NFS service is running under **Services → NFS**

Verify the export is visible from a Proxmox node:

```bash
showmount -e 192.168.10.50
```

Expected output:

```
Export list for 192.168.10.50:
/mnt/tank/proxmox-nfs 192.168.10.0/24
```

### Debian 12 or Ubuntu NFS Server

If you are running a DIY NFS server:

```bash
apt install nfs-kernel-server
```

Edit `/etc/exports`:

```bash
/srv/proxmox-nfs 192.168.10.0/24(rw,sync,no_root_squash,no_subtree_check)
```

Apply the changes:

```bash
exportfs -rav
systemctl restart nfs-kernel-server
```

Always include `no_subtree_check` — it eliminates a performance-killing consistency check that fires on every file access when subtree checking is enabled.

For production environments, put NFS traffic on a dedicated storage VLAN to isolate backup and ISO transfer load from your management network. [Configuring VLANs on Proxmox with Linux Bridges](/articles/configure-vlans-proxmox-linux-bridges/) covers that setup in full if you have not done it yet.

## How to Add NFS Storage in the Proxmox Web UI

1. Log into the Proxmox web UI at `https://<node-ip>:8006`
2. Navigate to **Datacenter → Storage → Add → NFS**
3. Fill in the form:
   - **ID**: A short identifier with no spaces (e.g., `nas-proxmox`)
   - **Server**: The NAS IP or hostname (e.g., `192.168.10.50`)
   - **Export**: Click the dropdown — Proxmox runs `showmount` against the server and lists available exports automatically
   - **Content**: Check all types this share will serve: `Disk image`, `ISO image`, `Container template`, `VZDump backup file`, `Snippets`
   - **Max Backups**: Set a per-VM retention limit if using this as a VZDump target
4. Click **Add**

The share mounts on all cluster nodes within seconds. Check the **Tasks** pane at the bottom of the UI to confirm there are no mount errors before creating any VMs against the new storage.

### What the Content Types Actually Do

| Content Type | File Format | Typical Use |
|---|---|---|
| Disk image | `.raw`, `.qcow2` | VM disk images |
| ISO image | `.iso` | OS install media |
| Container template | `.tar.zst` | LXC base images |
| VZDump backup file | `.vma.zst` | VM and container backups |
| Snippets | YAML/JSON | Cloud-init user-data configs |

## How to Add NFS Storage via the CLI

The `pvesm` tool is the right approach when scripting Proxmox node setup or managing storage from Ansible:

```bash
pvesm add nfs nas-proxmox \
  --server 192.168.10.50 \
  --export /mnt/tank/proxmox-nfs \
  --content images,iso,vztmpl,backup,snippets \
  --options vers=4.1
```

Verify the storage was added and check capacity:

```bash
pvesm status
```

To list the contents of the storage:

```bash
pvesm list nas-proxmox
```

Proxmox writes the storage definition to `/etc/pve/storage.cfg`, which `pmxcfs` replicates to all cluster nodes:

```ini
nfs: nas-proxmox
	path /mnt/pve/nas-proxmox
	server 192.168.10.50
	export /mnt/tank/proxmox-nfs
	content images,iso,vztmpl,backup,snippets
	options vers=4.1
```

## NFS Mount Options That Actually Improve Performance

Proxmox passes mount options directly through the `options` field. These are the ones worth setting:

```bash
pvesm set nas-proxmox --options vers=4.1,hard,timeo=600,retrans=2,nconnect=4
```

| Option | Effect | Recommendation |
|---|---|---|
| `vers=4.1` | Forces NFSv4.1 with session trunking | Always prefer v4.1 over v3 for cluster use |
| `hard` | Retries indefinitely if the server becomes unreachable | Required for VM disks — `soft` will corrupt data |
| `timeo=600` | 60-second timeout before retry (units: 0.1 second) | Increase on networks with occasional latency spikes |
| `retrans=2` | Retries before reporting an error | 2 is fine on stable LANs; default is 3 |
| `nconnect=4` | Opens 4 parallel TCP connections to the NFS server | 30–50% throughput increase on 10 GbE (kernel 5.15+) |
| `noatime` | Skips access-time write on reads | Minor write reduction on ISO and backup shares |

`nconnect=4` is the highest-impact single option if you are on 10 GbE. Benchmark before and after with:

```bash
dd if=/dev/zero of=/mnt/pve/nas-proxmox/test.img bs=1M count=1024 oflag=direct
rm /mnt/pve/nas-proxmox/test.img
```

On a TrueNAS SCALE system with an NVMe-backed pool and a direct 10 GbE connection, `nconnect=4` typically moves sequential write throughput from around 350 MB/s to 700–900 MB/s. On 1 GbE, the difference is negligible — the link is already saturated long before the connection count matters.

## Using NFS as a Proxmox Backup Target

Once NFS storage is added with the `backup` content type enabled, pointing a backup job at it is straightforward:

1. Go to **Datacenter → Backup → Add**
2. Set **Storage** to `nas-proxmox`
3. Choose your **Schedule** (daily, weekly, or a cron expression)
4. Configure **Retention** (keep last N backups per VM)
5. Save — Proxmox handles the rest, writing `.vma.zst` archives directly to the NFS share

For deduplication, encryption, and server-side integrity verification, [Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) is the better tool — it can also use an NFS share as its datastore backing, though local NVMe or a ZFS dataset gives better PBS performance for dedup index operations.

A practical combination that works well: VZDump to NFS for rapid daily snapshots, and PBS on a separate host for deduplicated, encrypted long-term retention with offsite replication.

## Common NFS Gotchas on Proxmox

### root_squash Blocks Disk Image Creation

This is the most common first-time issue. If you get a `Permission denied` error when creating a VM disk on NFS storage, the export is almost certainly using `root_squash`. Proxmox writes disk images as root; `root_squash` maps root to `nobody`, which has no write access.

Fix on TrueNAS: uncheck **Enable Root Squash** in the NFS share settings.
Fix on Linux: change `root_squash` to `no_root_squash` in `/etc/exports` and run:

```bash
exportfs -ra
```

### NFSv3 File Locking in a Cluster

NFSv3 uses a separate statd/lockd protocol for file locking. Under high concurrency — two Proxmox nodes creating disk images simultaneously — stale lock files can accumulate and cause `flock` failures. NFSv4.1 (`vers=4.1`) handles locking natively and eliminates this class of problem entirely.

### NFS Storage Shows as Unavailable After a NAS Reboot

Proxmox marks NFS storage unavailable if the mount times out during node boot. After the NAS comes back online, re-trigger the mount:

```bash
mount /mnt/pve/nas-proxmox
```

Or rescan via the UI: **Datacenter → Storage → nas-proxmox → More → Rescan**.

If your NAS boots slower than your Proxmox nodes, add `_netdev` to the mount options so the kernel waits for network availability before attempting the mount.

### QCOW2 and LXC Container Root Filesystems

QCOW2 disk images on NFS work fine for KVM VMs, but LXC containers cannot use QCOW2 for their root filesystems — LXC needs raw block devices or directory-backed storage. If you want LXC container data on NFS, use the `dir` storage plugin pointing at the NFS mountpoint rather than the native `nfs` plugin. This distinction is worth knowing before you try to migrate a container and get an unexpected error.

### ISO Upload Permissions

When uploading an ISO through the web UI, Proxmox writes it as `www-data` (UID 33). If you also mount the same NFS share from another client and try to delete or modify those files directly, you will hit permission errors. The clean rule: manage ISO files exclusively through the Proxmox UI or `pvesm` — do not mix access methods on the same export.

## Conclusion

NFS is the lowest-friction way to give every Proxmox cluster node shared access to ISO libraries, container templates, and VZDump archives without deploying Ceph. Add the storage once at the Datacenter level, set `no_root_squash` on the export, and include `vers=4.1,hard,nconnect=4` in your mount options for solid performance on modern hardware. The natural next step is setting up a scheduled VZDump job targeting this storage, then layering [Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) on top for deduplication and encryption.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="nfs"/>
        <category label="storage"/>
        <category label="truenas"/>
        <category label="backup"/>
        <category label="cluster-storage"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Migrate Hyper-V VMs to Proxmox VE Step by Step]]></title>
        <id>https://proxmoxpulse.com/articles/migrate-hyper-v-vms-proxmox-ve/</id>
        <link href="https://proxmoxpulse.com/articles/migrate-hyper-v-vms-proxmox-ve/"/>
        <updated>2026-04-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Export Hyper-V VMs, convert VHDX to qcow2, and import into Proxmox VE 9 without reinstalling. Covers VirtIO drivers, Gen 2 UEFI VMs, and Windows activation.]]></summary>
        <content type="html"><![CDATA[
Migrating from Hyper-V to Proxmox doesn't require starting from scratch. Export your VMs from Hyper-V, convert the VHDX disk images with `qemu-img`, import them using `qm importdisk`, and boot. Most Linux guests come up on the first try; Windows VMs need VirtIO drivers and occasionally a licensing touchup. By the end of this guide you'll have your first Hyper-V workload running on Proxmox VE 9 with no data loss and no OS reinstall.

## Key Takeaways

- **Export format**: Hyper-V uses VHDX; Proxmox works with raw or qcow2 — `qemu-img convert` handles the translation.
- **Linux guests**: Boot cleanly with no driver changes; switching to virtio-net is optional but improves throughput.
- **Windows guests**: Require VirtIO drivers post-import; use the Fedora VirtIO ISO to install them inside the running guest.
- **Snapshots**: Merge all Hyper-V checkpoints before export — the avhd/avhdx chain will break the VHDX if left intact.
- **Generation 2 VMs**: Use UEFI; match the firmware type when creating the Proxmox VM shell or the bootloader won't find the disk.

## Why Hyper-V Admins Are Moving to Proxmox

The calculus has shifted. Running Hyper-V on bare metal still requires a Windows Server host OS, which means CALs, activation, and Windows Update on your hypervisor. Proxmox VE runs Debian under the hood, has no per-VM licensing, and gives you KVM virtualization plus LXC containers from a single web UI — no Microsoft dependency anywhere in the stack.

For mixed workloads, the integration model is tighter too. Running containers directly alongside VMs without nested virtualization is a significant operational improvement. [Running Docker Inside LXC Containers on Proxmox](/articles/docker-inside-lxc-containers-proxmox/) shows what that looks like in practice, and it's one of the first things you'll want to set up after your VMs are migrated.

## What You Need Before You Start

Before touching anything in production, confirm you have:

- **Proxmox VE 9** installed and reachable. If you're starting fresh, [How to Install Proxmox VE on Any Hardware](/articles/install-proxmox-ve-on-any-hardware/) covers the full install from ISO to first login.
- **Disk space for exports**: Hyper-V allocates full VHDX capacity on export — a 500 GB dynamic disk can export as up to 500 GB even if only 80 GB is used. Plan accordingly.
- **`qemu-img`** on a Linux machine (or WSL2 on Windows). On Proxmox itself, it's pre-installed. On Debian/Ubuntu: `sudo apt install qemu-utils`.
- **A file transfer path** from your Hyper-V host to the Proxmox node — SSH/scp, a shared NFS mount, or an external USB drive all work.
- **The VirtIO ISO** if you're migrating Windows guests. Download details are below.

## How to Export VMs from Hyper-V

### Merge Snapshots First

This is the step most people skip and then regret. If your VM has checkpoints, they live as a chain of `.avhd` or `.avhdx` differencing disks alongside the base `.vhdx`. Exporting without merging produces an incomplete or corrupted image.

In Hyper-V Manager: right-click the VM → **Checkpoints** → delete all of them. Hyper-V merges the chain automatically when you delete — give it a few minutes per checkpoint on large disks. Once the checkpoint tree is empty, proceed with export.

### Export via Hyper-V Manager

Right-click the VM → **Export** → choose a destination folder. Hyper-V writes a folder structure with:

- `Virtual Machines/` — the VM config XML
- `Virtual Hard Disks/` — the `.vhdx` disk files
- `Snapshots/` — should be empty after the merge step

### Export via PowerShell

```powershell
Export-VM -Name "web-server-01" -Path "D:\HyperV-Exports\web-server-01"
```

For bulk exports across all VMs on the host:

```powershell
Get-VM | Export-VM -Path "D:\HyperV-Exports"
```

The export pauses disk I/O briefly for consistency. I recommend a clean shutdown for planned migrations rather than a live export — dirty exports are for disaster recovery, not deliberate moves.

## Converting VHDX Disks to qcow2

Proxmox natively imports raw and qcow2 disk formats. qcow2 is the better choice: it supports thin provisioning (empty sectors don't waste space), snapshots, and live Proxmox Backup Server backups.

### Convert on the Proxmox Node Directly

Copy the VHDX file to the Proxmox node first:

```bash
scp "user@hyperv-host:D:/HyperV-Exports/web-server-01/Virtual Hard Disks/web-server-01.vhdx" \
  root@proxmox:/tmp/
```

Then convert it on the node:

```bash
qemu-img convert -f vhdx -O qcow2 -p /tmp/web-server-01.vhdx /tmp/web-server-01.qcow2
```

The `-p` flag shows a progress bar. A 100 GB VHDX with 60 GB of actual data takes around 3–5 minutes on a SATA SSD. NVMe-to-NVMe cuts that to under two minutes. Verify the result:

```bash
qemu-img info /tmp/web-server-01.qcow2
```

You'll see `virtual size` (declared disk size) and `disk size` (actual space on disk) — the latter should match your used data, not the full VHDX allocation.

### Convert on Windows via WSL2

If moving the VHDX to Linux first isn't practical, WSL2 runs `qemu-img` directly:

```bash
# Inside WSL2 (Ubuntu 24.04)
sudo apt update && sudo apt install -y qemu-utils

qemu-img convert -f vhdx -O qcow2 -p \
  "/mnt/d/HyperV-Exports/web-server-01/Virtual Hard Disks/web-server-01.vhdx" \
  /tmp/web-server-01.qcow2
```

WSL2's I/O translation layer adds roughly 30–40% more time compared to native Linux. Fine for a one-time migration, slow for ten VMs.

## How to Import the VM into Proxmox

### Step 1: Create the VM Shell

In the Proxmox web UI, create a new VM and **uncheck "Add disk"** during the wizard — you're importing a disk, not creating one. Set these fields based on the Hyper-V generation:

| Setting | Linux Guest | Windows Gen 1 | Windows Gen 2 |
|---|---|---|---|
| OS Type | Linux 6.x | Windows 11/2022 | Windows 11/2022 |
| Machine type | q35 | q35 | q35 |
| BIOS | SeaBIOS | SeaBIOS | OVMF (UEFI) |
| SCSI controller | VirtIO SCSI | VirtIO SCSI | VirtIO SCSI |
| EFI disk | No | No | Yes |

For UEFI (Gen 2) Windows VMs, also check **Add EFI disk** — Proxmox needs this to store NVRAM variables including Secure Boot state. Note the VM ID the wizard assigns; we'll call it `101`.

### Step 2: Import the Disk

```bash
qm importdisk 101 /tmp/web-server-01.qcow2 local-lvm --format qcow2
```

Replace `local-lvm` with your actual storage pool name. Check what's available:

```bash
pvesm status
```

On a ZFS-based setup, use `local-zfs`. After the import completes you'll see output like:

```
Successfully imported disk as 'unused0:local-lvm:vm-101-disk-0'
```

### Step 3: Attach the Disk and Set Boot Order

In the web UI: **VM 101 → Hardware → unused0** → click **Edit** → set the bus:

- **VirtIO Block** for Linux guests
- **SCSI** (with the VirtIO SCSI controller already configured) for Windows guests

Then configure boot order: **Options → Boot Order** → enable the new disk and move it to first position.

### Step 4: Add a Network Adapter

Hyper-V virtual NICs don't carry over. Add one in **Hardware → Add → Network Device**:

- **VirtIO (paravirtualized)** for Linux guests
- **E1000** for Windows guests *initially* — switch to VirtIO after installing drivers inside the guest

Assign it to the correct Proxmox bridge (`vmbr0` for your LAN, or whichever bridge serves that network).

## Fixing Windows Guests After Import

### The BSOD Problem and How to Avoid It

If you import with a VirtIO disk and boot without drivers, Windows will bluescreen immediately with `INACCESSIBLE_BOOT_DEVICE`. Two ways around it:

**Safe path**: Import the disk as SCSI with the `lsi` controller type. Windows has inbox drivers for it, so the VM boots. Install VirtIO drivers from inside the running guest, then switch the disk and NIC to VirtIO.

**Fast path**: Mount the VirtIO ISO as a second CD-ROM before the first boot. When Windows hits the BSOD and reboots into WinRE, use the recovery console to load the VirtIO storage driver from the ISO, then boot normally.

Download the VirtIO ISO directly to Proxmox:

```bash
wget -P /var/lib/vz/template/iso/ \
  https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
```

### Installing VirtIO Drivers Inside the Guest

Once Windows is running, open **Device Manager** and point it at the mounted ISO. Install from these subdirectories (adjust for your Windows version):

```
vioscsi\w11\amd64\    → VirtIO SCSI storage driver (Windows 11)
NetKVM\w11\amd64\     → VirtIO network adapter
Balloon\w11\amd64\    → Memory balloon driver
qxl\w11\amd64\        → QXL display (optional, improves VNC performance)
```

For Windows Server 2022, use `2k22\amd64` in place of `w11\amd64`. After installing the storage and network drivers, shut down the VM, change the disk bus to **VirtIO Block** in Proxmox hardware, and change the NIC to **VirtIO**. It will come up on VirtIO from that point forward.

### Windows Activation After Migration

Expect deactivation. The virtual BIOS fingerprint changed when you moved from Hyper-V to KVM, and Windows ties its activation state to that fingerprint. For volume or MAK licenses:

```powershell
slmgr /ato
```

For UEFI OEM keys embedded in physical hardware, the key is tied to that machine's firmware — it won't transfer to a VM. Plan for this before migration day: either apply a volume key, set up a KMS server, or purchase a new license for the migrated instance.

## Networking and Storage Equivalents

Hyper-V virtual switches don't map directly to Proxmox, but the translation is straightforward:

| Hyper-V | Proxmox Equivalent |
|---|---|
| External virtual switch | Linux bridge (`vmbr0`) with physical NIC uplink |
| Internal virtual switch | Linux bridge without uplink |
| Private virtual switch | Isolated Linux bridge, no uplink |
| VLAN tagging on NIC | VLAN tag field on the VM network device |
| Storage Spaces mirror | ZFS mirror pool or LVM-thin |
| Hyper-V Replica | Proxmox Backup Server replication |

If your Hyper-V VMs ran on VLANs, you'll need to recreate the VLAN config on Proxmox bridges before the VMs come online — otherwise the guests will boot into a black-hole network segment. [Configuring VLANs on Proxmox with Linux Bridges](/articles/configure-vlans-proxmox-linux-bridges/) has the full bridge and VLAN tag configuration walkthrough.

## Common Pitfalls

**Secure Boot violations**: Gen 2 Hyper-V VMs use Secure Boot. Proxmox's OVMF supports it but leaves it disabled by default. If a Windows VM won't boot and shows a Secure Boot error in the VNC console, press **Del** at the OVMF splash screen, navigate to **Device Manager → Secure Boot Configuration**, and either disable Secure Boot or enroll the Microsoft certificate database.

**Dynamic Memory disappears**: Hyper-V's Dynamic Memory doesn't automatically translate to KVM's balloon driver. After installing VirtIO drivers, enable the balloon device in Proxmox (**Hardware → Add → VirtIO Balloon**). Without it, whatever RAM you set at VM creation is fixed — the guest can't give memory back.

**Time sync drift**: Hyper-V uses its own enlightenment-based time sync, which disappears after migration. Windows guests will fall back to Windows Time Service syncing from the host. Make sure your Proxmox node is configured to sync from a reliable NTP source so guests inherit accurate time.

**Generation 1 vs Generation 2 mismatch**: Gen 1 is MBR + legacy BIOS. Gen 2 is GPT + UEFI. If you create the Proxmox VM shell with SeaBIOS but the original was Gen 2, the bootloader won't find the disk. Check the Hyper-V VM's **Firmware** settings before you export — it's listed right there.

## Validating Before Cutover

Don't update DNS or decommission the Hyper-V VM until you've confirmed:

- The VM boots to login without errors in the VNC console
- Network works: ping the gateway, ping `1.1.1.1`, test internal DNS
- Application checks pass: database responds, web service returns HTTP 200, scheduled tasks run
- Backup is configured and tested — [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) shows how to schedule and verify backups before you call the migration done

Keep the Hyper-V export on disk for at least one week after cutover. Disk space is cheap; an emergency rollback that takes 10 minutes beats one that takes 10 hours.

## Conclusion

The migration from Hyper-V to Proxmox comes down to three commands — `Export-VM`, `qemu-img convert`, and `qm importdisk` — with most of the elapsed time spent on disk I/O rather than configuration. Linux VMs typically just boot; Windows VMs need an extra 30 minutes for VirtIO drivers and a licensing check. Once the first workload is running on Proxmox, set up Proxmox Backup Server for the migrated VMs before moving the next one — that's the right order of operations, not an afterthought.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="hyper-v"/>
        <category label="vm-migration"/>
        <category label="vhdx"/>
        <category label="qemu-img"/>
        <category label="virtio"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox Let's Encrypt ACME Certificate Setup Guide]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-lets-encrypt-acme-certificate/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-lets-encrypt-acme-certificate/"/>
        <updated>2026-04-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up free Let's Encrypt SSL certificates on Proxmox VE using the built-in ACME client. Works for homelab hosts using DNS challenge — no public IP needed.]]></summary>
        <content type="html"><![CDATA[
The browser "Your connection is not private" warning on your Proxmox web UI is more than visual friction — every modern browser suppresses password autofill on untrusted origins, and if you're accessing your node through Tailscale or a reverse proxy, broken certificate validation creates real operational headaches. Proxmox VE 9.x ships a full ACME client built into both the web UI and CLI. In under 15 minutes, you can have a free, auto-renewing Let's Encrypt certificate on any Proxmox node — including homelab hosts with no public IP — using the DNS-01 challenge. This guide walks through the setup with Cloudflare as the DNS provider, but the same steps apply to AWS Route 53, Hetzner, DigitalOcean, and the 30+ other providers Proxmox ships plugins for.

## Key Takeaways

- **Built-in ACME client**: No certbot or acme.sh needed — Proxmox VE has included its own ACME client since version 6.2.
- **DNS-01 for homelabs**: If your Proxmox host isn't publicly reachable on port 80, DNS-01 proves domain ownership through your DNS provider's API instead.
- **Scoped API token**: Create a Cloudflare token with `Zone:DNS:Edit` permission only — do not use the global API key.
- **Auto-renewal**: The `pve-daily-update.timer` systemd unit renews certificates automatically when fewer than 30 days remain.
- **Per-node in clusters**: Each cluster node needs its own ACME configuration — there is no cluster-wide certificate push.

## Why the Default Self-Signed Certificate Is a Real Problem

Proxmox generates a self-signed certificate at install time using a local CA. Every browser flags it as untrusted, which means:

- Chrome and Firefox suppress password autofill on `https://` pages with cert errors
- Browser extensions and API clients refuse connections that fail certificate validation
- You train yourself to click through security warnings — exactly the reflex that [Proxmox firewall and SSH hardening](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) is designed to eliminate

A valid certificate fixes all of this and costs nothing beyond owning a domain.

## Prerequisites

Before starting, confirm three things:

1. You own a domain managed through a supported DNS provider (list available at `Datacenter > ACME > Challenge Plugins`)
2. Your Proxmox node has outbound internet access to `acme-v02.api.letsencrypt.org` on port 443
3. Your node's hostname is a fully qualified domain name

Check the current hostname:

```bash
hostname --fqdn
```

If the result is just `pve` with no domain suffix, fix it before proceeding. The [Proxmox installation guide](/articles/install-proxmox-ve-on-any-hardware/) covers hostname configuration as part of its initial setup checklist, but the quick fix is:

```bash
hostnamectl set-hostname pve.yourdomain.com
```

Then update `/etc/hosts` so the node's IP maps to the FQDN:

```ini
192.168.1.10  pve.yourdomain.com pve
```

## How to Register an ACME Account

Proxmox's ACME client needs a Let's Encrypt account to issue certificates. Register once per node.

**Via the Web UI**:

1. Navigate to `Datacenter > ACME`
2. Under "Accounts", click **Add**
3. Enter your email address and accept the Terms of Service
4. Select the **Staging** directory first — it has no rate limits and lets you validate the full flow without burning production quota

**Via the CLI**:

```bash
# Staging — use this first
pvenode acme account register staging your@email.com \
  --directory https://acme-staging-v02.api.letsencrypt.org/directory

# Production — switch to this after staging succeeds
pvenode acme account register default your@email.com \
  --directory https://acme-v02.api.letsencrypt.org/directory
```

List registered accounts at any point:

```bash
pvenode acme account list
```

## HTTP-01 vs DNS-01: Which Challenge Type to Use

| Challenge | Requirement | Best for |
|-----------|-------------|----------|
| HTTP-01 | Port 80 publicly reachable on your domain's IP | Public-facing hosts |
| DNS-01 | API access to your DNS provider | Homelabs, private IPs, wildcards |
| TLS-ALPN-01 | Port 443 publicly reachable | Rarely needed with Proxmox |

For a typical homelab Proxmox node, DNS-01 is the right call. Your host stays completely internal — only the Proxmox node needs outbound HTTPS access to Let's Encrypt's servers and your DNS provider's API. No inbound port forwarding required.

## Setting Up the Cloudflare DNS Plugin

First, create a scoped API token in Cloudflare:

1. Log into Cloudflare > **My Profile > API Tokens > Create Token**
2. Select the **Edit zone DNS** template
3. Scope it to your specific zone (domain) — not "All zones"
4. Copy the token immediately — it won't be shown again

Add the plugin in Proxmox:

**Web UI**: `Datacenter > ACME > Challenge Plugins > Add`
- Plugin ID: `cloudflare`
- Plugin type: `Cloudflare Managed DNS`
- API Token: paste your token

**CLI**:

```bash
pvenode acme plugin add dns cloudflare \
  --api cf \
  --data "CF_Token=your_cloudflare_api_token_here"
```

Verify the plugin was saved:

```bash
pvenode acme plugin list
```

## Configuring the Domain on Your Node

Attach a domain and the DNS plugin to your node's certificate configuration.

**Web UI**: `Node > Certificates > ACME > Add`
- Domain: `pve.yourdomain.com`
- Challenge type: `DNS`
- Plugin: `cloudflare`

**CLI**:

```bash
# Set the ACME account for this node
pvenode config set --acme "account=default"

# Set the domain with the DNS plugin
pvenode config set --acmedomain0 "pve.yourdomain.com,plugin=cloudflare"
```

Verify the config was written correctly:

```bash
grep -E "^acme" /etc/pve/nodes/$(hostname)/config
```

Expected output:

```ini
acme: account=default
acmedomain0: pve.yourdomain.com,plugin=cloudflare
```

## Ordering the Certificate

With the account and domain configured, request the certificate:

**Web UI**: `Node > Certificates > ACME` > **Order Certificates Now**

A task log shows the challenge flow in real time. DNS-01 validation against Cloudflare typically takes 30-60 seconds — Cloudflare's API propagation is fast enough that the challenge succeeds on the first validation attempt.

**CLI**:

```bash
pvenode acme cert order
```

If you're using the staging account, the certificate issuer will be "Fake LE Intermediate X1" — that's correct. Once staging succeeds, switch to the production account and re-order:

```bash
pvenode config set --acme "account=default"
pvenode acme cert order --force
```

After a successful order, the Proxmox web UI immediately starts serving the new certificate. Reload your browser — the padlock should now show a valid issuer.

## How Auto-Renewal Works

Proxmox does not use a separate cron job for certificate renewal. The `pve-daily-update.service` systemd unit runs once per day and checks whether any node certificates expire within 30 days. If they do, it renews automatically via the same ACME config.

Check the timer status:

```bash
systemctl status pve-daily-update.timer
```

Trigger a manual renewal check:

```bash
pvenode acme cert renew
```

If a renewal fails silently, check the update log:

```bash
grep -i acme /var/log/pveupdate.log | tail -20
```

Expect to see lines confirming the renewal check ran and either skipped (cert still valid) or completed successfully.

## Gotchas and Pitfalls From Real Use

**Rate limits hit fast during testing**: Let's Encrypt's production CA allows 5 failed certificate orders per domain per hour. If your plugin config is wrong and you retry quickly, you'll burn the limit. Always test with the staging CA first — it has no rate limits.

**DNS propagation timing**: The Cloudflare plugin inserts the TXT record and then waits before signaling ACME to validate. Cloudflare is typically 5-15 seconds. Slower DNS providers can take 2-5 minutes, and the Proxmox ACME client's built-in propagation wait may time out before slower providers finish. If validation fails consistently with a non-Cloudflare provider, look for a `sleep` or `propagation_seconds` setting in its plugin script under `/usr/share/proxmox-acme/dnsapi/`.

**Wildcard certificates require DNS-01**: Let's Encrypt issues wildcard certs only via DNS-01. To get `*.yourdomain.com`, set:

```bash
pvenode config set --acmedomain0 "*.yourdomain.com,plugin=cloudflare"
```

Note that `*.yourdomain.com` covers `pve.yourdomain.com` but not the apex `yourdomain.com`. Add a second domain entry (`--acmedomain1`) for the apex if you need it.

**Cluster nodes each need separate certs**: In a three-node cluster, configure ACME independently on `pve1`, `pve2`, and `pve3`. Each node's config lives at `/etc/pve/nodes/<nodename>/config`. There is no mechanism to push a certificate from the cluster view. Once you've invested the time setting up a [full Proxmox private cloud](/articles/build-private-cloud-home-proxmox-ve/), budget an extra 10 minutes per additional node for certificate setup.

**Port 8006 vs port 80**: HTTP-01 challenge needs port 80 forwarded to the Proxmox host — not port 8006. Forwarding 80 → 8006 via NAT will fail the challenge because the ACME token is served on port 80, not the web UI port.

## Verifying the Certificate

After ordering, confirm the cert details:

**Web UI**: `Node > Certificates` — you'll see the Let's Encrypt cert listed alongside the original self-signed CA cert.

**CLI**:

```bash
openssl x509 \
  -in /etc/pve/local/pveproxy-ssl.pem \
  -text -noout \
  | grep -E "(Subject:|Issuer:|Not After)"
```

Expected output:

```
Subject: CN=pve.yourdomain.com
Issuer: C=US, O=Let's Encrypt, CN=R10
Not After : Jul 23 12:00:00 2026 GMT
```

The certificate is valid for 90 days. Proxmox renews it when fewer than 30 days remain, so in steady state you should never see expiry-related downtime.

## Using a Different DNS Provider

Proxmox ships ACME DNS plugins for 30+ providers. List all available APIs:

```bash
ls /usr/share/proxmox-acme/dnsapi/
```

The naming convention: `dns_cf.sh` → pass `--api cf` to `pvenode acme plugin add dns`. For AWS Route 53:

```bash
pvenode acme plugin add dns myroute53 \
  --api route53 \
  --data "AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE&AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
```

If your DNS provider isn't in the list, the `acme-dns` delegation approach works universally — you create a CNAME from `_acme-challenge.yourdomain.com` to a subdomain on a separate acme-dns server you control. More setup, but compatible with any registrar.

## Conclusion

Setting up Let's Encrypt on Proxmox takes about 15 minutes and permanently eliminates the certificate warning on your management interface. Register a staging account, add your DNS plugin, configure the domain, confirm the staging cert orders cleanly, then switch to production — the systemd timer handles every renewal from that point forward. This pairs directly with the steps in the [Proxmox firewall, fail2ban, and SSH hardening guide](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) to give your management interface a properly secured baseline from day one.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="lets-encrypt"/>
        <category label="acme"/>
        <category label="ssl"/>
        <category label="certificates"/>
        <category label="dns-challenge"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox High Availability Setup for Automatic VM Failover]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-high-availability-vm-failover/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-high-availability-vm-failover/"/>
        <updated>2026-04-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up Proxmox HA Manager to automatically restart VMs after a node failure. Covers fencing requirements, HA group config, and live failover testing on PVE 9.1.]]></summary>
        <content type="html"><![CDATA[
Proxmox High Availability Manager restarts your VMs automatically on a surviving node within about 60-90 seconds of detecting a node failure — no manual intervention, no SSH session at 3am. By the end of this guide, you'll have a working HA cluster with properly configured fencing, HA groups, and a tested failover. I'm running this on a three-node Proxmox VE 9.1 cluster with Ceph shared storage, but the procedure is identical for iSCSI or NFS-backed clusters.

## Key Takeaways

- **3 nodes minimum**: Two-node clusters can't maintain quorum after a single failure — HA needs a majority vote to proceed.
- **Fencing is mandatory**: Without a working watchdog or IPMI fence agent, Proxmox HA will refuse to restart VMs to avoid split-brain data corruption.
- **Shared storage required**: VMs must live on storage accessible from all nodes — Ceph, iSCSI, NFS, or shared ZFS over FC.
- **Recovery takes 60-90 seconds**: The delay is deliberate — Proxmox waits for fencing confirmation before restarting anything.
- **Test with a hard power-off**: A graceful shutdown doesn't replicate a real failure scenario.

## How Proxmox HA Actually Works

Proxmox HA runs on two daemons: `pve-ha-lrm` (Local Resource Manager, one per node) and `pve-ha-crm` (Cluster Resource Manager, one elected leader per cluster). The CRM watches resource states; the LRM executes commands on its local node.

When a node goes down, the sequence is:

1. Corosync marks the node unreachable after missed heartbeats.
2. The CRM waits for **fencing confirmation** — either a watchdog reset or an IPMI power-cycle that proves the failed node is genuinely off.
3. Once fenced, the CRM issues relocate or restart commands for all HA-managed VMs.
4. The LRM on a surviving node starts each VM from the shared storage pool.

Step 2 is where most misconfigured HA setups stall. Without fencing, the CRM correctly refuses to restart VMs — the original node might still be running and holding disk locks, and starting a second instance would corrupt the VM's filesystem.

### Why the Three-Node Minimum Matters

Corosync requires a majority (quorum) to operate. With two nodes, losing one leaves you at exactly 50% — no majority, cluster services halt. With three nodes, losing one leaves you at 66% — quorum maintained, HA proceeds normally.

You can work around a two-node cluster with a lightweight `qdevice` (a tie-breaker service running on something like a Raspberry Pi), but three nodes is the cleaner path. If you're starting from scratch, the guide on [building a private Proxmox cloud at home](/articles/build-private-cloud-home-proxmox-ve/) walks through the full multi-node cluster setup prerequisites.

## What You Need Before Enabling HA

Check all of these before touching the HA configuration panel. Missing any one of them produces an HA setup that *looks* active but silently fails when you actually need it.

**Cluster:**
- Three or more PVE 9.1 nodes in the same cluster
- Corosync heartbeat latency under 5ms — use a dedicated cluster NIC if you can
- Synchronized time on all nodes: run `chronyc tracking` and confirm offset under 100ms

**Storage:**
- Target VMs must use shared storage: Ceph RBD, iSCSI, NFS, or Fibre Channel
- Local storage (`local-lvm`, `local-zfs`) silently disqualifies a VM from HA eligibility

**Fencing:**
- A hardware watchdog device at `/dev/watchdog` or `/dev/watchdog0`
- Or IPMI/iDRAC/iLO configured as a fence agent with tested, working credentials

Verify your watchdog device is present:

```bash
ls /dev/watchdog*
```

If nothing appears, load the software fallback as a stopgap (acceptable for testing, not for production):

```bash
modprobe softdog
echo "softdog" >> /etc/modules
```

## Configure the Hardware Watchdog with watchdog-mux

Proxmox ships `watchdog-mux`, a daemon that multiplexes the watchdog device so multiple HA processes can share it safely. It must be running on every cluster node.

Check and enable it:

```bash
systemctl status watchdog-mux
systemctl enable --now watchdog-mux
```

Verify the LRM connected to it:

```bash
journalctl -u pve-ha-lrm --since "5 minutes ago" | grep -i watchdog
```

You should see a line confirming the LRM opened `/run/watchdog-mux.sock`. Errors here mean fencing is broken and recovery will hang indefinitely.

The watchdog timeout is configurable:

```ini
# /etc/default/pve-ha-manager
HA_WATCHDOG_TIMEOUT=60
```

The 60-second default is appropriate for most setups. Shorter values increase sensitivity to transient network blips; longer values delay recovery.

### Setting Up IPMI Fencing for Bare-Metal Nodes

For bare-metal servers with IPMI — which covers most enterprise hardware and many homelab boards — IPMI fencing is more reliable than a software watchdog alone. It gives you hard power control even when the OS is completely unresponsive.

Install the fence agents package on all nodes:

```bash
apt install fence-agents
```

Test your BMC credentials before configuring anything:

```bash
fence_ipmilan -a 192.168.1.52 -l admin -p yourpassword -o status
```

Expected output: `Status: ON`. If this fails, fix IPMI access first — there is no point configuring HA fencing around a broken BMC connection. While you're securing IPMI access, make sure it's restricted to your management VLAN; the [Proxmox hardening guide](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) has practical firewall rules for exactly this scenario.

Configure the fence agent per-node via the Proxmox API:

```bash
pvesh set /nodes/pve2/config \
  --fence-plugin ipmilan \
  --fence-ipmi-ip 192.168.1.52 \
  --fence-ipmi-user admin \
  --fence-ipmi-password yourpassword
```

## How to Create HA Groups and Enroll VMs

### Create an HA Group

HA groups control which nodes are eligible to run a set of VMs and in what priority order. Navigate to **Datacenter → HA → Groups → Add**, or use the API:

```bash
pvesh create /cluster/ha/groups \
  --group critical-vms \
  --nodes "pve1:3,pve2:2,pve3:1"
```

The trailing number is priority — higher wins. Equal priority means Proxmox picks the surviving node arbitrarily.

| Option | Effect |
|--------|--------|
| `restricted` | VMs only ever run on nodes listed in this group |
| `nofailback` | VMs don't migrate back when the preferred node recovers |
| Node priority | Determines which surviving node receives the VM first |

Start with one group containing all nodes at equal priority. Tune after watching real failovers.

### Add VMs and Containers to the HA Group

In the web UI: select a VM, click **More → Manage HA**. Or with the CLI:

```bash
pvesh create /cluster/ha/resources \
  --sid vm:101 \
  --group critical-vms \
  --state started \
  --max_restart 3 \
  --max_relocate 3
```

- `--state started`: the desired state HA will actively maintain
- `--max_restart`: restart attempts on the current node before escalating to relocation
- `--max_relocate`: relocation attempts across nodes before marking the resource failed

LXC containers use `--sid ct:102`. Confirm all enrolled resources:

```bash
pvesh get /cluster/ha/resources
```

Before adding a VM, always verify its disk is on shared storage:

```bash
qm config 101 | grep -E "^(scsi|virtio|ide|sata)"
# You want output like:
# scsi0: ceph-pool:vm-101-disk-0,size=32G
# Not:
# scsi0: local-lvm:vm-101-disk-0,size=32G
```

A VM on `local-lvm` appears enrolled and healthy in the HA panel, then silently fails to recover when you need it most. There is no warning at enrollment time.

## How to Test HA Failover the Right Way

Do not use `systemctl poweroff` to test failover. A clean shutdown lets the node announce its departure to the cluster, which changes how the CRM handles the transition — it's not a realistic crash simulation.

Use a hard power-off instead. From a machine with IPMI access:

```bash
ipmitool -H 192.168.1.51 -U admin -P yourpassword chassis power off
```

Alternatively, on a dedicated test node, force a kernel panic:

```bash
# WARNING: This immediately crashes the system. Test nodes only.
echo c > /proc/sysrq-trigger
```

Watch recovery in real time from a surviving node:

```bash
watch -n2 "pvesh get /cluster/ha/status/current"
```

Expected timeline:
- **0-30s**: Corosync detects the absent node, CRM initiates fencing
- **30-60s**: Watchdog resets the failed node, or IPMI confirms power-off
- **60-90s**: CRM issues relocation commands; LRM brings VMs online on the surviving node

If the status stays in `recovery` past 90 seconds, the CRM is waiting on a fencing confirmation that never arrived:

```bash
journalctl -u pve-ha-crm -f
```

The log will tell you exactly which fence operation stalled. It's almost always either `watchdog-mux` not running on every node after a reboot, or stale IPMI credentials.

## Common HA Mistakes to Avoid

**VM on local storage.** Enrolled in HA, appears healthy, fails silently on recovery. Verify storage before adding any resource.

**Skipping the IPMI fence test.** `fence_ipmilan ... -o status` takes 10 seconds to run. Skipping it takes hours to debug when HA stalls at 3am.

**Two nodes without a qdevice.** One failure, no quorum, HA freezes. Either add a third node or deploy `corosync-qnetd` on a lightweight device before relying on HA for anything real.

**NTP drift.** Corosync is sensitive to clock skew. Offset over a few hundred milliseconds triggers spurious node-unreachable events. Run `timedatectl status` on each node and confirm NTP is active and synced.

**max_restart set to 1.** A VM that needs 45 seconds to complete its startup health check will relocate unnecessarily on the first failed check. Set `max_restart` to at least 3 for non-trivial workloads.

**No N-1 capacity planning.** HA restarts VMs, but if surviving nodes are already at 90% RAM utilization, the VMs fail to start anyway. For a three-node cluster with 128 GB per node, plan as though any single node may be absent — cap total allocated RAM at 256 GB.

## Conclusion

With `watchdog-mux` confirmed running, shared storage in place, and VMs enrolled in HA groups, Proxmox automatically recovers critical workloads within 90 seconds of a node failure. Fencing isn't bureaucratic overhead — it's the safety mechanism that makes corruption-free restarts possible. Run the hard power-off test before you declare success.

Once HA is protecting your VMs at the infrastructure level, add point-in-time recovery at the data level: schedule regular backups via [Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) so that even a storage failure has a fallback beyond the last snapshot.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="proxmox-ha"/>
        <category label="high-availability"/>
        <category label="vm-failover"/>
        <category label="fencing"/>
        <category label="cluster"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[LVM-Thin Pools on Proxmox for VM Snapshots Without ZFS]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-lvm-thin-pools-vm-snapshots/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-lvm-thin-pools-vm-snapshots/"/>
        <updated>2026-04-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up LVM-thin pools on Proxmox VE 9.1 for copy-on-write snapshots without ZFS memory overhead. Works on any block device with no ECC RAM required.]]></summary>
        <content type="html"><![CDATA[
LVM-thin provisioning gives you copy-on-write snapshots on virtually any block device — spinning rust, SATA SSD, or NVMe — without the ECC RAM requirement or memory overhead that ZFS demands. If you're running a homelab node with 16–32 GB of system RAM and need live snapshots for VMs and containers, LVM-thin is the right answer. By the end of this guide you'll have an LVM-thin pool configured as a Proxmox storage backend, know how to snapshot and roll back VMs in seconds, and understand exactly where this approach beats ZFS and where it falls short.

## Key Takeaways

- **No ECC tax**: LVM-thin snapshots work on any block device with no special RAM requirements.
- **Copy-on-write**: Snapshots consume space only for changed blocks, not a full clone of the disk.
- **Proxmox-native**: LVM-thin is a first-class storage type in Proxmox VE 9.1 — no plugins or patches required.
- **Snapshot chains**: Performance degrades noticeably past 3–4 chained snapshots per volume; keep chains short.
- **Best fit**: Ideal for single-node homelabs, dedicated SSDs, and dev/test environments where ZFS overhead isn't justified.

## Why Choose LVM-Thin Over ZFS?

ZFS is excellent for the right workload. But ZFS is memory-hungry by design — the ARC cache eats RAM aggressively, and on a node with 16 or 32 GB that's a real constraint when you also want to run ten or more VMs. ZFS also strongly prefers ECC RAM for its data integrity guarantees, and ECC-capable consumer motherboards cost meaningfully more.

LVM-thin sits at the other end of the spectrum. It's a Linux kernel feature (dm-thin), runs on any block device, uses almost no RAM overhead, and gives you the one ZFS feature most admins actually need day-to-day: copy-on-write snapshots.

Here's how the main Proxmox storage backends compare for VM workloads:

| Storage Type | Snapshots | Thin-Provisioned | RAM Overhead | Hardware Requirement |
|---|---|---|---|---|
| LVM (thick) | No | No | Minimal | Any block device |
| LVM-Thin | Yes (CoW) | Yes | Minimal | Any block device |
| ZFS | Yes (CoW) | Yes | High (ARC) | ECC RAM preferred |
| Directory (qcow2) | Yes (file) | Yes | Minimal | Any filesystem |
| Ceph (RBD) | Yes (CoW) | Yes | Moderate | 3+ nodes |

Directory storage with qcow2 also supports snapshots, but qcow2 performance degrades under concurrent I/O because the format serializes writes internally. LVM-thin avoids that — snapshots are tracked by the kernel block layer, and raw disk images maintain full sequential write speed.

## Prerequisites and Disk Selection

You need an unformatted block device: a whole disk, a partition, or space on an existing PV. A dedicated SSD or NVMe is the right choice. Don't carve LVM-thin out of the same disk as your Proxmox OS root — contention from OS writes will hurt VM I/O latency under load.

For this guide I'll use `/dev/sdb`, a 500 GB SATA SSD added to an existing Proxmox VE 9.1 node. Adjust device paths to match your hardware. If you're still selecting hardware, [How to Install Proxmox VE on Any Hardware](/articles/install-proxmox-ve-on-any-hardware/) covers what to look for in drives and whether consumer SSDs hold up in always-on roles.

Check the disk is clean before touching it:

```bash
lsblk -f /dev/sdb
wipefs -a /dev/sdb   # Wipe leftover filesystem signatures if present
```

## Step 1: Create the Physical Volume and Volume Group

```bash
pvcreate /dev/sdb
vgcreate vg-thin /dev/sdb
```

Verify:

```bash
pvs
vgs
```

Expected output from `vgs`:

```
  VG       #PV #LV #SN Attr   VSize    VFree
  pve        1  17   0 wz--n- <476.94g  <96.00g
  vg-thin    1   0   0 wz--n- <465.76g <465.76g
```

The `pve` VG is your existing Proxmox install. `vg-thin` is the new one, ready for the pool.

## Step 2: Create the Thin Pool Logical Volume

Allocate 95% of the VG to the pool and leave 5% unallocated for LVM metadata expansion. Thin pools need metadata headroom — running the VG to 100% causes hard I/O failures across every volume in the pool simultaneously.

```bash
lvcreate \
  --type thin-pool \
  --name pool0 \
  --extents 95%VG \
  vg-thin
```

Verify the result:

```bash
lvs -a vg-thin
```

You'll see the pool LV and its hidden metadata sibling (`[pool0_tmeta]`). That's expected — LVM manages metadata allocation internally and the brackets indicate a hidden helper volume.

## Step 3: Register the Thin Pool in Proxmox Storage

You can do this via the web UI or directly with `pvesm`.

### Web UI Method

1. Open **Datacenter → Storage → Add → LVM-Thin**
2. Set **ID**: `ssd-thin`
3. Set **Volume Group**: `vg-thin`
4. Set **Thin Pool**: `pool0`
5. Set **Content**: `Disk image, Container` (add `Snippets` if needed)
6. Click **Add**

### CLI Method

```bash
pvesm add lvmthin ssd-thin \
  --vgname vg-thin \
  --thinpool pool0 \
  --content images,rootdir
```

Verify the storage is active:

```bash
pvesm status
```

You should see `ssd-thin` listed with `active` status and the available capacity reported correctly.

## Step 4: Create VMs and Containers on LVM-Thin

When creating a VM in the web UI, select `ssd-thin` from the storage dropdown for the disk. Via CLI:

```bash
qm create 200 \
  --name test-vm \
  --memory 2048 \
  --cores 2 \
  --net0 virtio,bridge=vmbr0

qm set 200 \
  --scsi0 ssd-thin:32 \
  --ide2 ssd-thin:cloudinit \
  --boot order=scsi0
```

The `ssd-thin:32` syntax allocates a 32 GB thin-provisioned volume. The pool doesn't pre-allocate 32 GB — it consumes actual disk space only as data is written. For LXC containers, the `rootdir` content type enables the same thin allocation for container root filesystems.

## How to Take and Roll Back LVM-Thin Snapshots

A snapshot on LVM-thin is a new thin volume that shares blocks with the origin. When either volume writes to a block, the dm-thin kernel driver copies the original block before overwriting. No data is duplicated at snapshot time — only divergences accumulate going forward.

Take a snapshot of VM 200:

```bash
qm snapshot 200 pre-upgrade \
  --description "Before kernel 6.12 upgrade" \
  --vmstate 0
```

The `--vmstate 0` flag skips saving RAM state, making the snapshot near-instant and much smaller. For an upgrade-and-rollback workflow, a disk-only snapshot is almost always sufficient — run `sync` inside the guest first to flush pending writes to disk.

List snapshots:

```bash
qm listsnapshot 200
```

Rollback:

```bash
qm rollback 200 pre-upgrade
```

Rollback is instant regardless of how much data changed between snapshot and rollback. The thin pool reassigns block mappings without moving any data.

## Monitoring Pool Usage Before It Causes Problems

A full thin pool is a hard failure — all volumes go read-only at once. Monitor usage proactively:

```bash
lvs -o +data_percent,metadata_percent vg-thin/pool0
```

Sample output:

```
  LV    VG       Attr       LSize   Pool  Origin Data%  Meta%
  pool0 vg-thin  twi-aotz-- 440.00g             23.47  1.82
```

Configure LVM autoextend in `/etc/lvm/lvm.conf` as a safety net:

```ini
activation {
  thin_pool_autoextend_threshold = 80
  thin_pool_autoextend_percent = 20
}
```

This grows the pool by 20% when it hits 80% full — provided unallocated space exists in the VG. That's exactly why we left the 5% reserve during pool creation.

**Gotcha from the field**: If you snapshot frequently and then delete the parent volumes without removing the snapshots first, the metadata volume grows faster than the data volume. Watch `Meta%` separately; the metadata pool is much smaller and will surprise you at an inconvenient time.

## Using LVM-Thin With Proxmox Backup Server

LVM-thin and Proxmox Backup Server integrate cleanly. PBS uses its own change-block-tracking (dirty-bitmap) mechanism for incremental backups, independent of LVM snapshots — it doesn't consume or require LVM snapshots internally. You can chain them yourself: take an LVM-thin snapshot before a PBS backup run to guarantee a consistent source while the VM continues running.

For backup scheduling and retention policy configuration, [Automated Backups with Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) walks through the full PBS setup — everything there applies equally to LVM-thin-backed VMs and containers.

## When LVM-Thin Is Not the Right Choice

LVM-thin is not a silver bullet. Here's where I'd choose something else:

- **Silent corruption protection**: ZFS checksums catch bitrot during scrubs; LVM-thin does not checksum data. For NAS workloads or long-lived archival data, ZFS wins.
- **Multi-node shared storage**: LVM-thin is strictly local to one node. For clusters requiring live migration with shared disk, Ceph RBD is the correct backend.
- **Deep snapshot chains**: LVM-thin degrades past 3–4 chained snapshots per volume. ZFS handles deep chains more gracefully, and qcow2 can technically go deeper too.
- **High-RAM ECC servers**: If you have 64+ GB ECC RAM and production workloads, ZFS overhead amortizes well and you gain checksumming plus native compression.

For homelab nodes where RAM is limited and snapshot capability matters more than byte-level integrity, LVM-thin is the correct default.

## How to Migrate Existing VMs to LVM-Thin

If you have VMs on directory storage or thick LVM and want to move them to the thin pool, Proxmox handles it live without stopping the VM:

```bash
qm move-disk 100 scsi0 ssd-thin --delete 1
```

The `--delete 1` flag removes the source disk after the move completes successfully. Expect the move to finish in under two minutes for a 50 GB disk on NVMe-to-NVMe; SATA-to-SATA will be closer to five minutes for the same size. Proxmox uses an internal mirroring approach — the VM stays online throughout.

If you're building out a broader homelab architecture with multiple storage tiers, [Build a Private Cloud at Home with Proxmox VE](/articles/build-private-cloud-home-proxmox-ve/) covers how LVM-thin fits alongside ZFS and Ceph on multi-role nodes.

## Conclusion

LVM-thin is the practical middle ground for Proxmox storage: copy-on-write snapshots, thin allocation, and solid I/O performance with no RAM overhead and no ECC requirement. Set it up on a dedicated SSD, register it in Proxmox as a storage backend, and you have a snapshot-capable layer that works on commodity hardware. Next step: configure Proxmox Backup Server to target this pool and add scheduled retention-based backups so your LVM-thin VMs are protected automatically.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="lvm"/>
        <category label="storage"/>
        <category label="snapshots"/>
        <category label="thin-provisioning"/>
        <category label="proxmox-ve"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[K3s Kubernetes Cluster on Proxmox VMs Setup Guide]]></title>
        <id>https://proxmoxpulse.com/articles/k3s-kubernetes-cluster-proxmox-vms/</id>
        <link href="https://proxmoxpulse.com/articles/k3s-kubernetes-cluster-proxmox-vms/"/>
        <updated>2026-04-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Deploy a three-node K3s Kubernetes cluster on Proxmox VMs using cloud-init templates. From bare VMs to a working kubeconfig in 30 minutes, with Longhorn persistent storage.]]></summary>
        <content type="html"><![CDATA[
Deploying K3s on Proxmox VMs gives you a production-ready Kubernetes cluster that boots from cloud-init templates in under 30 minutes. The end result: a three-node cluster — one control plane, two workers — with a working `kubeconfig` ready for `kubectl` and Longhorn handling persistent storage across both workers. This guide uses Proxmox VE 9.1 and K3s v1.32, the current stable release as of April 2026. If you've got Proxmox running and want Kubernetes without upstream complexity, K3s is the most direct path there.

## Key Takeaways

- **Template first**: Build one Ubuntu 24.04 cloud-init template, clone it for every node — identical base, zero config drift.
- **VM sizing**: Control plane needs 2 vCPU and 4 GB RAM minimum; workers at 2 vCPU / 4 GB handle most homelab workloads comfortably.
- **Networking**: K3s uses Flannel VXLAN by default — a single Proxmox Linux bridge handles it without SDN or VLAN config.
- **Storage**: Longhorn needs a dedicated virtio disk per worker, not the OS disk — add it before deploying Longhorn.
- **HA tradeoff**: Single control plane is fine for homelabs; true HA requires three control-plane nodes with embedded etcd, worth it only for production.

## Why K3s Instead of Full Kubernetes on Proxmox

K3s is a CNCF-certified Kubernetes distribution maintained by SUSE. It packages the entire control plane as a single ~70 MB binary, replaces etcd with SQLite by default (or embedded etcd for HA), and drops cloud-provider integrations that don't apply to Proxmox anyway.

For homelab and small production setups, the advantages over kubeadm-managed Kubernetes are concrete:

- **Single binary install**: No `kubeadm init`, no separate etcd cluster, no kubelet config juggling.
- **Lower RAM floor**: K3s control plane idles around 500 MB vs. 1.5–2 GB for a full Kubernetes control plane.
- **Auto-upgrade support**: The `system-upgrade-controller` lets you roll cluster upgrades without SSH-ing into each node.
- **Built-in ingress**: Traefik ships as the default ingress controller — functional out of the box, swappable if you prefer ingress-nginx.

If you're already [running Docker inside LXC containers on Proxmox](/articles/docker-inside-lxc-containers-proxmox/) and want to move toward orchestration, K3s is the lowest-friction upgrade path. Docker-in-LXC works well for a handful of services, but once you hit five or more containers that need health checks, scheduling, and rolling deployments, Kubernetes scheduling pays for itself immediately.

## Hardware and VM Requirements

You don't need a dedicated machine. A single Proxmox host with 32 GB RAM and an NVMe drive can run this entire setup with room to spare for other VMs.

| Node | vCPU | RAM | OS Disk | Extra Disk | Role |
|------|------|-----|---------|------------|------|
| k3s-control | 2 | 4 GB | 32 GB | — | Control plane |
| k3s-worker-1 | 2 | 4 GB | 32 GB | 50 GB (Longhorn) | Worker |
| k3s-worker-2 | 2 | 4 GB | 32 GB | 50 GB (Longhorn) | Worker |

The Longhorn disks are thin-provisioned virtio disks — Proxmox allocates storage lazily, so a 50 GB thin disk only consumes what Longhorn actually writes. On NVMe-to-NVMe, expect a 50 GB Longhorn volume to provision in under 10 seconds.

All three VMs on the same bridge (`vmbr0`) is sufficient for a homelab cluster. If you want to isolate cluster traffic from your LAN — and for anything exposed to the internet you should — see [configuring VLANs on Proxmox with Linux bridges](/articles/configure-vlans-proxmox-linux-bridges/) for a clean segmentation approach before you start.

## Building the Base Ubuntu Cloud-Init Template

Every node in this cluster starts as a clone of the same base template. Get the template right and the rest is cloning plus one install command per node.

### Download the Ubuntu 24.04 Cloud Image

SSH into your Proxmox host and pull the cloud image:

```bash
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img \
  -O /tmp/noble-server-cloudimg-amd64.img
```

Ubuntu cloud images ship with `cloud-init` pre-installed and the `ubuntu` user pre-configured for SSH key injection. No guest OS bootstrapping required.

### Create and Configure the Template VM

```bash
# VM ID 9000 is a common convention for templates
qm create 9000 \
  --name ubuntu-2404-cloud \
  --memory 4096 \
  --cores 2 \
  --net0 virtio,bridge=vmbr0 \
  --ostype l26 \
  --agent enabled=1

# Import the cloud image as the primary disk
qm importdisk 9000 /tmp/noble-server-cloudimg-amd64.img local-lvm

# Attach it as a scsi disk and set boot order
qm set 9000 \
  --scsihw virtio-scsi-pci \
  --scsi0 local-lvm:vm-9000-disk-0,discard=on,ssd=1 \
  --boot c \
  --bootdisk scsi0

# Add the cloud-init drive
qm set 9000 --ide2 local-lvm:cloudinit

# Serial console is required for cloud-init display
qm set 9000 --serial0 socket --vga serial0

# Inject your SSH public key and set DHCP as the default
qm set 9000 \
  --ciuser ubuntu \
  --sshkeys ~/.ssh/authorized_keys \
  --ipconfig0 ip=dhcp
```

### Resize the OS Disk and Convert to Template

The cloud image ships as a 2.2 GB raw disk. Resize it before converting — you cannot resize a template disk after conversion.

```bash
qm resize 9000 scsi0 32G
qm template 9000
```

That's the template. Every K3s node will be a full clone of VM 9000, booting with a fresh hostname and a DHCP address on first start. If the disk shows as `unused0` after import instead of being attached, run `qm set 9000 --scsi0 local-lvm:vm-9000-disk-0` to re-attach it — this happens when the storage name in the import path doesn't exactly match the storage ID in Proxmox.

## How to Deploy the K3s Control Plane Node

### Clone the Template and Assign a Static IP

Static IPs are not optional here. If the control plane IP changes after cluster initialization, the TLS certificates and Flannel overlay both break.

```bash
# Full clone so the worker is independent (not a linked clone)
qm clone 9000 101 --name k3s-control --full

# Static IP for the control plane
qm set 101 --ipconfig0 ip=192.168.1.10/24,gw=192.168.1.1

qm start 101
```

Wait about 30 seconds for cloud-init to finish its first-boot run, then SSH in:

```bash
ssh ubuntu@192.168.1.10
```

### Install K3s on the Control Plane

```bash
curl -sfL https://get.k3s.io | sh -s - server \
  --cluster-init \
  --tls-san 192.168.1.10 \
  --disable traefik \
  --node-name k3s-control
```

Flags worth explaining:

- `--cluster-init` initializes embedded etcd, enabling HA expansion later if you add more control-plane nodes.
- `--tls-san 192.168.1.10` adds the control plane's IP to the TLS certificate SANs — required for `kubectl` connections from outside the VM.
- `--disable traefik` is personal preference; remove this flag if you want Traefik as your ingress controller out of the box.

K3s installs, starts, and enables the `k3s` systemd service in about 45 seconds on a modern CPU. Grab the node token for worker joins:

```bash
sudo cat /var/lib/rancher/k3s/server/node-token
```

Copy the kubeconfig to your local machine and fix the server address:

```bash
# Run from your local machine
scp ubuntu@192.168.1.10:/etc/rancher/k3s/k3s.yaml ~/.kube/config
sed -i 's/127.0.0.1/192.168.1.10/g' ~/.kube/config
chmod 600 ~/.kube/config
```

Verify:

```bash
kubectl get nodes
```

`k3s-control` should appear in `Ready` state within 60 seconds of the install completing.

## Joining Worker Nodes to the Cluster

Clone the template twice more:

```bash
qm clone 9000 102 --name k3s-worker-1 --full
qm set 102 --ipconfig0 ip=192.168.1.11/24,gw=192.168.1.1

qm clone 9000 103 --name k3s-worker-2 --full
qm set 103 --ipconfig0 ip=192.168.1.12/24,gw=192.168.1.1

qm start 102 && qm start 103
```

SSH into each worker and run the K3s agent installer. Replace `<your-node-token>` with the token from the previous step:

```bash
curl -sfL https://get.k3s.io | \
  K3S_URL=https://192.168.1.10:6443 \
  K3S_TOKEN=<your-node-token> \
  sh -s - agent \
  --node-name k3s-worker-1
```

Repeat on `k3s-worker-2` with `--node-name k3s-worker-2`. Each agent join completes in under 30 seconds. From your local machine:

```bash
kubectl get nodes -o wide
```

Expected output:

```
NAME           STATUS   ROLES                       AGE   VERSION
k3s-control    Ready    control-plane,etcd,master   5m    v1.32.3+k3s1
k3s-worker-1   Ready    <none>                      2m    v1.32.3+k3s1
k3s-worker-2   Ready    <none>                      1m    v1.32.3+k3s1
```

## Adding Persistent Storage with Longhorn

K3s includes a `local-path` provisioner that creates node-local volumes — fine for stateless workloads, useless for anything that needs to survive pod rescheduling to a different node. Longhorn replicates block storage across your worker nodes and fixes this.

### Add a Dedicated Disk to Each Worker

From the Proxmox host (VMs stay running — virtio hotplug works on Linux 5.x+ guests):

```bash
qm set 102 --virtio1 local-lvm:50,discard=on
qm set 103 --virtio1 local-lvm:50,discard=on
```

The disks appear immediately as `/dev/vdb` inside the VMs. Do not partition or format them — Longhorn manages the raw block device directly.

### Install Longhorn Prerequisites on Each Worker

```bash
sudo apt-get install -y open-iscsi nfs-common
sudo systemctl enable --now iscsid
```

Skipping `open-iscsi` is the single most common reason Longhorn volumes get stuck in `Attaching`. The iSCSI initiator failure is silent — the pod just hangs.

### Deploy Longhorn v1.7.1

```bash
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.7.1/deploy/longhorn.yaml
```

Watch the rollout (takes 3–4 minutes on first deploy):

```bash
kubectl -n longhorn-system get pods --watch
```

Once all pods are running, set Longhorn as the default storage class:

```bash
kubectl patch storageclass local-path \
  -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

kubectl patch storageclass longhorn \
  -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
```

Now any PVC without an explicit storage class gets a Longhorn volume replicated across both workers.

## Common Gotchas and How to Fix Them

**Nodes stuck in `NotReady` after join**: Almost always a firewall issue. K3s needs ports 6443 (API), 8472/UDP (Flannel VXLAN), and 10250 (kubelet) open between nodes. If `ufw` is active on your VMs, it will silently drop VXLAN traffic. Either disable it or open those ports explicitly:

```bash
sudo ufw allow 6443/tcp
sudo ufw allow 8472/udp
sudo ufw allow 10250/tcp
```

**Node IPs change after a DHCP lease renewal**: This is why the static IP step matters. If you skipped it and used DHCP, the flannel overlay breaks when the IP changes. Fix by setting static IPs via `qm set` cloud-init config and reprovisioning the affected node.

**`kubectl get nodes` shows the node as `NotReady` after a Proxmox host reboot**: Check that the `k3s` and `k3s-agent` services started correctly. Cloud-init sometimes races with systemd on first boot after a snapshot restore.

```bash
sudo systemctl status k3s          # control plane
sudo journalctl -u k3s-agent -f    # workers
```

**Template disk shows as `unused0` after import**: Re-attach it manually:

```bash
qm set 9000 --scsi0 local-lvm:vm-9000-disk-0
```

This happens when the storage name used in `qm importdisk` doesn't exactly match the storage pool ID shown in the Proxmox UI.

## Securing the Cluster

The default K3s kubeconfig at `/etc/rancher/k3s/k3s.yaml` is world-readable on the control plane — fix that immediately:

```bash
sudo chmod 600 /etc/rancher/k3s/k3s.yaml
```

The node token in `/var/lib/rancher/k3s/server/node-token` grants full cluster join rights. Treat it like a root password and rotate it after initial setup.

For the Proxmox host layer, [hardening Proxmox VE with firewall, fail2ban, and SSH security](/articles/hardening-proxmox-firewall-fail2ban-ssh-security/) covers host-level lockdown you should do in parallel with cluster setup.

For disaster recovery: [Proxmox Backup Server](/articles/automated-backups-proxmox-backup-server/) can snapshot all three K3s VMs at the hypervisor level, giving you a clean restore point before cluster upgrades or Kubernetes version bumps. Pair hypervisor snapshots with Longhorn's built-in snapshot support for application-level recovery.

## Conclusion

You now have a three-node K3s v1.32 cluster on Proxmox VE 9.1: control plane with embedded etcd, two workers with Longhorn persistent storage, and a local `kubeconfig` ready for `kubectl`. The cloud-init template approach means adding a fourth node is a `qm clone` and a 30-second agent join — no manual OS setup. The logical next step is deploying an ingress controller (ingress-nginx requires two `kubectl apply` commands) and exposing your first service outside the cluster.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="kubernetes"/>
        <category label="k3s"/>
        <category label="proxmox"/>
        <category label="cloud-init"/>
        <category label="longhorn"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Self-Host OpenClaw AI Assistant in Proxmox LXC]]></title>
        <id>https://proxmoxpulse.com/articles/self-host-openclaw-ai-assistant-proxmox-lxc/</id>
        <link href="https://proxmoxpulse.com/articles/self-host-openclaw-ai-assistant-proxmox-lxc/"/>
        <updated>2026-04-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how to self-host OpenClaw, the open-source local AI assistant, inside a Proxmox VE LXC container. Step-by-step guide for homelab sysadmins.]]></summary>
        <content type="html"><![CDATA[
Your laptop is not a server. It sleeps, it travels, and the moment you close the lid your local AI assistant goes dark along with it. Running OpenClaw on a Proxmox VE node changes that entirely — your homelab AI stays online 24/7, never uploads your conversations or files to a third-party cloud, and costs nothing beyond the electricity your server was already burning. If privacy, true ownership of your data, and a persistent assistant that actually remembers your context are things you care about, this guide will get you running.

## Why Run OpenClaw in a Proxmox LXC

Proxmox VE gives you two paths for hosting a service like OpenClaw: a full virtual machine or an LXC container. For a gateway-style service, LXC wins on almost every axis.

LXC containers share the host kernel, so you skip the overhead of a full hypervisor, a duplicated OS boot stack, and redundant memory pages. A well-tuned OpenClaw LXC idles at under 200 MB of RAM, compared to 500 MB or more for an equivalent VM. That headroom matters when your Proxmox node is already juggling Home Assistant, a media server, and a handful of other containers.

Snapshot and backup ergonomics are another win. You can freeze the entire container filesystem in seconds with `pct snapshot`, roll it back instantly if a ClawHub skill install goes sideways, and schedule vzdump backups to fire at 2 AM without ever touching the guest OS. If you later decide to add a local inference engine like Ollama, you can expand the existing LXC or spin up a second one dedicated to model serving — Proxmox even supports GPU passthrough to LXC containers on recent kernels, so that upgrade path is wide open for your openclaw proxmox setup.

## Provisioning the LXC Container

Log in to the Proxmox web UI and pull the latest Debian 12 (Bookworm) LXC template from your local template store or a configured repository. Debian 12 is the safest baseline here — it ships with a recent glibc, NodeSource supports it fully, and the package ecosystem is rock-solid.

A sensible baseline for OpenClaw running against a cloud API (Anthropic, OpenAI, or Google):

- **CPU**: 2 cores
- **RAM**: 4 GB
- **Disk**: 20 GB
- **Network**: vmbr0 or your LAN bridge, static IP recommended

If you plan to run local inference inside the same container — Ollama with a 7B parameter model, for example — bump RAM to at least 8 GB and disk to 40 GB. Local models are memory-hungry and you will feel the squeeze fast if you underallocate.

Create the container from the Proxmox host shell:

```bash
pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname openclaw \
  --cores 2 \
  --memory 4096 \
  --swap 512 \
  --rootfs local-lvm:20 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --features nesting=1 \
  --start 1
```


The `--unprivileged 1` flag is the most important security decision you make here. It maps the container's root user to an unprivileged UID on the host, so a container escape does not hand an attacker real root access to your Proxmox node. The `--features nesting=1` flag is optional but worth enabling now — if you decide to run Docker-based skills via ClawHub later, you will already have it in place.

Attach to the running container:

```bash
pct exec 200 -- bash
```


## Installing OpenClaw

Start with a clean system update inside the container:

```bash
apt update && apt upgrade -y
apt install -y curl ca-certificates git
```


OpenClaw requires Node.js 22.14 as a minimum, but the project recommends Node.js 24. The cleanest way to get it on Debian 12 is via the NodeSource setup script:

```bash
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt install -y nodejs
node --version
```


The version output should read `v24.x.x`. If you prefer managing Node versions with nvm instead:

```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 24
nvm use 24
```


With Node.js in place, run the OpenClaw one-liner installer:

```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```


If you prefer going through npm directly:

```bash
npm i -g openclaw
```


Verify the installation completed successfully:

```bash
openclaw --version
```


If the command is not found, make sure `/usr/local/bin` or your npm global bin path is on `PATH`. A quick `source ~/.bashrc` or a fresh shell session usually resolves it.

## First-Run Onboarding

OpenClaw's onboarding wizard handles provider selection, API key entry, and daemon registration in a single interactive session. Run:

```bash
openclaw onboard --install-daemon
```


The `--install-daemon` flag is what makes this a proper homelab AI deployment — it registers OpenClaw as a systemd service so the gateway starts automatically every time the LXC boots, no manual intervention needed.

The wizard will ask which AI provider you want to use:

- **Anthropic** — Claude models, strong reasoning and instruction-following
- **OpenAI** — GPT-4o and variants
- **Google** — Gemini models via AI Studio
- **Local (Ollama)** — No API key required, inference runs on your own hardware

For a first-time local ai proxmox lxc setup, starting with Anthropic or OpenAI is the fastest path to a working assistant. Paste your API key when prompted. You can add additional providers or switch to a local model at any time from the gateway dashboard.

Once the wizard finishes, verify the gateway is healthy:

```bash
openclaw gateway status
```


If the gateway shows as stopped, restart it and check again:

```bash
openclaw gateway restart
openclaw gateway status
```


The `openclaw gateway dashboard` command opens a local web UI where you can inspect connected chat channels, active sessions, memory state, and installed skills. Bookmark the dashboard URL — you will be back here often.

## Connecting Telegram as Your Control Channel

Of all the chat integrations OpenClaw supports — Telegram, Discord, Slack, Signal, iMessage, WhatsApp, Matrix, and Microsoft Teams — Telegram is the fastest to configure and the most practical for a homelab context. No server infrastructure required, just a bot token.

Open Telegram and start a chat with @BotFather. Send `/newbot`, follow the two-step prompts to name your bot, and copy the token it returns. It will look something like `1234567890:ABCDEFghijklmnop`.

Open the OpenClaw gateway dashboard:

```bash
openclaw gateway dashboard
```


Navigate to **Channels → Telegram → Add**, paste your bot token, and save. The gateway connects immediately — no restart needed.

Test it by opening your new bot in Telegram and sending a message:


Hello, what can you do?


If the gateway is running correctly you will get a response from your configured AI provider within a few seconds. From this point, your Telegram bot is a full-featured interface to your self-host ai assistant — it can browse the web, read and write files on the container filesystem, execute shell commands, maintain persistent memory across conversations, and trigger any skill you install from ClawHub.

Discord and Slack are straightforward second channels to add if you already run homelab infrastructure on those platforms. Both require creating an application in their respective developer portals and pasting a token or webhook URL into the gateway dashboard — the process is well-documented at docs.openclaw.ai/getting-started.

## Hardening It for Your Homelab

An AI assistant with shell execution capabilities is a powerful tool. It deserves deliberate hardening before you expose it beyond your local network.

### Stay Unprivileged

You already set `--unprivileged 1` at creation time. Do not change this. The unprivileged LXC boundary is the most impactful security control you have between OpenClaw and your Proxmox host kernel.

### Protect Your API Key

Store your API key only through the path that `OPENCLAW_CONFIG_PATH` points to. Do not hardcode it in shell scripts, do not drop it in `.bashrc`, and do not let it appear in any file that could be accidentally committed or shared. OpenClaw also respects `OPENCLAW_HOME` and `OPENCLAW_STATE_DIR` if you want to relocate its data directory to a dedicated bind mount or separate disk.

### Isolate the Container on Its Own VLAN

Put the OpenClaw LXC on a dedicated VLAN rather than your flat homelab LAN. On Proxmox, assign a VLAN tag to the container's network interface:

```bash
pct set 200 --net0 name=eth0,bridge=vmbr0,tag=20,ip=192.168.20.10/24,gw=192.168.20.1
```


This limits the blast radius if the container is ever compromised — it cannot directly reach your NAS, your router management interface, or other sensitive services without traversing your firewall and its explicit allow rules.

### Use Tailscale or WireGuard for Remote Access

Do not port-forward the OpenClaw gateway dashboard to the public internet. Instead, install Tailscale inside the container:

```bash
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
```


You can now reach the dashboard from anywhere in the world over an encrypted mesh tunnel with no open router ports. If you already run a WireGuard VPN for your homelab, a split-tunnel configuration works equally well.

### Review Shell Access Scope

OpenClaw can execute shell commands inside the container it runs on. Before pointing it at sensitive mount points or external storage, review the permission scope in the gateway dashboard. Start with access limited to a sandboxed working directory and expand deliberately as you build confidence in how the assistant uses those capabilities.

## Backups and Snapshots

One of the genuine pleasures of running a homelab AI inside Proxmox LXC is that your backup story is built in from day one.

### Snapshot Before Risky Changes

Before installing a new ClawHub skill, upgrading OpenClaw, or making config changes, take a named snapshot:

```bash
pct snapshot 200 pre-skill-install --description "Before installing browser skill"
```


If anything breaks, rollback takes seconds:

```bash
pct rollback 200 pre-skill-install
```


This tight feedback loop makes experimenting with new skills genuinely low-risk.

### Scheduled vzdump Backups

Set up a recurring vzdump job under **Datacenter → Backup** in the Proxmox web UI. A nightly run at 2 AM with seven daily copies and four weekly copies retained is a solid default for a service like this. The backup captures the entire LXC — OpenClaw's state directory, configuration, skill data, and the persistent memory it has built up across your sessions.

If you are running Proxmox Backup Server, offloading these backups there gives you deduplication and long-term retention without ballooning storage costs. The persistent memory OpenClaw accumulates over weeks and months becomes genuinely valuable context — treat it with the same care you would any other stateful service.

## Troubleshooting Common Issues

### Gateway Won't Start

Start with the status command:

```bash
openclaw gateway status
```


If it shows stopped or error, pull recent logs from systemd:

```bash
journalctl -u openclaw -n 50 --no-pager
```


The most common causes are a Node.js version below 22.14, a malformed YAML configuration file, or a port conflict with another service. Run `openclaw gateway restart` and check status again after about ten seconds.

### Node Version Too Old

If you see a `SyntaxError: Unexpected token` or a warning about unsupported engine versions, your Node.js is too old:

```bash
node --version
```


Anything below `v22.14.0` needs to be upgraded. Revisit the NodeSource installation steps and make sure you ran `setup_24.x`, not an older variant. After reinstalling Node.js, verify `openclaw --version` resolves correctly before restarting the gateway.

### Telegram Bot Is Silent

Work through this checklist in order:

1. Confirm the gateway is running with `openclaw gateway status`
2. Double-check the bot token — a single transposed character breaks authentication silently
3. Test outbound connectivity from inside the container: `curl -s https://api.telegram.org` should return a JSON response
4. If the LXC is on a restricted VLAN, verify your firewall allows outbound port 443 to Telegram's API servers

Most silent bot issues trace back to either a wrong token or a missing firewall rule on a locked-down VLAN.

### Running Out of RAM With a Local Model

If you added Ollama and a quantized model and the container starts swapping heavily, you have two clean options. The faster fix is to increase the LXC memory allocation live from the Proxmox UI — no container restart is required for most configurations. The cleaner long-term fix is to move Ollama to a dedicated LXC, keeping OpenClaw's gateway lightweight and independently scalable. The two containers communicate over the internal Proxmox network bridge, and the latency is negligible on local hardware.

## Conclusion

Running OpenClaw on a Proxmox LXC is one of the most satisfying things you can do with spare homelab capacity. You end up with a private, always-on AI assistant that works across all your chat apps, can browse the web, manage files, and run commands — and remembers everything across sessions without any of that context leaving your hardware.

The LXC model keeps resource overhead minimal, snapshots make skill experimentation genuinely safe, and VLAN isolation keeps your network architecture clean. The path from a fresh Proxmox host to a Telegram-connected self-host ai assistant is surprisingly short: provision the container, install Node.js 24, run the curl installer, and complete the onboarding wizard. Everything else in this guide is about making it robust.

The homelab AI rabbit hole goes as deep as you want to take it — local models, custom skills, multi-channel integrations, automated workflows. OpenClaw gives you a solid, open-source foundation to build on, entirely on your own terms.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="OpenClaw"/>
        <category label="AI Assistant"/>
        <category label="LXC Containers"/>
        <category label="Proxmox VE"/>
        <category label="Self-Hosting"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox Two-Factor Authentication: TOTP and WebAuthn Setup]]></title>
        <id>https://proxmoxpulse.com/articles/proxmox-two-factor-authentication-totp-webauthn/</id>
        <link href="https://proxmoxpulse.com/articles/proxmox-two-factor-authentication-totp-webauthn/"/>
        <updated>2026-04-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Enable TOTP and WebAuthn 2FA on Proxmox VE to protect your dashboard from credential attacks — step-by-step setup, enforcement, and recovery guide.]]></summary>
        <content type="html"><![CDATA[
Locking down your Proxmox VE dashboard with a strong password is a good start — but passwords alone aren't enough in 2026. A single phished credential, brute-forced API token, or reused password from another breach can hand an attacker full control of every VM on your node. Two-factor authentication (2FA) is the single highest-impact change you can make to your Proxmox security posture after the initial install.

Proxmox VE ships with built-in support for both TOTP (Time-based One-Time Passwords) and WebAuthn (hardware security keys and passkeys). Neither requires third-party software on the server — everything runs natively through the web UI. This guide walks you through enabling both methods, enforcing 2FA for all users, and recovering access if you ever lose your authenticator.

## Why 2FA Is Non-Negotiable for Proxmox

Your Proxmox web interface runs on port 8006 and accepts credentials directly over HTTPS. If that port is reachable from the internet — or even through a VPN or reverse proxy — it's a target.

The risks are concrete:

- **Credential stuffing** — automated bots test billions of leaked username/password pairs daily
- **Brute force** — the `root@pam` account is a known target; without 2FA, fail2ban is your only line of defense
- **Session hijacking** — tokens compromised from other services get tried against Proxmox
- **Insider threats** — a second factor limits damage from a guessed or shared password

Even if your Proxmox node never touches the public internet, 2FA is worth enabling. A single compromised device on your LAN is all it takes.

## Proxmox 2FA Options: TOTP vs WebAuthn

Proxmox VE 7+ supports two second-factor methods natively:

**TOTP (Time-based One-Time Passwords)**
- Works with any standard authenticator app — Aegis, Google Authenticator, Authy, Bitwarden
- Generates a 6-digit code that rotates every 30 seconds
- No hardware required; a smartphone is sufficient
- Recovery codes generated at enrollment time

**WebAuthn**
- Works with hardware security keys (YubiKey, Nitrokey) and platform authenticators (Windows Hello, Touch ID, passkeys)
- Phishing-resistant by design — the credential is bound to the exact origin URL
- Requires browser WebAuthn support (all modern browsers qualify)
- Best for high-security environments or users who travel with a hardware key

For most homelabs, TOTP is the right starting point. WebAuthn is worth adding if you have a YubiKey or want true phishing resistance beyond what TOTP provides.

## Prerequisites

Before starting, confirm you have:

- Proxmox VE 7.0 or later (VE 8/9 recommended — the 2FA UI is more polished)
- Access to the web UI at `https://your-proxmox-ip:8006`
- A login with `root@pam` or a user holding `Sys.Modify` on `/`
- A TOTP app installed on your phone (Aegis on Android is excellent; any RFC 6238-compliant app works)

For WebAuthn you'll additionally need a WebAuthn-compatible hardware key or a platform authenticator, plus a working HTTPS connection with a consistent hostname.

## Setting Up TOTP Authentication

TOTP is the easiest 2FA method to enable and works on any device with an authenticator app.

### Step 1: Open the Two-Factor Panel

1. Log into the Proxmox web UI
2. Click **Datacenter** in the left panel
3. Navigate to **Permissions → Two Factor**

This panel lets you configure global WebAuthn settings and see which users have factors enrolled.

### Step 2: Enroll TOTP for Your Account

1. Click your username in the top-right corner, then select **My Settings**
2. Under **Two Factor Authentication**, click **Add**
3. Select **TOTP** from the method dropdown
4. A QR code appears — scan it with your authenticator app
5. Enter the 6-digit code your app shows to verify the enrollment
6. **Copy your recovery keys and store them offline** — you will need these if you lose your phone

From this point on, every login for that account will prompt for a TOTP code after the password step.

### Step 3: Test Before Moving On

Log out completely, then log back in. After entering your password you should see a second prompt for a one-time password. Enter the code from your app.

If the code is rejected, check that your phone's clock is synchronized — TOTP codes fail if the clock drifts more than 30–90 seconds. On the Proxmox host you can confirm NTP sync with:

```bash
timedatectl status
```


Look for `NTP service: active` and a synchronized status. Clock drift on the server side causes the same problem in reverse.

## Setting Up WebAuthn

WebAuthn credentials are origin-bound, meaning a phishing site can't steal your key even if you're tricked into visiting it. That makes it meaningfully stronger than TOTP for anyone handling sensitive infrastructure.

### Step 1: Configure WebAuthn at the Datacenter Level

Before enrolling any keys you must set the relying party parameters:

1. Go to **Datacenter → Permissions → Two Factor**
2. Scroll to the **WebAuthn** section
3. Fill in:
   - **Relying Party Name**: A human-readable label, e.g. `Proxmox Homelab`
   - **ID**: The hostname used to reach Proxmox, e.g. `proxmox.local`
   - **Origin**: The full HTTPS URL including port, e.g. `https://proxmox.local:8006`
4. Click **Apply**

```yaml
# Example values
rpname: "Proxmox Homelab"
rpid: "proxmox.local"
origin: "https://proxmox.local:8006"
```


The `rpid` and `origin` must exactly match the URL you use to access Proxmox. If you change the hostname later, existing WebAuthn credentials will stop working.

### Step 2: Enroll a Security Key

1. Open **My Settings** from the top-right menu
2. Under **Two Factor Authentication**, click **Add**
3. Select **Security Key (WebAuthn)**
4. Give the key a descriptive name, e.g. `YubiKey 5 NFC`
5. Click **Register** — your browser prompts you to interact with the key (touch it, use Touch ID, etc.)
6. Once registered, the key appears in your 2FA list

Enroll at least two keys if you have them — your primary and one backup. A lost hardware key with no backup means falling back to recovery options.

### Step 3: Test the WebAuthn Login

Log out and back in. After the password step, your browser will prompt you to activate your security key. Touch it when the browser requests interaction.

For platform authenticators like Windows Hello you'll be prompted for your PIN or biometrics instead of a physical tap.

## Enforcing 2FA Across All Users

Enabling 2FA for your own account is good. Requiring it for everyone who can touch your hypervisor is better.

Proxmox VE 8+ introduced a **Two-Factor Policy** setting at the datacenter level that blocks dashboard access for any user without a factor enrolled.

### Setting the Datacenter Policy

1. Go to **Datacenter → Options**
2. Find the **Two-Factor Authentication** field
3. Set it to **Required**
4. Click **OK**

Users without 2FA enrolled will be prompted to set it up on their next login and cannot proceed until they do.

> **Important**: Enroll 2FA for `root@pam` before enabling this policy. Enabling it first locks out any account that hasn't enrolled — including your own.

### Checking Per-User Enrollment Status

From the web UI, **Datacenter → Permissions → Two Factor** lists all enrolled factors. From the CLI:

```bash
# List all Proxmox users
pveum user list
```

# Inspect the raw TFA config (contains hashed secrets — handle with care)
cat /etc/pve/priv/tfa.cfg


Never share or expose `tfa.cfg` — it contains the TOTP secrets and WebAuthn credential data for all users.

## API Tokens and 2FA

API tokens (`user@pam!tokenname`) don't support 2FA by design — they're meant for automation. This means token hygiene matters even more once 2FA is enforced for interactive logins.

Best practices for API tokens:

- Grant tokens **minimal permissions** — avoid `Administrator` or `PVEAdmin` roles
- Enable privilege separation so a token can't exceed its own grants
- Store tokens in environment variables or a secrets manager, never in plaintext config files
- Rotate tokens periodically and audit usage in `/var/log/pve/tasks/`

```bash
# Create a scoped token with privilege separation enabled
pveum user token add automation@pve backup-token --privsep 1
```

# Grant only the specific permission needed
pveum acl modify /storage/backups \
  --user automation@pve!backup-token \
  --role PVEDatastoreUser


With `--privsep 1` active, the token can only use permissions explicitly assigned to it — it cannot inherit everything from the parent user account.

## Recovery: Regaining Access After Losing Your Authenticator

This is the scenario most people worry about. Proxmox gives you three recovery paths in order of preference.

### Option 1: Use Your Recovery Keys

When you enrolled TOTP, Proxmox generated single-use recovery codes. If you saved them:

1. On the 2FA login prompt, click **Use recovery key**
2. Enter one of your saved codes
3. Once inside, immediately re-enroll a new TOTP device and generate fresh recovery codes

Store recovery codes in a password manager or printed in a physically secure location — not in the same place as the device you're recovering from.

### Option 2: Remove 2FA via CLI

If you have SSH or console access to the node:

```bash
# Remove all 2FA factors for a user
pveum user tfa delete root@pam
```


After running this, the account can log in with password only. Re-enroll immediately.

### Option 3: Edit the TFA Config Directly

As a last resort with physical console access:

```bash
# Back up first
cp /etc/pve/priv/tfa.cfg /root/tfa.cfg.bak
```

# Edit and remove the affected user's entry
nano /etc/pve/priv/tfa.cfg


The `pveum` CLI method is safer and should be tried before manually editing config files.

## Additional Security Layers to Stack with 2FA

Two-factor authentication is most effective as part of a layered approach:

**Restrict port 8006 by source IP**

Even with 2FA enabled, there's no reason to leave the management interface open to all subnets. Scope access using the Proxmox firewall:

```bash
# Allow web UI access only from your management network
pvesh create /nodes/pve/firewall/rules \
  --action ACCEPT --type in --proto tcp \
  --dport 8006 --source 192.168.1.0/24
```

pvesh create /nodes/pve/firewall/rules \
  --action DROP --type in --proto tcp --dport 8006


**Use a dedicated non-root daily account**

Create a Proxmox-realm admin account for routine work and reserve `root@pam` for break-glass access only:

```bash
pveum user add admin@pve --comment "Daily admin"
pveum acl modify / --user admin@pve --role Administrator
```


Enroll 2FA on the daily account and stop interactive root logins.

**Monitor failed logins**

```bash
# Tail failed authentication attempts in the proxy log
grep -i "authentication failure" /var/log/pveproxy/access.log | tail -20
```


Consider shipping this log to a central SIEM or at minimum checking it weekly.

## Conclusion

Two-factor authentication is one of the highest-value security changes you can make to a Proxmox VE installation. TOTP takes five minutes to set up and works with any authenticator app — there's no valid reason to leave it disabled. WebAuthn raises the bar further with phishing-resistant credentials for anyone handling production infrastructure.

The combination of 2FA, SSH key authentication, scoped API tokens, and Proxmox firewall rules closes the most common attack paths against the management plane. Enable 2FA today, save your recovery codes somewhere physically secure, then layer on the remaining controls. Your future self will appreciate it the next time a password shows up in a breach notification.
]]></content>
        <author>
            <name>Proxmox Pulse</name>
        </author>
        <category label="proxmox"/>
        <category label="security"/>
        <category label="two-factor-authentication"/>
        <category label="totp"/>
        <category label="webauthn"/>
    </entry>
</feed>