In a few of my recent posts I mentioned that I was trying to figure out how to publish my homelab automation code *without accidentally open-sourcing my entire infrastructure along with it* π
Turns out that’s a surprisingly non-trivial problem when your Ansible inventory looks like a cyberpunk treasure chest full of vaults, secrets and host-specific variables. ππ
After a bit of trial, error, caffeine and questionable life choices⦠I finally found a setup that works beautifully:
π two separate git remotes.
A **private self-hosted Gitea instance** keeps the *real* repository β including encrypted secrets, vault files and all the spicy internal bits.
Meanwhile a **public GitHub repo** only receives the clean automation code, with sensitive files excluded via `.gitignore` and replaced by friendly `.example` templates. π§Όβ¨
Anything secret is encrypted using **Ansible Vault (AES-256)** anyway β so even if something somehow slips through, all an attacker gets is encrypted gobbledygook. π§ πΎ
π¦ Repo:
π github.com/aptupgrademe/www_k3s
The project provisions either **Nextcloud** or **WordPress** on a fresh **single-node K3s cluster** running on **AlmaLinux 9**.
One command.
Zero click-ops.
Maximum nerd energy. β‘π€
π§ The Idea
Run:
ansible-playbook blog.yml --limit <host> --ask-vault-pass
β¦against a completely fresh VPS and watch it transform itself into a fully hardened, TLS-enabled, monitored WordPress or Nextcloud server like some kind of infrastructure PokΓ©mon evolution. πβ‘οΈπ
No manual SSH fiddling.
No mystery setup steps.
No βTODO: document laterβ graveyard. π
Everything is idempotent β re-run it anytime without nuking your data or summoning configuration drift demons.
βοΈ Stack at a Glance
- ποΈ K3s β Kubernetes without the enterprise trauma
- π F5 nginx-ingress β running as DaemonSet with
hostNetwork: true - π cert-manager β automatic Let’s Encrypt certs because browsers yearn for green locks
- π WordPress 6.9-fpm + nginx sidecar + MariaDB 10.11
- βοΈ Nextcloud 33-fpm + Redis + Collabora CODE for nerd-grade self-hosted office vibes
- π Prometheus + Grafana β because if you can’t graph it, does it even exist?
- π‘οΈ iptables β default DROP, portscan detection, ICMP rate limiting, tiny digital moat π°
- π Fail2Ban, auditd, rkhunter, dnf-automatic β paranoia as codeβ’
π What One Playbook Run Actually Does
- π Hardens SSH β custom port, key-only auth, tighter configs
- π₯ Builds IPv4 + IPv6 firewall rules with portscan detection via
xt_recent - π§± Enables SELinux enforcing with correct container contexts
- βΈοΈ Installs K3s, Helm, cert-manager and nginx-ingress
- π¦ Deploys the full Kubernetes workload stack automatically
- π Configures WordPress via WP-CLI like a civilized automation goblin
- βοΈ Configures Nextcloud via
occincluding Redis, SMTP and trusted domains - π Sets up monitoring exporters + Prometheus scraping + Grafana dashboards
- π‘οΈ Deploys Fail2Ban, auditd, rkhunter scans and unattended security updates
- β Probably consumes less coffee than I did while debugging it
π Secrets & Security
All credentials are encrypted using Ansible Vault (AES-256).
Sensitive files like:
vault.ymlvars.ymlhosts.yml
β¦never touch the public repository. π«π
The GitHub repo only contains reusable automation code and `.example` templates:
vars.yml.example
vault.yml.example
hosts.yml.example
Clone β fill in values β encrypt β deploy. β¨
π Things That Were Way Harder Than Expected
-
- Let’s Encrypt HTTP-01 challenge returning 301
Turns out redirecting everything to HTTPS also redirects the ACME challenge. Who could have guessed? π
Fixed using F5 Mergeable Ingresses (master/minion pattern) so/.well-known/bypasses redirects.
- Let’s Encrypt HTTP-01 challenge returning 301
“`
-
- Infinite HTTPS redirect loop
My first nginx logic basically DDoSed itself philosophically.
Fixed with a cleaner three-variable redirect pattern.
“`
- Infinite HTTPS redirect loop
“`
-
- mysqld_exporter v0.19.0 killed
DATA_SOURCE_NAME
Credentials now need a mounted.my.cnffile.
Also discovered that0600permissions don’t help if the container runs asnobody. π€‘
“`
- mysqld_exporter v0.19.0 killed
“`
-
- iptables OUTPUT DROP broke K3s internals
Because apparently Kubernetes enjoys talking to itself constantly.
Fixed with explicit ACCEPT rules for pod/service CIDRs.
“`
- iptables OUTPUT DROP broke K3s internals
“`
- Grafana dashboard 1860 showed βNo dataβ
Modern Grafana dashboards no longer contain__inputs.
Solution: tiny Python patcher script talking to the Grafana API like a little automation goblin.
“`
π§ͺ Try It Yourself
git clone https://github.com/aptupgrademe/www_k3s.git
cd www_k3s
cp inventory/hosts.yml.example inventory/hosts.yml
cp inventory/host_vars/test/vars.yml.example inventory/host_vars/myhost/vars.yml
# fill in your values, then:
ansible-vault encrypt inventory/host_vars/myhost/vault.yml
ansible-playbook blog.yml --limit myhost --ask-vault-pass
The full quickstart, role overview and variable documentation live in the GitHub README.
PRs, feedback and fellow infrastructure nerds welcome. ππ§βΈοΈ
