diff --git a/jekyll/_posts/infrastructure-snapshot/2023-08-13-infrastructure-snapshot.md b/jekyll/_posts/infrastructure-snapshot/2023-08-13-infrastructure-snapshot.md
new file mode 100644
index 0000000..f9abcb2
--- /dev/null
+++ b/jekyll/_posts/infrastructure-snapshot/2023-08-13-infrastructure-snapshot.md
@@ -0,0 +1,283 @@
+---
+layout: post
+title: Home Lab Infrastructure Snapshot August 2023
+date: 2023-08-27 22:23:00 Europe/Amsterdam
+categories: infrastructure homelab
+---
+
+I have been meaning to write about the current state of my home lab infrastructure for a while now.
+Now that the most important parts are quite stable, I think the opportunity is ripe.
+I expect this post to get quite long, so I might have to leave out some details along the way.
+
+This post will be a starting point for future infrastructure snapshots which I can hopefully put out periodically.
+That is, if there is enough worth talking about.
+
+Keep an eye out for the icon, which links to the source code and configuration of anything mentioned.
+Oh yeah, did I mention everything I do is open source?
+
+# Networking and Infrastructure Overview
+
+## Hardware and Operating Systems
+
+Let's start with the basics: what kind of hardware do I use for my home lab?
+The most important servers are my three [Gigabyte Brix GB-BLCE-4105](https://www.gigabyte.com/Mini-PcBarebone/GB-BLCE-4105-rev-10).
+Two of them have 16 GB of memory, and one 8 GB.
+I named these servers as follows:
+- **Atlas**: because this server was going to "lift" a lot of virtual machines.
+- **Lewis**: we started out with a "Max" server named after the Formula 1 driver Max Verstappen, but it kind of became an unmanagable behemoth without infrastructure-as-code. Our second server we subsequently named Lewis after his colleague Lewis Hamilton. Note: people around me vetoed these names and I am no F1 fan!
+- **Jefke**: it's a funny Belgian name. That's all.
+
+Here is a picture of them sitting in their cosy closet:
+
+![A picture of my servers.](servers.jpeg)
+
+If you look look to the left, you will also see a Raspberry pi 4B.
+I use this Pi to do some rudimentary monitoring whether servers and services are running.
+More on this in the relevant section below.
+The Pi is called **Iris** because it's a messenger for the other servers.
+
+I used to run Ubuntu on these systems, but I have since migrated away to Debian.
+The main reasons were Canonical [putting advertisements in my terminal](https://askubuntu.com/questions/1434512/how-to-get-rid-of-ubuntu-pro-advertisement-when-updating-apt) and pushing Snap which has a [proprietry backend](https://hackaday.com/2020/06/24/whats-the-deal-with-snap-packages/).
+Two of my servers run the newly released Debian Bookworm, while one still runs Debian Bullseye.
+
+## Networking
+
+For networking, I wanted hypervisors and virtual machines separated by VLANs for security reasons.
+The following picture shows a simplified view of the VLANs present in my home lab:
+
+![Picture showing the VLANS in my home lab.](vlans.png)
+
+All virtual machines are connected to a virtual bridge which tags network traffic with the DMZ VLAN.
+The hypervisors VLAN is used for traffic to and from the hypervisors.
+Devices from the hypervisors VLAN are allowed to connect to devices in the DMZ, but not vice versa.
+The hypervisors are connected to a switch using a trunk link, allows both DMZ and hypervisors traffic.
+
+I realised the above design using ifupdown.
+Below is the configuration for each hypervisor, which creates a new `enp3s0.30` interface with all DMZ traffic from the `enp3s0` interface [](https://git.kun.is/home/hypervisors/src/commit/71b96d462116e4160b6467533fc476f3deb9c306/ansible/dmz.conf.j2).
+
+```text
+auto enp3s0.30
+iface enp3s0.30 inet manual
+iface enp3s0.30 inet6 auto
+ accept_ra 0
+ dhcp 0
+ request_prefix 0
+ privext 0
+ pre-up sysctl -w net/ipv6/conf/enp3s0.30/disable_ipv6=1
+```
+
+This configuration seems more complex than it actually is.
+Most of it is to make sure the interface is not assigned an IPv4/6 address on the hypervisor host.
+The magic `.30` at the end of the interface name makes this interface tagged with VLAN ID 30 (DMZ for me).
+
+Now that we have an interface tagged for the DMZ VLAN, we can create a bridge where future virtual machines can connect to:
+
+```text
+auto dmzbr
+iface dmzbr inet manual
+ bridge_ports enp3s0.30
+ bridge_stp off
+iface dmzbr inet6 auto
+ accept_ra 0
+ dhcp 0
+ request_prefix 0
+ privext 0
+ pre-up sysctl -w net/ipv6/conf/dmzbr/disable_ipv6=1
+```
+
+Just like the previous config, this is quite bloated because I don't want the interface to be assigned an IP address on the host.
+Most importantly, the `bridge_ports enp3s0.30` line here makes this interface a virtual bridge for the `enp3s0.30` interface.
+
+And voilĂ , we now have a virtual bridge on each machine, where only DMZ traffic will flow.
+Here I verify whether this configuration works:
+
+ Show
+
+
+We can see that the two virtual interfaces are created, and are only assigned a MAC address and not a IP address:
+```text
+root@atlas:~# ip a show enp3s0.30
+4: enp3s0.30@enp3s0: mtu 1500 qdisc noqueue master dmzbr state UP group default qlen 1000
+ link/ether d8:5e:d3:4c:70:38 brd ff:ff:ff:ff:ff:ff
+5: dmzbr: mtu 1500 qdisc noqueue state UP group default qlen 1000
+ link/ether 4e:f7:1f:0f:ad:17 brd ff:ff:ff:ff:ff:ff
+```
+
+Pinging a VM from a hypervisor works:
+```text
+root@atlas:~# ping -c1 maestro.dmz
+PING maestro.dmz (192.168.30.8) 56(84) bytes of data.
+64 bytes from 192.168.30.8 (192.168.30.8): icmp_seq=1 ttl=63 time=0.457 ms
+```
+
+Pinging a hypervisor from a VM does not work:
+```text
+root@maestro:~# ping -c1 atlas.hyp
+PING atlas.hyp (192.168.40.2) 56(84) bytes of data.
+
+--- atlas.hyp ping statistics ---
+1 packets transmitted, 0 received, 100% packet loss, time 0ms
+```
+
+
+## DNS and DHCP
+
+Now that we have a working DMZ network, let's build on it to get DNS and DHCP working.
+This will enable new virtual machines to obtain a static or dynamic IP address and register their host in DNS.
+This has actually been incredibly annoying due to our friend [Network address translation (NAT)](https://en.wikipedia.org/wiki/Network_address_translation?useskin=vector).
+
+ NAT recap
+
+Network address translation (NAT) is a function of a router which allows multiple hosts to share a single IP address.
+This is needed for IPv4, because IPv4 addresses are scarce and usually one household is only assigned a single IPv4 address.
+This is one of the problems IPv6 attempts to solve (mainly by having so many IP addresses that they should never run out).
+To solve the problem for IPv4, each host in a network is assigned a private IPv4 address, which can be reused for every network.
+
+Then, the router must perform address translation.
+It does this by keeping track of ports opened by hosts in its private network.
+If a packet from the internet arrives at the router for such a port, it forwards this packet to the correct host.
+
+
+I would like to host my own DNS on a virtual machine (called **hermes**, more on VMs later) in the DMZ network.
+This basically gives two problems:
+
+1. The upstream DNS server will refer to the public internet-accessible IP address of our DNS server.
+This IP-address has no meaning inside the private network due to NAT and the router will reject the packet.
+2. Our DNS resolves hosts to their public internet-accessible IP address.
+This is similar to the previous problem as the public IP address has no meaning.
+
+The first problem can be remediated by overriding the location of the DNS server for hosts inside the DMZ network.
+This can be achieved on my router, which uses Unbound as its recursive DNS server:
+
+![Unbound overides for kun.is and dmz domains.](unbound_overrides.png)
+
+Any DNS requests to Unbound to domains in either `dmz` or `kun.is` will now be forwarded `192.168.30.7` (port 5353).
+This is the virtual machine hosting my DNS.
+
+The second problem can be solved at the DNS server.
+We need to do some magic overriding, which [dnsmasq](https://dnsmasq.org/docs/dnsmasq-man.html) is perfect for [](https://git.kun.is/home/hermes/src/commit/488024a7725f2325b8992e7a386b4630023f1b52/ansible/roles/dnsmasq/files/dnsmasq.conf):
+
+```conf
+alias=84.245.14.149,192.168.30.8
+server=/kun.is/192.168.30.7
+```
+
+This always overrides the public IPv4 address to the private one.
+It also overrides the DNS server for `kun.is` to `192.168.30.7`.
+
+Finally, behind the dnsmasq server, I run [Powerdns](https://www.powerdns.com/) as authoritative DNS server [](https://git.kun.is/home/hermes/src/branch/master/ansible/roles/powerdns).
+I like this DNS server because I can manage it with Terraform [](https://git.kun.is/home/hermes/src/commit/488024a7725f2325b8992e7a386b4630023f1b52/terraform/dns/kun_is.tf).
+
+Here is a small diagram showing my setup (my networking teacher would probably kill me for this):
+![Shitty diagram showing my DNS setup.](nat.png)
+
+# Virtualization
+https://github.com/containrrr/shepherd
+Now that we have laid out the basic networking, let's talk virtualization.
+Each of my servers are configured to run KVM virtual machines, orchestrated using Libvirt.
+Configuration of the physical hypervisor servers, including KVM/Libvirt is done using Ansible.
+The VMs are spun up using Terraform and the [dmacvicar/libvirt](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs) Terraform provider.
+
+This all isn't too exciting, except that I created a Terraform module that abstracts the Terraform Libvirt provider for my specific scenario [](https://git.kun.is/home/tf-modules/src/commit/e77d62f4a2a0c3847ffef4434c50a0f40f1fa794/debian/main.tf):
+```terraform
+module "maestro" {
+ source = "git::https://git.kun.is/home/tf-modules.git//debian"
+ name = "maestro"
+ domain_name = "tf-maestro"
+ memory = 10240
+ mac = "CA:FE:C0:FF:EE:08"
+}
+```
+
+This automatically creates a Debian virtual machines with the properties specified.
+It also sets up certificate-based SSH authentication which I talked about [before]({% post_url homebrew-ssh-ca/2023-05-23-homebrew-ssh-ca %}).
+
+# Clustering
+
+With virtualization explained, let's move up one level further.
+Each of my three physical servers hosts a virtual machine running Docker, which together form a Docker Swarm.
+I use Traefik as a reverse proxy which routes requests to the correct container.
+
+All data is hosted on a single machine and made available to containers using NFS.
+This might not be very secure (as NFS is not encrypted and no proper authentication), it is quite fast.
+
+As of today, I host the following services on my Docker Swarm [](https://git.kun.is/home/shoarma):
+- [Forgejo](https://forgejo.org/) as Git server
+- [FreshRSS](https://www.freshrss.org/) as RSS aggregator
+- [Hedgedoc](https://hedgedoc.org/) as Markdown note-taking
+- [Inbucket](https://hedgedoc.org/) for disposable email
+- [Cyberchef](https://cyberchef.org/) for the lulz
+- [Kitchenowl](https://kitchenowl.org/) for grocery lists
+- [Mastodon](https://joinmastodon.org/) for microblogging
+- A monitoring stack (read more below)
+- [Nextcloud](https://nextcloud.com/) for cloud storage
+- [Pihole](https://pi-hole.net/) to block advertisements
+- [Radicale](https://radicale.org/v3.html) for calendar and contacts sync
+- [Seafile](https://www.seafile.com/en/home/) for cloud storage and sync
+- [Shephard](https://github.com/containrrr/shepherd) for automatic container updates
+- [Nginx](https://nginx.org/en/) hosting static content (like this page!)
+- [Docker Swarm dashboard](https://hub.docker.com/r/charypar/swarm-dashboard/#!)
+- [Syncthing](https://syncthing.net/) for file sync
+
+# CI / CD
+
+For CI / CD, I run [Concourse CI](https://concourse-ci.org/) in a separate VM.
+This is needed, because Concourse heavily uses containers to create reproducible builds.
+
+Although I should probably use it for more, I currently use my Concourse for three pipelines:
+
+- A pipeline to build this static website and create a container image of it.
+The image is then uploaded to the image registry of my Forgejo instance.
+I love it when I can use stuff I previously built :)
+The pipeline finally deploys this new image to the Docker Swarm [](https://git.kun.is/pim/static/src/commit/eee4f0c70af6f2a49fabb730df761baa6475db22/pipeline.yml).
+- A pipeline to create a Concourse resource that sends Apprise alerts (Concourse-ception?) [](https://git.kun.is/pim/concourse-apprise-notifier/src/commit/b5d4413c1cd432bc856c45ec497a358aca1b8b21/pipeline.yml)
+- A pipeline to build a custom Fluentd image with plugins installed [](https://git.kun.is/pim/fluentd)
+
+# Backups
+
+To create backups, I use [Borg](https://www.borgbackup.org/).
+As I keep all data on one machine, this backup process is quite simple.
+In fact, all this data is stored in a single Libvirt volume.
+To configure Borg with a simple declarative script, I use [Borgmatic](https://torsion.org/borgmatic/).
+
+In order to back up the data inside the Libvirt volume, I create a snapshot to a file.
+Then I can mount this snapshot in my file system.
+The files can then be backed up while the system is still running.
+It is also possible to simply back up the Libvirt image, but this takes more time and storage [](https://git.kun.is/home/hypervisors/src/commit/71b96d462116e4160b6467533fc476f3deb9c306/ansible/roles/borg/backup.yml.j2).
+
+# Monitoring and Alerting
+
+The last topic I would like to talk about is monitoring and alerting.
+This is something I'm still actively improving and only just set up properly.
+
+## Alerting
+
+For alerting, I wanted something that runs entirely on my own infrastructure.
+I settled for Apprise + Ntfy.
+
+[Apprise](https://github.com/caronc/apprise) is a server that is able to send notifications to dozens of services.
+For application developers, it is thus only necessary to implement the Apprise API to gain access to all these services.
+The Apprise API itself is also very simple.
+By using Apprise, I can also easily switch to another notification service later.
+[Ntfy](https://ntfy.sh/) is free software made for mobile push notifications.
+
+I use this alerting system in quite a lot of places in my infrastructure, for example when creating backups.
+
+## Uptime Monitoring
+
+The first monitoring setup I created, was using [Uptime Kuma](https://github.com/louislam/uptime-kuma).
+Uptime Kuma periodically pings a service to see whether it is still running.
+You can do a literal ping, test HTTP response codes, check database connectivity and much more.
+I use it to check whether my services and VMs are online.
+And the best part is, Uptime Kuma supports Apprise so I get push notifications on my phone whenever something goes down!
+
+## Metrics and Log Monitoring
+
+A new monitoring system I am still in the process of deploying is focused on metrics and logs.
+I plan on creating a separate blog post about this, so keep an eye out on that (for example using RSS :)).
+Safe to say, it is no basic ELK stack!
+
+# Conclusion
+
+That's it for now!
+Hopefully I inspired someone to build something... or how not to :)
diff --git a/jekyll/_posts/infrastructure-snapshot/nat.png b/jekyll/_posts/infrastructure-snapshot/nat.png
new file mode 100644
index 0000000..0d5f72c
Binary files /dev/null and b/jekyll/_posts/infrastructure-snapshot/nat.png differ
diff --git a/jekyll/_posts/infrastructure-snapshot/servers.jpeg b/jekyll/_posts/infrastructure-snapshot/servers.jpeg
new file mode 100644
index 0000000..b269484
Binary files /dev/null and b/jekyll/_posts/infrastructure-snapshot/servers.jpeg differ
diff --git a/jekyll/_posts/infrastructure-snapshot/unbound_overrides.png b/jekyll/_posts/infrastructure-snapshot/unbound_overrides.png
new file mode 100644
index 0000000..f94394f
Binary files /dev/null and b/jekyll/_posts/infrastructure-snapshot/unbound_overrides.png differ
diff --git a/jekyll/_posts/infrastructure-snapshot/vlans.png b/jekyll/_posts/infrastructure-snapshot/vlans.png
new file mode 100644
index 0000000..7bf4add
Binary files /dev/null and b/jekyll/_posts/infrastructure-snapshot/vlans.png differ