Posted on :: Updated on ::

This is a living document.

  • Serves as a reference for my homelab.
  • Helps me grow my technical documentation skills.

Hosts

Physical

  • pve-01.home.majabojarska.dev — Lenovo Tiny M920q
    • Role: Main hypervisor
    • CPU: i5-8500T (6 x 2.1GHz)
    • RAM: 32GB DDR4 SODIMM, hoping to expand to 64GB once prices fall a bit (one can hope).
    • Storage:
      • OS, disk images: 2TB M.2 NVMe
      • PCIe passthrough to virtual guests: 3 x 1TB 2.5“ SATA SSD
  • pve-02.home.majabojarska.dev — Dell Optiplex 7020
    • Role: Experimental hypervisor, spinner array host
    • CPU: i5-4590 (4 x 3.3GHz)
    • RAM: 24GB DDR3 DIMM
    • Storage:
      • OS, disk images: 120GB 2.5“ SATA SSD
      • General purpose storage, VM passthrough:
        • 2 x 2TB 3.5“ SATA HDD
        • 1 x 640GB 3.5“ SATA HDD
  • pve-03.home.majabojarska.dev — Lenovo Tiny M720q
    • Role: Secondary hypervisor, backup in case of pve-01 failure.
    • CPU: i3-6100T (3 x 3.2GHz)
    • RAM: 8GB DDR4 SODIMM
    • Storage:
      • OS, disk images: 240GB 2.5“ SATA SSD

Virtual

  • opnsense.home.majabojarska.dev
    • Role: homelab router
    • Hypervisor: pve-01.home.majabojarska.dev
  • kube-01.home.majabojarska.dev
    • Role: Single-node Kubernetes (K3s) cluster, bulk of self-hosted services.
    • Hypervisor: pve-01.home.majabojarska.dev
  • hass.home.majabojarska.dev
  • nas.home.majabojarska.dev
    • Role: NAS
    • Hypervisor: pve-02.home.majabojarska.dev
  • majabojarska.dev
    • Role: general purpose VPS
    • Hypervisor: Linode Cloud

Networking

Devices

  • Netgear GS308E

    • Handles port-based, VLAN-aware switching (802.1Q).
    • Facilitates running a router over a single ethernet interface in a secure fashion, via tagged VLANs.
  • Archer C6 v2

    • Dumb, VLAN-aware AP with multiple software-defined APs
    • 2.4GHz, 5GHz
    • Uplinked to the router a single trunk with tagged VLANs.
  • OPNsense, virtualized on pve-01.home.majabojarska.dev.

VLANs

IDRole
0 (untagged)Homelab
10IoT network
20Guest network

Addressing

NetworkIPv4 range (CIDR)GatewayHas DHCP?
Homelab192.168.1.0/24192.168.1.1Yes
IoT192.168.10.0/24192.168.10.1Yes
Guest192.168.20.0/24192.168.20.1Yes

DNS

  • majabojarska.dev is the root domain for all my infrastructure needs.
  • home.majabojarska.dev always points to 192.168.1.1 (intranet), which serves as the recursive resolver for any matching subdomains (wildcard).
  • Tailscale is set up to use 192.168.1.1 as the resolver (one of). Meaning, remote VPN session resolve homelab domains just fine.
  • 192.168.1.1 resolves outside domains via DoH, with multiple upstream resolvers configured for redundancy.
  • home.majabojarska.dev enforces DNS blocklist policies for ads, spam, and digital trash that I do not wish to resolve successfully.

VPN

The OPNsense router handles all VPN routing, so that client devices can run light at home.

NTP

home.majabojarska.dev runs the Chrony NTP server at the standard port 123.

Storage

🚧

This section will describe the different storage pools/devices on different hosts, and their purposes.

Backups

  • Personal dev machines
    • Method: Vorta (Borg Backup) to a dedicated ZFS location on nas.home.majabojarska.dev.
    • Schedule: weekly
    • Trigger: manual, mostly due to nas.home.majabojarska.dev being powered off most of the time to limit energy consumption.
  • Kubernetes PVs, ETCD snapshots:
    • Method: Borgmatic
    • Schedule: nightly
    • Trigger: midnight cron
    • Notes:
      • As a pre-backup script, Borgmatic triggers an ETCD snapshot (to filesystem), and then cordons, drains, and stops the K3s systemd unit.
      • Backing up PVs and ETCD snapshots together ensures that upon disaster recovery, the “state of the world” is congruent between the cluster’s state, and the applications’ state.
  • OPNsense configuration is version tracked in a self-hosted Git instance.
    • Method: Git plugin
    • Schedule: N/A
    • Trigger: any configuration change

Secret management

Ansible

Ansible secrets are encrypted via [ansible-vault](ansible-vault decrypt group_vars/all/secrets.yaml).

# Decrypt
ansible-vault decrypt group_vars/all/secrets.yaml

# Encrypt
ansible-vault encrypt group_vars/all/secrets.yaml

The encryption key is not tracked by VCS, but it’s kept in the homelab’s password manager.

When cloning the repo on a new machine, place the key at <repo_root>/.vault_pass (defined via ansible.cfg).

NixOS hosts

  • Secrets for NixOS hosts are encrypted at rest via agenix.
    • Effectively, the encryption is based on SSH key pairs.
  • During deployment, they’re shipped encrypted, and decrypted on the target host, with its own private key.
  • For each host: both the developer’s (mine), and the target host’s public keys are enrolled in the encryption scheme. In consequence, any of the corresponding private keys can be used to decrypt the secret, in order to update its contents.

The implementation is based on the NixOS Agenix documentation.

To edit an age secret or create a new one:

# From dir containing 'secrets.nix'
# Alternatively, specify the path to the 'secrets.nix' file via the RULES env var.
nix run github:ryantm/agenix -- -e foo-token.age

Kubernetes

  • Kubernetes Secret objects are provisioned by the External Secrets Operator.
  • ExternalSecrets define the Secrets to be provisioned (and managed).
    • Think of them as Secret recipes.
    • They only contain entry IDs and field names, referencing the backing secret store.
  • Bitwarden is the secret store of choice, and it’s defined via a ClusterSecretStore resource (cluster-wide).
  • No plain-text secrets are stored in the infra Git repository, nor should they ever be.
  • Bitwarden connectivity is provided by a Bitwarden CLI instance, running in-cluster, in HTTP server mode.
  • Network connectivity between ESO components and the Bitwarden server is governed by a NetworkPolicy, and enforced by the Flannel CNI.
  • Whenever new secrets are added, ESO might need a couple minutes to complete the secret reconciliation. Use longer timeouts to account for this when deploying new components requiring Secrets.

SOPS

As of 2025-12-20, secrets are being migrated over to SOPS backed by age, deployed via FluxCD.

Most notably:

Handy ~/.zshrc snippet to aid with secret handling:

export SOPS_AGE_KEY_FILE="${HOME}/.sops/age.agekey"
alias sops-age-encrypt="sops --encrypt --age $(cat $SOPS_AGE_KEY_FILE | grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' --in-place"
alias sops-age-decrypt="sops --decrypt --age $(cat $SOPS_AGE_KEY_FILE | grep -oP "public key: \K(.*)") --encrypted-regex '^(data|stringData)$' --in-place"

Future plans & ongoing work

Services

Networking

Hardware

Disk bays

Planning to migrate my 2.5“/3.5“ disks to a PCIe SATA controller. I’ll use the following designs for rack-mounting the disks:

Table of Contents