From 2f0d0b5e4c57ddffe0e03d722c8b57a2d02f6392 Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Wed, 15 Apr 2026 17:18:12 +0300 Subject: [PATCH] Port to NixOS: replace Helm chart with flake-based NixOS config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Helm/k3s setup with a declarative NixOS configuration targeting a Raspberry Pi 4. Services run as podman containers under systemd, with data on an external HD at /mnt/data. Key components: - flake.nix: multi-host flake with pi-main (aarch64) and a placeholder for a second machine - modules/common.nix: shared system config (nix, podman, sops, SSH) - modules/storage.nix: external HD mount with per-service subdirs - modules/caddy.nix: Caddy with cloudflare DNS-01 ACME + authelia forward_auth - modules/cloudflared.nix: Cloudflare tunnel for remote access - modules/backup.nix: restic daily backups with NC maintenance mode pre-hook - modules/services/{openldap,authelia,gitea,nextcloud,phpldapadmin}.nix: core services - modules/services/{jellyfin,transmission}.nix: media services (disabled by default) - secrets/: sops-nix scaffold with .sops.yaml age key config - hosts/pi-main/: hardware config + service selection for the Pi - PORTING.md: step-by-step migration guide (SD card → data restore → verify) --- Chart.yaml | 6 - PORTING.md | 400 +++++++++++++++++ README.org | 115 ----- backup-longhorn-volumes.sh | 60 --- backup-nextcloud.sh | 120 ------ copy-longhorn-from-hd.sh | 41 -- files/authelia-config.yaml | 87 ---- files/baikal-server.php | 30 -- files/davical-config.php | 580 ------------------------- files/gitea-app.ini | 95 ---- files/radicale-configmap.ini | 11 - files/sabre-server.php | 30 -- files/sogo.conf | 94 ---- flake.nix | 73 ++++ get-secret-val.sh | 1 - gitea-commands.txt | 10 - hosts/pi-main/default.nix | 83 ++++ hosts/pi-main/hardware.nix | 84 ++++ modules/backup.nix | 150 +++++++ modules/caddy.nix | 185 ++++++++ modules/cloudflared.nix | 77 ++++ modules/common.nix | 117 +++++ modules/services/authelia.nix | 200 +++++++++ modules/services/gitea.nix | 198 +++++++++ modules/services/jellyfin.nix | 55 +++ modules/services/nextcloud.nix | 135 ++++++ modules/services/openldap.nix | 116 +++++ modules/services/phpldapadmin.nix | 46 ++ modules/services/transmission.nix | 61 +++ modules/storage.nix | 105 +++++ scripts/backup-longhorn-to-disk.sh | 184 -------- scripts/get-pvc-mapping.sh | 16 - scripts/list-longhorn-backups.sh | 37 -- scripts/longhorn-fuse.py | 70 --- scripts/longhorn-nbdkit.py | 49 --- scripts/mount-longhorn-volume.sh | 146 ------- scripts/restore-fast.py | 48 --- scripts/restore-longhorn-backup.sh | 94 ---- scripts/restore-longhorn-volume.sh | 135 ------ secrets/.gitignore | 10 + secrets/.sops.yaml | 24 ++ secrets/secrets.yaml | 54 +++ templates/_definitions.yaml | 27 -- templates/auth.yaml | 668 ----------------------------- templates/media.yaml | 211 --------- templates/phpldapadmin.yaml | 80 ---- unused/auth-templates.yaml | 24 -- unused/baikal.yaml | 117 ----- unused/dav.yaml | 71 --- unused/davical.yaml | 213 --------- unused/gitea.yaml | 131 ------ unused/jellyfin.yaml | 92 ---- unused/ldap-auth.yaml | 70 --- unused/nextcloud.yaml | 206 --------- unused/paperless.yaml | 230 ---------- unused/radicale.yaml | 122 ------ unused/sabre.yaml | 118 ----- unused/sogo.yaml | 162 ------- values.yaml | 65 --- 59 files changed, 2173 insertions(+), 4666 deletions(-) delete mode 100644 Chart.yaml create mode 100644 PORTING.md delete mode 100644 README.org delete mode 100755 backup-longhorn-volumes.sh delete mode 100755 backup-nextcloud.sh delete mode 100755 copy-longhorn-from-hd.sh delete mode 100644 files/authelia-config.yaml delete mode 100644 files/baikal-server.php delete mode 100644 files/davical-config.php delete mode 100644 files/gitea-app.ini delete mode 100644 files/radicale-configmap.ini delete mode 100644 files/sabre-server.php delete mode 100644 files/sogo.conf create mode 100644 flake.nix delete mode 100644 get-secret-val.sh delete mode 100644 gitea-commands.txt create mode 100644 hosts/pi-main/default.nix create mode 100644 hosts/pi-main/hardware.nix create mode 100644 modules/backup.nix create mode 100644 modules/caddy.nix create mode 100644 modules/cloudflared.nix create mode 100644 modules/common.nix create mode 100644 modules/services/authelia.nix create mode 100644 modules/services/gitea.nix create mode 100644 modules/services/jellyfin.nix create mode 100644 modules/services/nextcloud.nix create mode 100644 modules/services/openldap.nix create mode 100644 modules/services/phpldapadmin.nix create mode 100644 modules/services/transmission.nix create mode 100644 modules/storage.nix delete mode 100755 scripts/backup-longhorn-to-disk.sh delete mode 100644 scripts/get-pvc-mapping.sh delete mode 100755 scripts/list-longhorn-backups.sh delete mode 100644 scripts/longhorn-fuse.py delete mode 100644 scripts/longhorn-nbdkit.py delete mode 100755 scripts/mount-longhorn-volume.sh delete mode 100644 scripts/restore-fast.py delete mode 100755 scripts/restore-longhorn-backup.sh delete mode 100755 scripts/restore-longhorn-volume.sh create mode 100644 secrets/.gitignore create mode 100644 secrets/.sops.yaml create mode 100644 secrets/secrets.yaml delete mode 100644 templates/_definitions.yaml delete mode 100644 templates/auth.yaml delete mode 100644 templates/media.yaml delete mode 100644 templates/phpldapadmin.yaml delete mode 100644 unused/auth-templates.yaml delete mode 100644 unused/baikal.yaml delete mode 100644 unused/dav.yaml delete mode 100644 unused/davical.yaml delete mode 100644 unused/gitea.yaml delete mode 100644 unused/jellyfin.yaml delete mode 100644 unused/ldap-auth.yaml delete mode 100644 unused/nextcloud.yaml delete mode 100644 unused/paperless.yaml delete mode 100644 unused/radicale.yaml delete mode 100644 unused/sabre.yaml delete mode 100644 unused/sogo.yaml delete mode 100644 values.yaml diff --git a/Chart.yaml b/Chart.yaml deleted file mode 100644 index 369173c..0000000 --- a/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: homey -description: Deploy a fancy home environment! -type: application -version: 0.1.0 -appVersion: "1.16.0" diff --git a/PORTING.md b/PORTING.md new file mode 100644 index 0000000..cffd3b7 --- /dev/null +++ b/PORTING.md @@ -0,0 +1,400 @@ +# Porting Guide — Helm/k3s → NixOS + +This document walks through setting up the new NixOS-based home server from +scratch on a Raspberry Pi 4, restoring data from old Longhorn volumes, and +verifying each service. + +--- + +## Prerequisites (on your workstation) + +- `nix` with flakes enabled (`~/.config/nix/nix.conf`: `experimental-features = nix-command flakes`) +- `sops` + `age` CLI tools (`nix-shell -p sops age`) +- An SSH key pair + +--- + +## Phase 0 — Secrets + +### 0.1 Generate your age key (workstation) + +```bash +age-keygen -o ~/.config/sops/age/keys.txt +age-keygen -y ~/.config/sops/age/keys.txt # print public key +``` + +You will add the Pi's public key in step 2.2; for now add your workstation +public key so you can edit the secrets file offline. + +Edit `secrets/.sops.yaml`, replace the placeholder with your workstation pubkey: + +```yaml +- age: + - AGE-PUBLIC-KEY-YOUR-WORKSTATION # ← paste here +``` + +### 0.2 Fill in secrets.yaml + +`secrets/secrets.yaml` is a **plaintext template** — do not commit it until +encrypted. Fill in all values: + +| Key | Source | +|-----|--------| +| `openldap/admin_password` | From old k8s secret `openldap-admin` | +| `openldap/config_password` | From old k8s secret `openldap-config` | +| `openldap/ro_password` | From old k8s secret `openldap-ro` | +| `gitea/admin_password` | From old k8s secret `gitea-admin-pass` | +| `nextcloud/admin_password` | From old k8s secret `nextcloud-admin-pass` | +| `nextcloud/postgres_password` | From old k8s secret `nextcloud-postgres-pass` | +| `authelia/jwt_secret` | Generate: `openssl rand -hex 64` | +| `authelia/session_secret` | Generate: `openssl rand -hex 64` | +| `authelia/storage_encryption_key` | Generate: `openssl rand -hex 64` | +| `gitea/lfs_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` | +| `gitea/oauth2_jwt_secret` | Generate: `openssl rand -base64 32 \| tr -d '='` | +| `gitea/internal_token` | Generate: `openssl rand -base64 75 \| tr -d '\n='` | +| `cloudflare/api_token` | Cloudflare dashboard → API Tokens → DNS:Edit | +| `cloudflare/tunnel_token` | Created in Phase 3 (Cloudflare setup) | +| `restic/password` | Generate: `openssl rand -base64 32` | + +To get old k8s secrets (if the cluster is still running): + +```bash +kubectl get secret openldap-admin -n homey -o jsonpath='{.data.password}' | base64 -d +kubectl get secret openldap-config -n homey -o jsonpath='{.data.password}' | base64 -d +kubectl get secret openldap-ro -n homey -o jsonpath='{.data.password}' | base64 -d +kubectl get secret gitea-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d +kubectl get secret nextcloud-admin-pass -n homey -o jsonpath='{.data.password}' | base64 -d +kubectl get secret nextcloud-postgres-pass -n homey -o jsonpath='{.data.password}' | base64 -d +``` + +### 0.3 Encrypt secrets.yaml (workstation, before committing) + +```bash +sops --encrypt --in-place secrets/secrets.yaml +git add secrets/secrets.yaml secrets/.sops.yaml +``` + +--- + +## Phase 1 — Install NixOS on the Raspberry Pi 4 + +### 1.1 Flash the SD card + +Download the NixOS aarch64 SD card image: + +``` +https://nixos.org/download#nixos-iso +→ "Raspberry Pi (aarch64) SD card image" +``` + +Flash with: + +```bash +# macOS +sudo dd if=nixos-*-aarch64-linux.img of=/dev/rdiskN bs=4m status=progress + +# Linux +sudo dd if=nixos-*-aarch64-linux.img of=/dev/sdX bs=4M status=progress conv=fsync +``` + +Label the partitions to match `hardware.nix`: + +```bash +# After flashing, mount the root partition and relabel if needed: +sudo e2label /dev/sdX2 NIXOS_SD +sudo fatlabel /dev/sdX1 FIRMWARE +``` + +Boot the Pi from the SD card. You should get a serial console or HDMI output. + +### 1.2 Initial network setup + +On the Pi (serial or HDMI): + +```bash +# Find your IP +ip addr + +# Set a temporary password for nixos user to SSH in +passwd nixos +``` + +From your workstation: + +```bash +ssh nixos@ +``` + +### 1.3 Copy the flake to the Pi + +```bash +# From your workstation (repo root) +rsync -avz --exclude='.git' . nixos@:/tmp/homey/ +``` + +### 1.4 Generate the Pi's age key + +```bash +# On the Pi +nix-shell -p age --run 'age-keygen -o /tmp/pi-age-key.txt' +age-keygen -y /tmp/pi-age-key.txt # print public key +``` + +Copy the public key back to your workstation. Add it to `secrets/.sops.yaml`: + +```yaml +- age: + - AGE-PUBLIC-KEY-YOUR-WORKSTATION + - AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME # ← paste Pi's public key here +``` + +Re-encrypt secrets so the Pi can decrypt them: + +```bash +# On workstation +sops updatekeys secrets/secrets.yaml +git add secrets/.sops.yaml secrets/secrets.yaml +git commit -m "Add Pi age key" + +# Copy updated files to Pi +rsync -avz secrets/ nixos@:/tmp/homey/secrets/ +``` + +Place the Pi's private key where sops-nix expects it: + +```bash +# On the Pi +sudo mkdir -p /var/lib/sops-nix +sudo cp /tmp/pi-age-key.txt /var/lib/sops-nix/key.txt +sudo chmod 600 /var/lib/sops-nix/key.txt +``` + +### 1.5 Configure host-specific settings + +Edit `hosts/pi-main/default.nix` on the Pi (or on workstation first): + +1. Set your SSH public key in `users.users.admin.openssh.authorizedKeys.keys` +2. Set `homey.storage.device` to your USB drive: + ```bash + ls -la /dev/disk/by-id/ | grep -v part + ``` +3. Set `homey.backup.repository` to your backup destination + +Edit `hosts/pi-main/hardware.nix` if the disk labels differ from defaults. + +### 1.6 Install NixOS + +```bash +# On the Pi +sudo nixos-install --flake /tmp/homey#pi-main --no-root-passwd + +# Reboot +sudo reboot +``` + +After reboot, SSH in with your admin key: + +```bash +ssh admin@ +``` + +--- + +## Phase 2 — Restore Data from Old Volumes + +Mount the external HD (if not auto-mounted): + +```bash +sudo mount /dev/disk/by-id/ /mnt/data +``` + +Copy data from the old Longhorn volume backups into the new layout: + +```bash +# Adjust source paths to wherever your Longhorn volume dumps are +BACKUP_SRC=/path/to/longhorn/backups + +# OpenLDAP +sudo rsync -av $BACKUP_SRC/openldap/etc-ldap-slapd.d/ /mnt/data/openldap/etc-ldap-slapd.d/ +sudo rsync -av $BACKUP_SRC/openldap/var-lib-ldap/ /mnt/data/openldap/var-lib-ldap/ + +# Gitea +sudo rsync -av $BACKUP_SRC/gitea/data/ /mnt/data/gitea/data/ + +# Nextcloud +sudo rsync -av $BACKUP_SRC/nextcloud/html/ /mnt/data/nextcloud/html/ +# Restore postgres from pg_dump if available, otherwise restore the data dir: +sudo rsync -av $BACKUP_SRC/nextcloud/db/ /mnt/data/nextcloud/db/ +``` + +Fix ownership (containers run as UID 1000 or root depending on image): + +```bash +# openldap runs as root inside the container +sudo chown -R root:root /mnt/data/openldap/ + +# gitea runs as git (UID 1000) +sudo chown -R 1000:1000 /mnt/data/gitea/ + +# nextcloud runs as www-data (UID 33) +sudo chown -R 33:33 /mnt/data/nextcloud/html/ +# postgres data owned by postgres (UID 999 in the postgres image) +sudo chown -R 999:999 /mnt/data/nextcloud/db/ +``` + +--- + +## Phase 3 — Cloudflare Tunnel Setup + +### 3.1 Create the tunnel in Cloudflare Zero Trust + +1. Go to [https://one.dash.cloudflare.com](https://one.dash.cloudflare.com) → Networks → Tunnels +2. Click "Create a tunnel" → Cloudflared → Name it `pi-main` +3. Copy the tunnel token (long string starting with `eyJ...`) +4. Add it to `secrets/secrets.yaml` under `cloudflare/tunnel_token` +5. Re-encrypt: `sops secrets/secrets.yaml` (the file opens in `$EDITOR`) + +### 3.2 Configure public hostnames in the Cloudflare dashboard + +In the tunnel's "Public Hostnames" tab, add: + +| Subdomain | Domain | Service | +|-----------|--------|---------| +| `auth` | `home.zakobar.com` | `https://localhost:443` | +| `git` | `home.zakobar.com` | `https://localhost:443` | +| `nextcloud` | `home.zakobar.com` | `https://localhost:443` | +| `ldapadmin` | `home.zakobar.com` | `https://localhost:443` | +| `jellyfin` | `home.zakobar.com` | `https://localhost:443` | +| `torrent` | `home.zakobar.com` | `https://localhost:443` | + +For each entry, under "Additional settings" → TLS → **No TLS Verify: ON** +(because cloudflared connects to `localhost` but the cert is for the real hostname). + +### 3.3 Update DNS in Cloudflare + +Add a CNAME for `home.zakobar.com` pointing to your tunnel's UUID (Cloudflare +creates this automatically when you add hostnames). You do not need to add +`home.zakobar.com` to your domain's A records — Cloudflare handles it. + +--- + +## Phase 4 — Rebuild and Verify + +After restoring data and completing Cloudflare setup, apply the final config: + +```bash +# On the Pi +sudo nixos-rebuild switch --flake /path/to/homey#pi-main +``` + +### Verification checklist + +```bash +# All container services running? +systemctl list-units 'podman-*' --state=active + +# OpenLDAP responding? +ldapsearch -x -H ldap://127.0.0.1:389 -b dc=home,dc=zakobar,dc=com -D "cn=admin,dc=home,dc=zakobar,dc=com" -W + +# Authelia health? +curl -s http://localhost:9091/api/health | python3 -m json.tool + +# Caddy serving TLS? +curl -I https://auth.home.zakobar.com + +# Gitea login? +# Visit https://git.home.zakobar.com — should redirect to authelia if not logged in + +# Nextcloud? +# Visit https://nextcloud.home.zakobar.com + +# Cloudflare tunnel connected? +systemctl status cloudflared-tunnel-pi-main +``` + +--- + +## Phase 5 — Local DNS (optional but recommended) + +To access services without going through Cloudflare on the LAN, add these +records to your router's DNS or Pi-hole: + +``` +192.168.1.100 home.zakobar.com +192.168.1.100 auth.home.zakobar.com +192.168.1.100 git.home.zakobar.com +192.168.1.100 nextcloud.home.zakobar.com +192.168.1.100 ldapadmin.home.zakobar.com +192.168.1.100 jellyfin.home.zakobar.com +192.168.1.100 torrent.home.zakobar.com +``` + +Replace `192.168.1.100` with your Pi's actual LAN IP. + +--- + +## Day-to-day Operations + +### Apply config changes + +```bash +sudo nixos-rebuild switch --flake /path/to/homey#pi-main +``` + +### Edit secrets + +```bash +sops secrets/secrets.yaml +# Save and exit — sops re-encrypts automatically +# Then copy to Pi and rebuild +``` + +### Browse service data on disk + +```bash +ls /mnt/data/ +ls /mnt/data/gitea/data/ +# No special tools needed — plain filesystem +``` + +### Trigger a manual backup + +```bash +sudo systemctl start restic-backups-homey.service +``` + +### List backup snapshots + +```bash +sudo restic -r \ + --password-file /run/secrets/restic_password \ + snapshots +``` + +### Restore a single service from backup + +```bash +sudo systemctl stop podman-gitea.service +sudo restic -r restore latest \ + --target / \ + --include /mnt/data/gitea +sudo systemctl start podman-gitea.service +``` + +--- + +## Adding a Second Machine (future) + +1. Create `hosts/pi-secondary/default.nix` and `hardware.nix` +2. Enable the services you want on that machine +3. Services communicating cross-machine: reference the primary Pi's LAN IP or + hostname directly in environment variables (e.g. point gitea's LDAP config + at `192.168.1.100:389` rather than `127.0.0.1:389`). +4. Add the new host to `flake.nix`: + ```nix + pi-secondary = mkHost { + system = "x86_64-linux"; + hostPath = ./hosts/pi-secondary/default.nix; + }; + ``` +5. Generate an age key on the new machine and add it to `.sops.yaml`. diff --git a/README.org b/README.org deleted file mode 100644 index cf93173..0000000 --- a/README.org +++ /dev/null @@ -1,115 +0,0 @@ -#+title: Homey - -A home environment for everyone! - -* Installation - -Install using - -#+begin_src bash -helm upgrade --install homey . -n homey -#+end_src - -* Backing up - -We must find a better solution - -https://perfectmediaserver.com/day-two/top10apps.html - -Nefarious - -* LDAP Configuration - -Logins are done to PHPLDAPADMIN - -DN is like: - -cn=admin,dc=home,dc=,dc=io -get-secret-val.sh homey openldap-admin password - -First thing we do is create an organization unit called users - -To add a new user, we create a child entry to ou=users - -It has to be of type inetOrgPerson - -cn = Common Name, sn = Sur Name. -Select RDN = User Name (uid) (FROM DROP DOWN MENU) -UID = USERNAME, that is what is important. (In PHPLdapAdmin it is under User Name) - -Now we may continue! - -* GITEA - -Site Title: whatever - -SSH Server Domain: git. -SSH Server Port: 2222 -Gitea Base URL: http://git. - -Then add Administrator Account Settings: - -Administrator Username: gitea-admin -Password: from gitea-admin-pass -Email address must be populated - -That will work after a few minutes. - -Now we go into Authentication Sources - -Add a new LDAP Authentication source - -Authentication name: Home LDAP -Host: openldap -Port: 389 -Bind DN = cn=readonly,dc=home,dc=,dc=io -Bind Password: openldap-ro password -User Search Base: ou=users,dc=home,dc=,dc=io -user search filter = (uid=%s) -Admin filter (title=admin) -Username Attribute: uid -First Name Attribute: cn -Surname Attribute: sn -Email Attribute: mail - -* AUTHELIA - -https://github.com/authelia/authelia/blob/57d5fbd3f5c82e83296023dc1de6e4f5ff063c00/examples/compose/lite/authelia/configuration.yml -This fucking sucks -https://gist.github.com/james-d-elliott/5152d27c0781aee856a3383f1284998e - -* EVERYTHING -https://www.talkingquickly.co.uk/gitea-sso-with-keycloak-openldap-openid-connect - -* DRONE AND GITEA -? -https://dev.to/ruanbekker/self-hosted-cicd-with-gitea-and-drone-ci-200l - -* DAV - -https://gitlab.com/davical-project/davical/-/blob/master/config/example-config.php - -Line 800 ish for auth from reverse proxy - -* NEXTCLOUD - -I ran THIS command inside -su www-data -s /bin/bash -c php occ ldap:promote-group "admins" - -** When maintenence mode - -#+begin_example -kubectl exec --tty --stdin -n homey deploy/nextcloud -- su -l www-data -s /bin/bash -php /var/www/html/occ maintenance:mode --off -#+end_src - -* I UNDERSTAND - -I need to backup Chen's stuff -And... I need to Jellyfin - -* PAPERLESS - -https://github.com/paperless-ngx/paperless-ngx/blob/74c44fe418a91a526b5dab1a91fde4aaebd28bb1/docker/compose/docker-compose.postgres.yml - -For docker diff --git a/backup-longhorn-volumes.sh b/backup-longhorn-volumes.sh deleted file mode 100755 index 2d42cd8..0000000 --- a/backup-longhorn-volumes.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="$HOME/homey-backup" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -NODE="root@192.168.1.100" - -NC_DATA_PVC="pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815" -NC_DB_PVC="pvc-c5b28179-1b9c-462a-be5b-05c4f0bb36ca-5f2dbf4d" - -NC_DATA_DEST="$BACKUP_DIR/longhorn-nextcloud-data/$TIMESTAMP" -NC_DB_DEST="$BACKUP_DIR/longhorn-nextcloud-db/$TIMESTAMP" - -echo "=== Longhorn Volume Backup (Emergency) ===" -echo "Started: $(date)" -echo "" -echo "WARNING: Backing up raw Longhorn volume images" -echo "These are sparse files - actual data is smaller than file size" -echo "" - -mkdir -p "$NC_DATA_DEST" -mkdir -p "$NC_DB_DEST" - -echo "--- Backing up Nextcloud data volume ---" -echo "Source: $NODE:/hda/replicas/$NC_DATA_PVC/" -echo "Dest: $NC_DATA_DEST/" -echo "" - -rsync -avzP --no-owner --no-group --sparse \ - "$NODE:/hda/replicas/$NC_DATA_PVC/" \ - "$NC_DATA_DEST/" - -echo "" -echo "Nextcloud volume backup: $(du -sh "$NC_DATA_DEST" | cut -f1)" - -echo "" -echo "--- Backing up PostgreSQL volume ---" -echo "Source: $NODE:/hda/replicas/$NC_DB_PVC/" -echo "Dest: $NC_DB_DEST/" -echo "" - -rsync -avzP --no-owner --no-group --sparse \ - "$NODE:/hda/replicas/$NC_DB_PVC/" \ - "$NC_DB_DEST/" - -echo "" -echo "PostgreSQL volume backup: $(du -sh "$NC_DB_DEST" | cut -f1)" - -echo "" -echo "=== Backup Complete ===" -echo "Timestamp: $TIMESTAMP" -echo "Finished: $(date)" - -echo "" -echo "Total backup size:" -du -sh "$BACKUP_DIR/longhorn-nextcloud-data/$TIMESTAMP" "$BACKUP_DIR/longhorn-nextcloud-db/$TIMESTAMP" - -echo "" -echo "NOTE: These are raw Longhorn volume images." -echo "To restore, copy back to /hda/replicas/ and restart Longhorn." diff --git a/backup-nextcloud.sh b/backup-nextcloud.sh deleted file mode 100755 index c840e65..0000000 --- a/backup-nextcloud.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="$HOME/homey-backup" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -NAMESPACE="homey" -NODE="root@192.168.1.100" - -NC_DATA_DEST="$BACKUP_DIR/nextcloud-data/backups/$TIMESTAMP" -NC_DB_DEST="$BACKUP_DIR/nextcloud-postgres/backups/$TIMESTAMP" - -show_progress() { - local file="$1" - local label="$2" - local total="${3:-0}" - - while [[ ! -f "$file" ]]; do - sleep 0.2 - done - - while kill -0 "$BACKUP_PID" 2>/dev/null; do - local size=$(stat -c%s "$file" 2>/dev/null || echo "0") - if [[ "$total" -gt 0 ]]; then - local pct=$((size * 100 / total)) - local size_hr=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B") - local total_hr=$(numfmt --to=iec-i --suffix=B "$total" 2>/dev/null || echo "${total}B") - printf "\r%s: %s / %s (%d%%) " "$label" "$size_hr" "$total_hr" "$pct" - else - local size_hr=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B") - printf "\r%s: %s " "$label" "$size_hr" - fi - sleep 0.5 - done - - local final=$(stat -c%s "$file" 2>/dev/null || echo "0") - local final_hr=$(numfmt --to=iec-i --suffix=B "$final" 2>/dev/null || echo "${final}B") - printf "\r%s: %s - done! \n" "$label" "$final_hr" -} - -wait_for_cluster() { - echo "Waiting for Kubernetes cluster..." - local max_wait=300 - local start=$(date +%s) - while true; do - if kubectl get nodes &>/dev/null; then - echo "Cluster ready!" - return 0 - fi - local now=$(date +%s) - if [[ $((now - start)) -ge $max_wait ]]; then - echo "ERROR: Cluster not available after ${max_wait}s" - return 1 - fi - printf "\rWaiting... %ds " $((now - start)) - sleep 5 - done -} - -echo "=== Nextcloud Backup ===" -echo "Started: $(date)" - -mkdir -p "$NC_DATA_DEST" -mkdir -p "$NC_DB_DEST" - -if ! wait_for_cluster; then - echo "Cluster unavailable. Cannot proceed." - exit 1 -fi - -echo "" -echo "--- Backing up PostgreSQL ---" -DB_POD=$(kubectl get pods -n "$NAMESPACE" -l app=nextcloud-postgres -o jsonpath='{.items[0].metadata.name}') -if [[ -z "$DB_POD" ]]; then - echo "ERROR: PostgreSQL pod not found" - exit 1 -fi - -DB_SIZE=$(kubectl exec -n "$NAMESPACE" "$DB_POD" -- psql -U postgres -d nextcloud_db -t -c "SELECT pg_database_size('nextcloud_db');" 2>/dev/null | tr -d ' ' || echo "0") -echo "Database size: $(numfmt --to=iec-i --suffix=B "$DB_SIZE" 2>/dev/null || echo "$DB_SIZE bytes")" - -echo "Dumping database from $DB_POD..." -kubectl exec -n "$NAMESPACE" "$DB_POD" -- sh -c "pg_dump -U postgres nextcloud_db" | gzip > "$NC_DB_DEST/dump.sql.gz" & -BACKUP_PID=$! -show_progress "$NC_DB_DEST/dump.sql.gz" "Database" "$DB_SIZE" -wait $BACKUP_PID 2>/dev/null || true - -echo "Database backup: $(du -sh "$NC_DB_DEST/dump.sql.gz" | cut -f1)" - -echo "" -echo "--- Backing up Nextcloud data ---" -NC_POD=$(kubectl get pods -n "$NAMESPACE" -l app=nextcloud -o jsonpath='{.items[0].metadata.name}') -if [[ -z "$NC_POD" ]]; then - echo "ERROR: Nextcloud pod not found" - exit 1 -fi - -NC_SIZE=$(kubectl exec -n "$NAMESPACE" "$NC_POD" -- du -sb /var/www/html 2>/dev/null | awk '{print $1}' || echo "0") -echo "Data size: $(numfmt --to=iec-i --suffix=B "$NC_SIZE" 2>/dev/null || echo "$NC_SIZE bytes")" - -echo "Creating tar archive from $NC_POD..." -kubectl exec -n "$NAMESPACE" "$NC_POD" -- tar cf - -C /var/www/html . 2>/dev/null | gzip > "$NC_DATA_DEST/data.tar.gz" & -BACKUP_PID=$! -show_progress "$NC_DATA_DEST/data.tar.gz" "Data" "$NC_SIZE" -wait $BACKUP_PID 2>/dev/null || true - -echo "Data backup: $(du -sh "$NC_DATA_DEST/data.tar.gz" | cut -f1)" - -echo "" -echo "--- Creating latest symlinks ---" -rm -f "$BACKUP_DIR/nextcloud-data/latest" "$BACKUP_DIR/nextcloud-postgres/latest" -ln -sf "backups/$TIMESTAMP" "$BACKUP_DIR/nextcloud-data/latest" -ln -sf "backups/$TIMESTAMP" "$BACKUP_DIR/nextcloud-postgres/latest" - -echo "" -echo "=== Backup Complete ===" -echo "Timestamp: $TIMESTAMP" -echo "Finished: $(date)" - -echo "" -du -sh "$NC_DATA_DEST" "$NC_DB_DEST" diff --git a/copy-longhorn-from-hd.sh b/copy-longhorn-from-hd.sh deleted file mode 100755 index fd684b5..0000000 --- a/copy-longhorn-from-hd.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SRC="/mnt/replicas" -DEST="$HOME/homey-backup/longhorn-volumes" -SKIP="pvc-dfe2aa08-bbb8-423b-9001-fb6aea181597-baf06a7f" - -mkdir -p "$DEST" - -echo "=== Copying Longhorn volumes from HD ===" -echo "Source: $SRC" -echo "Dest: $DEST" -echo "Skip: $SKIP (Jellyfin)" -echo "" - -for pvc in "$SRC"/*/; do - name=$(basename "$pvc") - - if [[ "$name" == "$SKIP" ]]; then - echo "Skipping: $name" - continue - fi - - echo "" - echo "Copying: $name" - - src_size=$(sudo du -sb "$pvc" 2>/dev/null | awk '{print $1}' || echo "0") - src_size_hr=$(numfmt --to=iec-i --suffix=B "$src_size" 2>/dev/null || echo "${src_size}B") - echo "Size: $src_size_hr" - - sudo rsync -a --no-owner --no-group --info=progress2 "${pvc%/}" "$DEST/" - sudo chown -R "$USER" "$DEST/$name" - - size=$(du -sh "$DEST/$name" | cut -f1) - echo "Done: $size" -done - -echo "" -echo "=== Copy Complete ===" -echo "Total size:" -sudo du -sh "$DEST" diff --git a/files/authelia-config.yaml b/files/authelia-config.yaml deleted file mode 100644 index ba3ee3e..0000000 --- a/files/authelia-config.yaml +++ /dev/null @@ -1,87 +0,0 @@ -############################################################### -# Authelia minimal configuration # -############################################################### -theme: "light" -log: - level: "debug" -jwt_secret: {{ .homey_authelia_jwt | quote }} -authentication_backend: - ldap: - implementation: "custom" - url: "ldap://openldap:389" - timeout: "5s" - start_tls: false - base_dn: "{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}" - users_filter: "({username_attribute}={input})" - username_attribute: "uid" - additional_users_dn: "ou=users" - groups_filter: "(&(uniquemember=uid={input},ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}})(objectclass=groupOfUniqueNames))" - group_name_attribute: "cn" - additional_groups_dn: "ou=groups" - mail_attribute: "mail" - display_name_attribute: "uid" - permit_referrals: false - permit_unauthenticated_bind: false - user: "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}" - password: {{ .homey_openldap_ro | quote }} -totp: - issuer: "{{ .Values.homey.url }}" - disable: false -session: - name: authelia_session - secret: {{ .homey_authelia_session | quote }} - expiration: 3600 # 1 hour - inactivity: 7200 # 2 hours - domain: "{{ .Values.homey.url}}" # needs to be your root domain -storage: - local: - path: "/config/db.sqlite3" - encryption_key: {{ .homey_authelia_encryption_key | quote }} -access_control: - default_policy: "deny" - rules: - - domain: - - "auth.zakobar.com" - policy: "bypass" - - domain: - - "dav.{{ .Values.homey.url }}" - policy: "one_factor" - - domain: - - "ldapadmin.{{ .Values.homey.url }}" - subject: - - 'group:admins' - policy: "two_factor" - - domain: - - "*.admin.{{ .Values.homey.url }}" - subject: - - 'group:admins' - policy: "two_factor" - - domain: - - "*.admin.{{ .Values.homey.url }}" - policy: "deny" - - domain: - - "torrent.{{ .Values.homey.url }}" - subject: - - 'group:admins' - policy: "two_factor" - - domain: - - "torrent.{{ .Values.homey.url }}" - policy: "deny" - - domain: - - "stash-dl.{{ .Values.homey.url }}" - policy: "one_factor" - - domain: - - "stash.{{ .Values.homey.url }}" - policy: "one_factor" - - domain: - - "paperless.{{ .Values.homey.url }}" - policy: "one_factor" -notifier: - filesystem: - filename: "/var/lib/authelia/emails.txt" -ntp: - address: 'udp://time.cloudflare.com:123' - version: 3 - max_desync: '3s' - disable_startup_check: false - disable_failure: true diff --git a/files/baikal-server.php b/files/baikal-server.php deleted file mode 100644 index bc2fa1c..0000000 --- a/files/baikal-server.php +++ /dev/null @@ -1,30 +0,0 @@ -setBaseUri('server.php'); - -// The lock manager is reponsible for making sure users don't overwrite -// each others changes. -$lockBackend = new DAV\Locks\Backend\File('data/locks'); -$lockPlugin = new DAV\Locks\Plugin($lockBackend); -$server->addPlugin($lockPlugin); - -// This ensures that we get a pretty index in the browser, but it is -// optional. -$server->addPlugin(new DAV\Browser\Plugin()); - -// All we need to do now, is to fire up the server -$server->exec(); diff --git a/files/davical-config.php b/files/davical-config.php deleted file mode 100644 index 34183c4..0000000 --- a/files/davical-config.php +++ /dev/null @@ -1,580 +0,0 @@ -pg_connect[] = "dbname=davical user=postgres port=5432 host=davical-postgres password={{ .homey_davical_postgres_pass }}"; - - -/**************************** -********* Desirable ********* -*****************************/ - -$c->system_name = "{{ .Values.homey.organization }} CalDAV Server"; -$c->dbg = array( 'statistics' => 1, 'request' => 1, 'response' => 1 ); - -// $c->admin_email = 'calendar-admin@example.com'; -$c->restrict_setup_to_admin = true; -/*************************************************************************** -* * -* Caldav Server * -* * -***************************************************************************/ - -/** -* The "collections_always_exist" value defines whether a MKCALENDAR -* command is needed to create a calendar collection before calendar -* resources can be stored in it. You will want to leave this to the -* default (true) if people will be using Evolution or Sunbird / -* Lightning against this because that software does not support the -* creation of calendar collections. -* -* Default: true -*/ -// $c->collections_always_exist = false; - -/** -* The name of a user's "home" calendar and addressbook. These will be created -* for each new user. -* -* Defaults: -* home_calendar_name: 'calendar' -* home_addressbook_name: 'addresses' -*/ -// $c->home_calendar_name = 'calendar'; -// $c->home_addressbook_name = 'addresses'; - -/** -* Sets a numeric value indicating the maximum size in octets (bytes) of a resource -* that the server is willing to accept when an address object resource is stored -* in an address book collection (e.g. contacts with image attachments). -* Note that not all clients respect that property and that DAViCal won't deny creating -* or updating a resource that is larger than the specified limit if the client willingly or -* unwillingly ignores that property. Currently (late 2018) we only know of iOS devices to handle it properly. -* -* Default: 6550000 -*/ -// $c->carddav_max_resource_size = 6550000; - -/** -* If the above options are not suitable for your new users, use this to create -* a more complex default collection management. -* -* Note: if you use this configuration option both $c->home_calendar_name and -* $c->home_addressbook_name are ignored! -* -* See https://wiki.davical.org/index.php/Configuration/settings/default_collections -*/ -// $c->default_collections = array( -// array( -// 'type' => 'addressbook', -// 'name' => 'addresses', -// 'displayname' => '%fn addressbook', -// 'privileges' => null -// ), -// array( -// 'type' => 'calendar', -// 'name' => 'calendar', -// 'displayname' => '%fn calendar', -// 'privileges' => null -// ) -// ); - -/** -* An array of groups / permissions which should be automatically added -* for each new user created. This is a crude mechanism which we -* will hopefully manage to work out some better approach for in the -* future. For now, create an array that looks something like: -* array( 9 => 'R', 4 => 'A' ) -* to create a 'read' relationship to user_no 9 and an 'all' relation -* with user_no 4. -* -* Default: none -*/ -// $c->default_relationships = array(); - -/** -* An array of the privileges which will be configured for a user by default -* from the possible set of real privileges: -* 'read', 'write-properties', 'write-content', 'unlock', 'read-acl', 'read-current-user-privilege-set', -* 'bind', 'unbind', 'write-acl', 'read-free-busy', -* 'schedule-deliver-invite', 'schedule-deliver-reply', 'schedule-query-freebusy', -* 'schedule-send-invite', 'schedule-send-reply', 'schedule-send-freebusy' -* -* Or also from these aggregated privileges: -* 'write', 'schedule-deliver', 'schedule-send', 'all' -*/ -// $c->default_privileges = array('read-free-busy', 'schedule-query-freebusy'); - -/** -* An array of fields on the usr record which should be set to specific -* values when the users are created. -* -* Default: none -*/ -// $c->template_usr = array( -// 'active' => true, -// 'locale' => 'it_IT', -// 'date_format_type' => 'E', -// 'email_ok' => date('Y-m-d') -// ); - -/** -* If "hide_TODO" is true, then VTODO requested from someone other than the -* admin or owner of a calendar will not get an answer. Often these todo are -* only relevant to the owner, but in some shared calendar situations they -* might not be in which case you should set this to false. -* -* Default: true -*/ -// $c->hide_TODO = false; - -/** -* If true, then VALARM from someone other than the admin or owner of a -* calendar will not be included in the response. The default is false because -* the preferred behaviour is to enable/disable the alarms in your CalDAV -* client software. -* -* Default: false -*/ -// $c->hide_alarm = true; - -/** -* If you want to hide older events (in order to save resources, speed up -* clients, etc.) define the desired time interval in number of days. -*/ -// $c->hide_older_than = 90; - -/** -* Hide bound collections from certain clients -* -* If you want to use iOS (which does not support delegation) in combination -* with other software which does supports degation, you can use this option -* to tailor a working solution: bind all collections you want to see on iOS -* (emulation of delegation) and then hide these collections from other clients -* with real delegation support. -* -* Default: false/not set: always show bound collections -* -* If set to true: never show bound collections -* If set to an array: hide if any header => regex tuple matches -* Example: Hide bound collections from clients which send a User-Agent header -* matching regex1 OR an X-Client header matching regex2 -*/ -// $c->hide_bound = array( 'User-Agent'=>'#regex1#', 'X-Client'=>'#regex2#'); - -/** -* External subscription (BIND) minimum refresh interval -* Required if you want to enable remote binding ( webcal subscriptions ) -* -* Default: none -*/ -// $c->external_refresh = 60; - -/** -* External subscription (BIND) user agent string -* Required if your remote calendar only delivers to known user agents. -* -* Default: none -*/ -// $c->external_ua_string = ''; - -/** -* If you want to force DAViCal to use HTTP Digest Authentication for CalDAV -* access. Note that this requires all user passwords to be stored in plain text -* in the database. It is probably better to configure the webserver to do -* Digest auth against a separate user database (see below for Webserver Auth). -*/ -// $c->http_auth_mode = "Digest"; - -/** -* Provide freebusy information to any (unauthenticated) user via the -* freebusy.php URL. Only events marked as PRIVATE will be excluded from the -* report. -* -* Default: false (authentication required) -*/ -// $c->public_freebusy_url = true; - -/** -* The "support_obsolete_free_busy_property" value controls whether, -* during a PROPFIND, the obsolete Scheduling property "calendar-free-busy-set" -* is returned. Set the value to true to support the property only if your -* client requires it, however note that PROPFIND performance may be -* adversely affected if you do so. -* -* Introduced in DAViCal version 1.1.4 in support of Issue #31 Database -* Performance Improvements. -* -* Default: false -*/ -// $c->support_obsolete_free_busy_property = false; - -/** -* The default locale will be "en_NZ"; -* -* If you are in a non-English locale, you can set the default_locale -* configuration to one of the supported locales. -* -* Supported Locales (at present, see: "select * from supported_locales ;" for a full list) -* -* "de_DE", "en_NZ", "es_AR", "fr_FR", "nl_NL", "ru_RU" -* -* If you want locale support you probably know more about configuring it than me, but -* at this stage it should be noted that all translations are UTF-8, and pages are -* served as UTF-8, so you will need to ensure that the UTF-8 versions of these locales -* are supported on your system. -* -* People interested in providing new translations are directed to the Wiki: -* https://wiki.davical.org/w/Translating_DAViCal -*/ -// $c->default_locale = "en_NZ"; - -/** -* This is used to construct URLs which are passed in the answers to the client. You may -* want to force this to a specific domain in responses if your system is accessed by -* multiple names, otherwise you probably won't need to change it. -* -* Default: $_SERVER['SERVER_NAME'] -*/ -// $c->domain_name = 'example.com'; - -/** -* If this option is set to true, then "@$c->domain_name" is appended to the -* user login name if it does not contain the @ character. If email addresses -* are used as user names in Davical, this fixes a problem with MacOS X 10.6 -* Addressbook that cannot login to CardDav account. -* -* Default: false -*/ -// $c->login_append_domain_if_missing = true; - -/** -* Many people want this, but it may be a security issue for you, so it is -* disabled by default. If you enable it, then confidential / private events -* will be visible to the 'organizer' or 'attendee' lists. The reason that -* this becomes a security issue is that this identification needs to be based -* on the user's e-mail address. The user's e-mail address is generally -* something which they can set, so they could change it to be the address of -* an attendee of a meeting and then would be able to read the meeting. -* -* Without this, the only person who can view/change PRIVATE or CONFIDENTIAL -* events in a calendar is someone with full administrative rights to the calendar -* usually the owner. -* -* If the only person that devious is your sysadmin then you probably already -* enabled this option... -* -* Default: false -*/ -// $c->allow_get_email_visibility = false; - -/** -* Disable calendar-proxy-{read,write} on PROPFIND -* -* This can be useful if clients are known to not use this information, -* as it is very expensive to compute (especially on servers with lots of -* users who share their collections) and most clients will never use it, -* or ask for it explicitly using an expand-property REPORT, which is not -* affected by this option. -* -* Default: false/unset -* -* If set to false (or unset): always show -* If set to true: never show -* If set to an array: hide if any header => regex tuple matches -*/ -// $c->disable_caldav_proxy_propfind_collections = array( 'User-Agent'=>'#regex1#', 'X-Client'=>'#regex2#'); - -/** -* A limiter on how many times we'll apply the recurrence rules for an event -* to find the next valid one. -* -* Default: 100 -* -* If you see the following error message, you may want to consider increasing -* it: -* RRULE, loop limit has been hit in GetMoreInstances, you probably want to increase $c->rrule_loop_limit -*/ -// $c->rrule_loop_limit = 100; - -/** -* EXPERIMENTAL: -* If true, names of groups (prefixed with "@") given as an event attendee -* will get resolved to a list of members of that group. Note that CalDAV -* clients might get confused by this server behavior until they get -* synced again. -* -* Default: false. -*/ -// $c->enable_attendee_group_resolution = true; - - -/*************************************************************************** -* * -* Scheduling * -* * -***************************************************************************/ - -/** -* If you want to turn off scheduling functions you can set this to 'false' and -* DAViCal will not advertise the ability to schedule, leaving it to calendar -* clients to send out and receive scheduling requests. -* -* Default: true -*/ -// $c->enable_auto_schedule = false; - -/** -* If true, then remote scheduling will be enabled. There is a possibility -* of receiving spam events in calendars if enabled, you will at least know -* what domain the spam came from as domain key signatures are required for -* events to be accepted. -* -* You probably need to setup Domain Keys for your domain as well as the -* appropiate DNS SRV records. -* -* for example, if DAViCal is installed on cal.example.com you should have -* DNS SRV records like this: -* _ischedules._tcp.example.com. IN SRV 0 1 443 cal.example.com -* _ischedule._tcp.example.com. IN SRV 0 1 80 cal.example.com -* -* DNS TXT record for signing outbound requests -* example: -* cal._domainkey.example.com. 86400 IN TXT "k=rsa\; t=s\; p=PUBKEY" -* -* Default: false -*/ -// $c->enable_scheduling = true; - -/** -* Domain Key domain to use when signing outbound scheduling requests, this -* is the domain with the public key in a TXT record as shown above. -* -* TODO: enable domain/signing by per user keys, patches welcome. -* -* Default: none -*/ -// $c->scheduling_dkim_domain = ''; - -/** -* Domain Key selector to use when signing outbound scheduling requests. -* -* TODO: enable selectors/signing by per user keys, patches welcome. -* -* Default: 'cal' -*/ -// $c->scheduling_dkim_selector = 'cal'; - -/* -* Domain Key private key -* Required if you want to enable outbound remote server scheduling -* -* Default: none -*/ -// $c->schedule_private_key = 'PRIVATE-KEY-BASE-64-DATA'; - - -/*************************************************************************** -* * -* Operation behind a Reverse Proxy * -* * -***************************************************************************/ - -/** -* If you install DAViCal behind a reverse proxy (e.g. an SSL offloader or -* application firewall, or in order to present services from different machines -* on a single public IP / hostname), the client IP, protocol and port used may -* be different from what the web server is reporting to DAViCal. Often, the -* original values are written to the X-Real-IP and/or X-Forwarded-For, -* X-Forwarded-Proto and X-Forwarded-Port headers. You can instruct DAViCal to -* attempt to "do the right thing" and use the content of these headers instead, -* when they are available. -* -* CAUTION: Malicious clients can spoof these headers. When you enable this, you -* need to make sure your reverse proxy erases any pre-existing values of all -* these headers, and that no untrusted requests can reach DAViCal without -* passing the proxy server. -* -* Default: false -*/ -unset( $_SERVER['HTTP_X_REAL_IP'] ); -$c->trust_x_forwarded = true; - -/* Set all values manually. */ -// $_SERVER['HTTPS'] = 'on'; -// $_SERVER['SERVER_PORT'] = 443; -// $_SERVER['REMOTE_ADDR'] = $_SERVER['Client-IP']; - - -/*************************************************************************** -* * -* External Authentication Sources * -* * -***************************************************************************/ - -/** -* Allow specifying another way to control access of the user by authenticating -* him against other drivers such has LDAP (the default is the PgSQL DB) -* $c->authenticate_hook['call'] should be set to the name of the plugin and must -* be a valid function that will be call like this: -* call_user_func( $c->authenticate_hook['call'], $username, $password ) -* -* The login mechanism is used in 2 different places: -* - for the web interface in: index.php that calls DAViCalSession.php that extends -* Session.php (from AWL libraries) -* - for the caldav client in: caldav.php that calls HTTPAuthSession.php -* Both Session.php and HTTPAuthSession.php check against the -* authenticate_hook['call'], although for HTTPAuthSession.php this will be for -* each page. For Session.php this will only occur during login. -* -* $c->authenticate_hook['config'] should be set up with any configuration data -* needed by the authenticate call - see below or in the Wiki for details. -* If you want to develop your own authentication plugin, have a look at -* awl/inc/AuthPlugins.php or any of the inc/drivers_*.php files. -* -* $c->authenticate_hook['optional'] = true; can be set to try default authentication -* as well in case the configured hook should report a failure. -*/ -// $c->authenticate_hook['optional'] = true; - -/********************************/ -/******* Other AWL hook *********/ -/********************************/ -// require_once('auth-functions.php'); -// $c->authenticate_hook = array( -// 'call' => 'AuthExternalAwl', -// 'config' => array( -// // A PgSQL database connection string for the database containing user records -// 'connection' => 'dbname=wrms host=otherhost port=5433 user=general', -// // Which columns should be fetched from the database -// 'columns' => "user_no, active, email_ok, joined, last_update AS updated, last_used, username, password, fullname, email", -// // a WHERE clause to limit the records returned. -// 'where' => "active AND org_code=7" -// ) -// ); - - -/********************************/ -/*********** LDAP hook **********/ -/********************************/ -/* -* For Active Directory go down to the next example. -*/ - -putenv('LDAPTLS_REQCERT=never'); -$c->authenticate_hook['call'] = 'LDAP_check'; -$c->authenticate_hook['config'] = array( - 'uri' => 'ldaps://openldap:636', - 'bindDN' => 'cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}', - 'passDN' => '{{ .homey_openldap_ro }}', - 'protocolVersion' => 3, // Version of LDAP protocol to use - 'optReferrals' => 0, // whether to automatically follow referrals returned by the LDAP server - 'networkTimeout' => 10, // timeout in seconds - 'baseDNUsers' => 'ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}', - 'filterUsers' => 'objectClass=person', - 'baseDNGroups' => 'ou=groups,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}', - 'filterGroups' => 'objectClass=groupOfUniqueNames', - 'mapping_field' => array("username" => "uid", - "modified" => "modifyTimestamp", - "fullname" => "cn" , - "email" => "mail" - ), // used to create the user based on their ldap properties - 'group_mapping_field' => array("username" => "cn", - "modified" => "modifyTimestamp", - "fullname" => "cn" , - "members" => "memberUid" - ), // used to create the group based on the ldap properties - 'group_member_dnfix' => true, // if your "members" field contains the full DN and needs to be truncated to just the uid - 'startTLS' => 'no', -); - -include('drivers_ldap.php'); -/********************************/ -/****** Webserver does Auth *****/ -/********************************/ - -/** -* It is quite common that the webserver can do the authentication for you, -* and you just want DAViCal to trust the username that the webserver will pass -* through (in the REMOTE_USER or REDIRECT_REMOTE_USER environment variable). -* In that case, set server_auth_type (can be an array) to the value provided by -* the webserver in the AUTH_TYPE environment variable, as well as the two -* following options as needed. -* -* Note that this method does not pull account details from anywhere, so you -* will first need to create an account in DAViCal for each username that will -* authenticate in this way - it's just that the password on that account will -* be ignored and authentication will happen through the authentication method -* that the webserver is configured with. -*/ -$c->authenticate_hook['server_auth_type'] = 'Basic'; -include_once('AuthPlugins.php'); - -/** -* Uncomment this to use Webserver Auth for CalDAV access in addition to the -* Admin web pages. -*/ - -/** -* If your Webserver Auth method provides a logout URL (traditional Basic Auth -* does not), you can enter it here so the Logout link in the Admin web pages -* can point to it. -*/ -$c->authenticate_hook['logout'] = 'https://auth.{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}/logout?rd=dav.{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim}}'; - -/*************************************************************************** -* * -* Push Notification Server * -* * -***************************************************************************/ - -/* -* This enable XMPP PubSub push notifications to clients that request them. -* N.B. this will publish urls for ALL updates and does NOT restrict -* subscription permissions on the jabber server! That means anyone with -* read access to the pubsub tree of your jabber server can watch for updates, -* they will only see URL's to the updated entries not the calendar data. -* -* Only tested with ejabberd 2.0.x -*/ - -// $c->notifications_server = array( -// 'host' => $_SERVER['SERVER_NAME'], // jabber server hostname -// 'jid' => 'user@example.com', // user(JID) to login/ publish as -// 'password' => '', // password for above account -// // 'debug_jid' => 'otheruser@example.com' // send a copy of all publishes to this jid -// ); -// include ( 'pubsub.php' ); - - -/*************************************************************************** -* * -* Detailed Metrics * -* * -***************************************************************************/ - -/* -* This enables a /metrics.php URL containing detailed metrics about the -* operation of DAViCal. Ideally you will be running memcache if you are -* interested in keeping metrics, but there is a simple metrics collection -* available to you without running memcache. -* -* Note that there is currently no way of enabling metrics via memcache -* without memcache being enabled for all of DAViCal. -*/ -// $c->metrics_style = 'counters'; // Just the simple counter-based metrics -// $c->metrics_style = 'memcache'; // Only the metrics using memcache -// $c->metrics_style = 'both'; // Both styles of metrics -// $c->metrics_collectors = array('127.0.0.1'); // Restrict access to only this IP address -// $c->metrics_require_user = 'metricsuser'; // Restrict access to only connections authenticating as this user - -/*************************************************************************** -* * -* Audit Logging * -* * -***************************************************************************/ -/* To enable audit logging to syslog you can uncomment the following line. -* -* This file is suitable for basic auditing, if you want/need more comprehensive -* logging then see: -* http://wiki.davical.org/index.php/Configuration/hooks/log_caldav_action -*/ -// include('log_caldav_action.php'); - diff --git a/files/gitea-app.ini b/files/gitea-app.ini deleted file mode 100644 index ad31759..0000000 --- a/files/gitea-app.ini +++ /dev/null @@ -1,95 +0,0 @@ -APP_NAME = {{ .Values.homey.organization }} -RUN_MODE = prod -RUN_USER = git -WORK_PATH = /data/gitea - -[repository] -ROOT = /data/git/repositories - -[repository.local] -LOCAL_COPY_PATH = /data/gitea/tmp/local-repo - -[repository.upload] -TEMP_PATH = /data/gitea/uploads - -[server] -APP_DATA_PATH = /data/gitea -DOMAIN = git.{{ .Values.homey.url }} -HTTP_PORT = 3000 -ROOT_URL = https://git.{{ .Values.homey.url }}/ -DISABLE_SSH = true -SSH_PORT = 443 -SSH_LISTEN_PORT = 22 -LFS_START_SERVER = true -LFS_JWT_SECRET = {{ .homey_gitea_lfs_jwt_secret | b64enc | replace "=" "" }} -OFFLINE_MODE = false - -[lfs] -PATH = /data/git/lfs - -[database] -PATH = /data/gitea/gitea.db -DB_TYPE = sqlite3 -HOST = localhost:3306 -NAME = gitea -USER = root -PASSWD = -LOG_SQL = false -SCHEMA = -SSL_MODE = disable -CHARSET = utf8 - -[indexer] -ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve - -[session] -PROVIDER_CONFIG = /data/gitea/sessions -PROVIDER = file - -[picture] -AVATAR_UPLOAD_PATH = /data/gitea/avatars -REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - -[attachment] -PATH = /data/gitea/attachments - -[log] -MODE = console -LEVEL = info -ROUTER = console -ROOT_PATH = /data/gitea/log - -[security] -INSTALL_LOCK = true -SECRET_KEY = -REVERSE_PROXY_LIMIT = 1 -REVERSE_PROXY_TRUSTED_PROXIES = * -INTERNAL_TOKEN = {{ .homey_gitea_random_internal_token }} -PASSWORD_HASH_ALGO = pbkdf2 - -[service] -DISABLE_REGISTRATION = true -REQUIRE_SIGNIN_VIEW = false -REGISTER_EMAIL_CONFIRM = false -ENABLE_NOTIFY_MAIL = false -ALLOW_ONLY_EXTERNAL_REGISTRATION = true -ENABLE_CAPTCHA = false -DEFAULT_KEEP_EMAIL_PRIVATE = false -DEFAULT_ALLOW_CREATE_ORGANIZATION = true -DEFAULT_ENABLE_TIMETRACKING = true -NO_REPLY_ADDRESS = noreply.localhost -ENABLE_REVERSE_PROXY_AUTHENTICATION = true -ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true - -[mailer] -ENABLED = false - -[openid] -ENABLE_OPENID_SIGNIN = false -ENABLE_OPENID_SIGNUP = false - -[oauth2] -ENABLE = false -JWT_SECRET = {{ .homey_gitea_oauth2_jwt_secret | b64enc | replace "=" "" }} diff --git a/files/radicale-configmap.ini b/files/radicale-configmap.ini deleted file mode 100644 index 493827c..0000000 --- a/files/radicale-configmap.ini +++ /dev/null @@ -1,11 +0,0 @@ -[server] -hosts = 0.0.0.0:5232 - -[auth] -type = http_x_remote_user - -[storage] -filesystem_folder = /data/collections - -[web] -type = none diff --git a/files/sabre-server.php b/files/sabre-server.php deleted file mode 100644 index bc2fa1c..0000000 --- a/files/sabre-server.php +++ /dev/null @@ -1,30 +0,0 @@ -setBaseUri('server.php'); - -// The lock manager is reponsible for making sure users don't overwrite -// each others changes. -$lockBackend = new DAV\Locks\Backend\File('data/locks'); -$lockPlugin = new DAV\Locks\Plugin($lockBackend); -$server->addPlugin($lockPlugin); - -// This ensures that we get a pretty index in the browser, but it is -// optional. -$server->addPlugin(new DAV\Browser\Plugin()); - -// All we need to do now, is to fire up the server -$server->exec(); diff --git a/files/sogo.conf b/files/sogo.conf deleted file mode 100644 index cab53bc..0000000 --- a/files/sogo.conf +++ /dev/null @@ -1,94 +0,0 @@ -{ - /* ********************* Main SOGo configuration file ********************** - * * - * Since the content of this file is a dictionary in OpenStep plist format, * - * the curly braces enclosing the body of the configuration are mandatory. * - * See the Installation Guide for details on the format. * - * * - * C and C++ style comments are supported. * - * * - * This example configuration contains only a subset of all available * - * configuration parameters. Please see the installation guide more details. * - * * - * ~sogo/GNUstep/Defaults/.GNUstepDefaults has precedence over this file, * - * make sure to move it away to avoid unwanted parameter overrides. * - * * - * **************************************************************************/ - - /* Database configuration (mysql:// or postgresql://) */ - SOGoProfileURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_user_profile"; - OCSFolderInfoURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_folder_info"; - OCSSessionsFolderURL = "postgresql://sogo:sogo@sogo-postgres:5432/sogo/sogo_sessions_folder"; - - /* Mail */ - SOGoDraftsFolderName = Drafts; - SOGoSentFolderName = Sent; - SOGoTrashFolderName = Trash; - //SOGoIMAPServer = localhost; - //SOGoSieveServer = sieve://127.0.0.1:4190; - //SOGoSMTPServer = smtp://domain:port/?tls=YES; - //SOGoMailDomain = acme.com; - SOGoMailingMechanis = smtp; - //SOGoForceExternalLoginWithEmail = NO; - //SOGoMailSpoolPath = /var/spool/sogo; - //NGImap4ConnectionStringSeparator = "/"; - - /* Notifications */ - //SOGoAppointmentSendEMailNotifications = NO; - //SOGoACLsSendEMailNotifications = NO; - //SOGoFoldersSendEMailNotifications = NO; - - /* Authentication */ - SOGoPasswordChangeEnabled = YES; - - SOGoUserSources = ( - { - type = ldap; - CNFieldName = cn; - UIDFieldName = uid; - IDFieldName = uid; // first field of the DN for direct binds - bindFields = (uid, mail); // array of fields to use for indirect binds - baseDN = "ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}"; - bindDN = "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}"; - bindPassword = "{{ .homey_openldap_ro }}"; - canAuthenticate = YES; - displayName = "Shared Addresses"; - hostname = ldap://openldap:389; - id = public; - isAddressBook = YES; - } - ); - - /* Web Interface */ - //SOGoPageTitle = SOGo; - SOGoVacationEnabled = YES; - SOGoForwardEnabled = YES; - SOGoSieveScriptsEnabled = YES; - //SOGoMailAuxiliaryUserAccountsEnabled = YES; - //SOGoTrustProxyAuthentication = NO; - SOGoXSRFValidationEnabled = YES; - - /* General - SOGoTimeZone *MUST* be defined */ - SOGoLanguage = English; - SOGoTimeZone = Asia/Jerusalem; - //SOGoCalendarDefaultRoles = ( - // PublicDAndTViewer, - // ConfidentialDAndTViewer - //); - //SOGoSuperUsernames = (sogo1, sogo2); // This is an array - keep the parens! - SxVMemLimit = 384; - //WOPidFile = "/var/run/sogo/sogo.pid"; - SOGoMemcachedHost = "/var/run/memcached/memcached.sock"; - - /* Debug */ - SOGoDebugRequests = YES; - SoDebugBaseURL = YES; - ImapDebugEnabled = YES; - LDAPDebugEnabled = YES; - PGDebugEnabled = YES; - MySQL4DebugEnabled = YES; - SOGoUIxDebugEnabled = YES; - WODontZipResponse = YES; - //WOLogFile = /var/log/sogo/sogo.log; -} - diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8ae7fe4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,73 @@ +{ + description = "Homey - self-hosted home server NixOS configuration"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + + # sops-nix for secret management + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # Caddy with Cloudflare DNS plugin (not in nixpkgs mainline) + caddy-cloudflare = { + url = "github:NixOS/nixpkgs/nixos-24.11"; # see modules/caddy.nix for override + }; + }; + + outputs = { self, nixpkgs, sops-nix, ... }@inputs: + let + # Shared specialArgs passed to every host + commonArgs = { + inherit inputs; + # Top-level site config — override per-host if needed + homeyConfig = { + domain = "home.zakobar.com"; # base domain for all services + organization = "Zakobar Home Server"; + timezone = "Asia/Jerusalem"; + # External HD mount point — set in hardware.nix per host + # dataDir is intentionally NOT set here; each host sets it + }; + }; + + mkHost = { system, hostPath, extraModules ? [] }: + nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = commonArgs; + modules = [ + sops-nix.nixosModules.sops + hostPath + ./modules/common.nix + ./modules/storage.nix + ./modules/caddy.nix + ./modules/cloudflared.nix + ./modules/backup.nix + ./modules/services/openldap.nix + ./modules/services/authelia.nix + ./modules/services/gitea.nix + ./modules/services/nextcloud.nix + ./modules/services/phpldapadmin.nix + ./modules/services/jellyfin.nix + ./modules/services/transmission.nix + ] ++ extraModules; + }; + + in { + nixosConfigurations = { + + # Primary Raspberry Pi 4 + pi-main = mkHost { + system = "aarch64-linux"; + hostPath = ./hosts/pi-main/default.nix; + }; + + # Future second machine (placeholder — uncomment and configure when ready) + # pi-secondary = mkHost { + # system = "x86_64-linux"; # or aarch64-linux for another Pi + # hostPath = ./hosts/pi-secondary/default.nix; + # }; + + }; + }; +} diff --git a/get-secret-val.sh b/get-secret-val.sh deleted file mode 100644 index ee5a868..0000000 --- a/get-secret-val.sh +++ /dev/null @@ -1 +0,0 @@ -kubectl get secret -n $1 $2 --template={{.data.$3}} | base64 -d | xclip -selection c diff --git a/gitea-commands.txt b/gitea-commands.txt deleted file mode 100644 index 3361c42..0000000 --- a/gitea-commands.txt +++ /dev/null @@ -1,10 +0,0 @@ -kubectl exec -it -n homey deploy/gitea -- su - git -c "/usr/local/bin/gitea admin auth update-ldap --id=1 --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter \"(&(objectClass=person)(uid=%s))\" --admin-filter \"(memberOf=CN=admins,ou=groups,dc=zakobar,dc=com)\" --email-attribute mail --bind-dn=cn=readonly,dc=zakobar,dc=com --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9" - -kubectl exec -it -n homey deploy/gitea -- su - git -c "/usr/local/bin/gitea admin user delete --username aner" - - -gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter "&(objectClass=inetOrgPerson)(uid=%s)" --email-attribute mail --bind-dn="cn=readonly,dc=zakobar,dc=com" --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9 - -gitea admin auth update-ldap --id=1 --name ldap --security-protocol unencrypted --host openldap --port 389 --user-search-base ou=users,dc=zakobar,dc=com --user-filter "(&(objectClass=person)(uid=%s))" --email-attribute mail --bind-dn="cn=readonly,dc=zakobar,dc=com" --bind-password=VqxPZHwDCkFsLWaroyb880zdH1JTCvz9 - -kubectl exec -it -n homey deploy/authelia -- /bin/bash -c "cat /var/lib/authelia/emails.txt" diff --git a/hosts/pi-main/default.nix b/hosts/pi-main/default.nix new file mode 100644 index 0000000..87b5e2c --- /dev/null +++ b/hosts/pi-main/default.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Pi-main host configuration. +# This file declares which services run on this machine and any +# host-specific overrides. Hardware config lives in hardware.nix. + +{ + imports = [ + ./hardware.nix + ]; + + # ------------------------------------------------------------------------- + # Identity + # ------------------------------------------------------------------------- + networking.hostName = "pi-main"; + + # ------------------------------------------------------------------------- + # Admin user + # ------------------------------------------------------------------------- + users.users.admin = { + isNormalUser = true; + extraGroups = [ "wheel" "podman" ]; + # Paste your SSH public key here + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAA... your-key-here" + ]; + }; + + security.sudo.wheelNeedsPassword = false; # convenience on a home server + + # ------------------------------------------------------------------------- + # External HD + # ------------------------------------------------------------------------- + homey.storage = { + # Replace with the actual by-id path of your USB drive. + # Find it: ls -la /dev/disk/by-id/ | grep -v part + device = "/dev/disk/by-id/REPLACE-WITH-YOUR-DRIVE-ID"; + mountPoint = "/mnt/data"; + fsType = "ext4"; + }; + + # ------------------------------------------------------------------------- + # Services enabled on this host + # ------------------------------------------------------------------------- + + # Auth stack (run these together — authelia depends on openldap) + homey.openldap.enable = true; + homey.authelia.enable = true; + + # Productivity + homey.gitea.enable = true; + homey.nextcloud.enable = true; + homey.phpldapadmin.enable = true; + + # Media (enable when ready) + homey.jellyfin.enable = false; + homey.transmission.enable = false; + + # Reverse proxy + Cloudflare + homey.caddy.enable = true; + homey.cloudflared.enable = true; + + # Backups + homey.backup.enable = true; + # Where to send restic backups — set to your backup destination: + # "sftp:user@nas.local:/backups/homey" + # "b2:your-bucket-name:homey" + # "rclone:remote:homey" + homey.backup.repository = "sftp:REPLACE-WITH-BACKUP-DESTINATION"; + + # ------------------------------------------------------------------------- + # Local DNS overrides (optional — makes LAN clients hit the Pi directly + # instead of going through Cloudflare for *.home.zakobar.com) + # ------------------------------------------------------------------------- + # If you run Pi-hole or Adguard, add these records there instead. + # networking.extraHosts = '' + # 192.168.1.100 home.zakobar.com + # 192.168.1.100 auth.home.zakobar.com + # 192.168.1.100 git.home.zakobar.com + # 192.168.1.100 nextcloud.home.zakobar.com + # 192.168.1.100 ldapadmin.home.zakobar.com + # ''; +} diff --git a/hosts/pi-main/hardware.nix b/hosts/pi-main/hardware.nix new file mode 100644 index 0000000..4127cca --- /dev/null +++ b/hosts/pi-main/hardware.nix @@ -0,0 +1,84 @@ +{ config, lib, pkgs, modulesPath, ... }: + +# Hardware configuration for the primary Raspberry Pi 4 (8 GB). +# +# SD card layout assumed: +# /dev/mmcblk0p1 — /boot/firmware (FAT32, ~256 MB) +# /dev/mmcblk0p2 — / (ext4) +# +# External HD: +# Set homey.storage.device to the by-id path of your USB drive. +# Example: /dev/disk/by-id/usb-WD_Elements_12345-0:0-part1 +# Find it with: ls -la /dev/disk/by-id/ +# +# To generate this file fresh after installing NixOS on the Pi, run: +# nixos-generate-config --show-hardware-config +# and merge the output here. + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + # ------------------------------------------------------------------------- + # Boot loader — Raspberry Pi 4 uses U-Boot / extlinux + # ------------------------------------------------------------------------- + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + + # Pi 4 kernel — use the mainline kernel with RPi patches + kernelPackages = pkgs.linuxPackages_rpi4; + + # tmpfs for /tmp — keep the SD card writes down + tmp.useTmpfs = true; + + # Modules needed for USB storage (external HD) + initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "uas" ]; + kernelModules = []; + extraModulePackages = []; + }; + + # ------------------------------------------------------------------------- + # Filesystems + # ------------------------------------------------------------------------- + fileSystems."/" = { + device = "/dev/disk/by-label/NIXOS_SD"; # label the root partition NIXOS_SD when flashing + fsType = "ext4"; + options = [ "noatime" ]; + }; + + fileSystems."/boot/firmware" = { + device = "/dev/disk/by-label/FIRMWARE"; # FAT32 boot partition + fsType = "vfat"; + options = [ "fmask=0022" "dmask=0022" ]; + }; + + # External HD — device path is set in default.nix via homey.storage.device. + # storage.nix creates the actual fileSystems entry from that option. + + swapDevices = []; + + # ------------------------------------------------------------------------- + # Hardware + # ------------------------------------------------------------------------- + hardware = { + # Enable the RPi firmware (needed for GPU, WiFi, Bluetooth) + raspberry-pi."4".apply-overlays-dtmerge.enable = true; + + # Disable GPU memory split for a headless server (gives more RAM to OS) + # Set via config.txt if needed: gpu_mem=16 + }; + + # ------------------------------------------------------------------------- + # Platform + # ------------------------------------------------------------------------- + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + + # ------------------------------------------------------------------------- + # Power management + # ------------------------------------------------------------------------- + powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; +} diff --git a/modules/backup.nix b/modules/backup.nix new file mode 100644 index 0000000..b89aefa --- /dev/null +++ b/modules/backup.nix @@ -0,0 +1,150 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Restic backup module. +# +# Backs up all service data directories from the external HD. +# Schedule: daily at 03:00, keep 7 daily / 4 weekly / 6 monthly snapshots. +# +# Before a backup, Nextcloud is put into maintenance mode and postgres is +# pg_dump'd to a file. This ensures consistent DB backups. +# +# Secrets consumed from sops: +# restic/password +# +# The backup repository URL is set per-host in default.nix: +# homey.backup.repository = "sftp:user@nas:/backups/homey"; +# +# Restore: +# restic -r restore latest --target /mnt/data +# (or restore a single path: --include /mnt/data/openldap) + +let + cfg = config.homey.backup; + dataDir = config.homey.storage.mountPoint; +in +{ + options.homey.backup = { + enable = lib.mkEnableOption "Restic backup jobs"; + + repository = lib.mkOption { + type = lib.types.str; + example = "sftp:user@nas.local:/backups/homey"; + description = '' + Restic repository URL. Examples: + sftp:user@host:/path + b2:bucket-name:prefix + rclone:remote:path + /local/path (for testing) + ''; + }; + + schedule = lib.mkOption { + type = lib.types.str; + default = "03:00"; + description = "systemd OnCalendar expression for the daily backup."; + }; + + pruneRetention = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + daily = "7"; + weekly = "4"; + monthly = "6"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."restic/password" = { owner = "root"; }; + + # ----------------------------------------------------------------------- + # Pre-backup hook: pg_dump + nextcloud maintenance mode + # ----------------------------------------------------------------------- + systemd.services."homey-backup-pre" = { + description = "Pre-backup hooks (pg_dump, NC maintenance mode)"; + serviceConfig = { + Type = "oneshot"; + ExecStart = pkgs.writeShellScript "backup-pre" '' + set -euo pipefail + + # Put Nextcloud into maintenance mode (if running) + if systemctl is-active --quiet podman-nextcloud.service; then + podman exec nextcloud php occ maintenance:mode --on || true + fi + + # Dump postgres (if running) + if systemctl is-active --quiet podman-nextcloud-postgres.service; then + install -d -m 700 ${dataDir}/nextcloud/db-dump + podman exec nextcloud-postgres \ + pg_dump -U postgres nextcloud_db \ + > ${dataDir}/nextcloud/db-dump/nextcloud.sql + fi + ''; + }; + }; + + systemd.services."homey-backup-post" = { + description = "Post-backup hooks (take NC out of maintenance mode)"; + serviceConfig = { + Type = "oneshot"; + ExecStart = pkgs.writeShellScript "backup-post" '' + set -euo pipefail + if systemctl is-active --quiet podman-nextcloud.service; then + podman exec nextcloud php occ maintenance:mode --off || true + fi + ''; + }; + }; + + # ----------------------------------------------------------------------- + # Restic backup service + # ----------------------------------------------------------------------- + services.restic.backups.homey = { + repository = cfg.repository; + passwordFile = config.sops.secrets."restic/password".path; + cacheDir = "${dataDir}/restic-cache"; + + paths = [ + "${dataDir}/openldap" + "${dataDir}/authelia" + "${dataDir}/gitea" + "${dataDir}/nextcloud" + # media and transmission config included when those services are enabled: + "${dataDir}/jellyfin" + "${dataDir}/transmission" + # Deliberately excluded: media/* (large, can be re-downloaded) + ]; + + # Exclude Nextcloud's raw DB directory in favour of the pg_dump file + exclude = [ + "${dataDir}/nextcloud/db" + "${dataDir}/restic-cache" + ]; + + timerConfig = { + OnCalendar = cfg.schedule; + Persistent = true; # run on next boot if missed + }; + + pruneOpts = [ + "--keep-daily ${cfg.pruneRetention.daily}" + "--keep-weekly ${cfg.pruneRetention.weekly}" + "--keep-monthly ${cfg.pruneRetention.monthly}" + ]; + }; + + # Wire the pre/post hooks around the restic job + systemd.services."restic-backups-homey" = { + requires = [ "homey-backup-pre.service" ]; + after = [ "homey-backup-pre.service" ]; + }; + + systemd.services."homey-backup-post" = { + after = [ "restic-backups-homey.service" ]; + wantedBy = [ "restic-backups-homey.service" ]; + }; + }; +} diff --git a/modules/caddy.nix b/modules/caddy.nix new file mode 100644 index 0000000..901ca2b --- /dev/null +++ b/modules/caddy.nix @@ -0,0 +1,185 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Caddy reverse proxy. +# +# Features: +# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.home.zakobar.com +# - forward_auth to Authelia for protected vhosts +# - Plain reverse_proxy for public vhosts (authelia itself, nextcloud) +# - Listens on :80 (redirect) and :443 (TLS) +# +# Because nixpkgs ships Caddy without the cloudflare DNS plugin by default, +# we build a custom Caddy with it using the xcaddy wrapper from nixpkgs. +# +# Secrets consumed from sops: +# cloudflare/api_token + +let + cfg = config.homey.caddy; + domain = homeyConfig.domain; + + # Build Caddy with the Cloudflare DNS plugin. + # This compiles on the Pi (slow once, cached after). + caddyWithCloudflare = pkgs.caddy.override { + externalPlugins = [ + { + name = "github.com/caddy-dns/cloudflare"; + version = "89f16b99c18ef49c8bb470a82f895bce01cbaece"; + } + ]; + vendorHash = lib.fakeHash; # replace with real hash after first build + }; + + # Reusable Authelia forward_auth snippet + # Returns a Caddyfile snippet block that applies forward_auth. + # copy_headers makes Authelia's Remote-* headers available downstream. + autheliaForwardAuth = '' + forward_auth localhost:9091 { + uri /api/verify?rd=https://auth.${domain} + copy_headers Remote-User Remote-Name Remote-Groups Remote-Email + # On auth failure, redirect to the authelia login page + @goauth status 401 + handle_response @goauth { + redir https://auth.${domain}?rm={method} 302 + } + } + ''; + +in +{ + options.homey.caddy = { + enable = lib.mkEnableOption "Caddy reverse proxy"; + + acmeEmail = lib.mkOption { + type = lib.types.str; + default = "admin@zakobar.com"; + description = "Email for Let's Encrypt ACME registration."; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."cloudflare/api_token" = { + owner = config.services.caddy.user; + }; + + # ----------------------------------------------------------------------- + # Caddy service + # ----------------------------------------------------------------------- + services.caddy = { + enable = true; + package = caddyWithCloudflare; + + # Global options + globalConfig = '' + email ${cfg.acmeEmail} + # Use Cloudflare DNS-01 challenge for wildcard cert + acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN} + ''; + + # Each virtual host + virtualHosts = { + + # ------------------------------------------------------------------ + # Authelia — public, no auth gate (it IS the auth gate) + # ------------------------------------------------------------------ + "auth.${domain}" = { + extraConfig = '' + reverse_proxy localhost:9091 + ''; + }; + + # ------------------------------------------------------------------ + # Gitea — protected behind one_factor Authelia + # ------------------------------------------------------------------ + "git.${domain}" = { + extraConfig = '' + ${autheliaForwardAuth} + reverse_proxy localhost:3000 + ''; + }; + + # ------------------------------------------------------------------ + # Nextcloud — public auth (Nextcloud manages its own users + LDAP) + # Authelia is not gating nextcloud directly because NC has its own + # login flow. We still want HTTPS. + # ------------------------------------------------------------------ + "nextcloud.${domain}" = { + extraConfig = '' + # Redirect CardDAV/CalDAV discovery + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 + + # Large uploads (5 GB) + request_body { + max_size 5GB + } + + reverse_proxy localhost:8080 + ''; + }; + + # ------------------------------------------------------------------ + # phpLDAPadmin — two_factor, admins only (enforced by authelia policy) + # ------------------------------------------------------------------ + "ldapadmin.${domain}" = { + extraConfig = '' + ${autheliaForwardAuth} + reverse_proxy localhost:8081 + ''; + }; + + # ------------------------------------------------------------------ + # Jellyfin — one_factor (added when enabled) + # ------------------------------------------------------------------ + "jellyfin.${domain}" = { + extraConfig = '' + ${autheliaForwardAuth} + reverse_proxy localhost:8096 + ''; + }; + + # ------------------------------------------------------------------ + # Transmission — two_factor, admins only (enforced by authelia policy) + # ------------------------------------------------------------------ + "torrent.${domain}" = { + extraConfig = '' + ${autheliaForwardAuth} + reverse_proxy localhost:9091_transmission + ''; + # NOTE: transmission uses 9091 too; we'll bind it to 9092 in its + # module to avoid a clash with authelia. + }; + + }; + }; + + # ----------------------------------------------------------------------- + # Pass Cloudflare token as env var to the caddy systemd unit + # ----------------------------------------------------------------------- + systemd.services.caddy = { + serviceConfig = { + EnvironmentFile = pkgs.writeText "caddy-cf-env" + "CLOUDFLARE_API_TOKEN_FILE=${config.sops.secrets."cloudflare/api_token".path}"; + # Caddy supports _FILE suffix for env vars via its secret file reader, + # but cloudflare plugin reads CLOUDFLARE_API_TOKEN directly. + # We write a wrapper ExecStartPre to populate the env var from the file: + ExecStartPre = [ + (pkgs.writeShellScript "caddy-inject-cf-token" '' + export CLOUDFLARE_API_TOKEN=$(cat ${config.sops.secrets."cloudflare/api_token".path}) + systemctl set-environment CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" + '') + ]; + }; + after = lib.mkAfter [ "podman-authelia.service" ]; + wants = lib.mkAfter [ "podman-authelia.service" ]; + }; + + # ----------------------------------------------------------------------- + # Firewall — open HTTP + HTTPS (already in common.nix, explicit here too) + # ----------------------------------------------------------------------- + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; +} diff --git a/modules/cloudflared.nix b/modules/cloudflared.nix new file mode 100644 index 0000000..c810f61 --- /dev/null +++ b/modules/cloudflared.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Cloudflare Tunnel (cloudflared) — remote access without open inbound ports. +# +# Architecture: +# Internet → Cloudflare edge → cloudflared tunnel (outbound from Pi) +# → Caddy on localhost → service containers +# +# The tunnel is configured to route each hostname to Caddy's HTTPS listener. +# Caddy handles TLS and forward_auth; cloudflared just carries the traffic. +# +# Setup steps (one-time, done from the Cloudflare dashboard): +# 1. Go to Zero Trust → Networks → Tunnels → Create a tunnel +# 2. Name it (e.g. "pi-main") +# 3. Copy the tunnel token — add it to secrets.yaml as cloudflare/tunnel_token +# 4. In the tunnel's "Public Hostnames" config, add routes: +# auth.home.zakobar.com → http://localhost:80 (or https://localhost:443) +# git.home.zakobar.com → https://localhost:443 +# nextcloud.home.zakobar.com → https://localhost:443 +# ldapadmin.home.zakobar.com → https://localhost:443 +# jellyfin.home.zakobar.com → https://localhost:443 +# torrent.home.zakobar.com → https://localhost:443 +# Set "No TLS Verify" = true (Caddy's cert is from Let's Encrypt but +# the hostname seen by cloudflared is localhost, so hostname verification +# would fail without this flag). +# +# The tunnel_token approach (--token) is the simplest: one secret, no config +# file needed on the Pi. +# +# Secrets consumed from sops: +# cloudflare/tunnel_token + +let + cfg = config.homey.cloudflared; +in +{ + options.homey.cloudflared = { + enable = lib.mkEnableOption "Cloudflare Tunnel for remote access"; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."cloudflare/tunnel_token" = { owner = "cloudflared"; }; + + # ----------------------------------------------------------------------- + # cloudflared service + # NixOS 24.11 ships services.cloudflared natively. + # ----------------------------------------------------------------------- + services.cloudflared = { + enable = true; + tunnels = { + "pi-main" = { + # credentialsFile is not used with token-based auth; + # the token is passed via environment variable instead. + # We override the systemd unit below to inject it. + default = "http_status:404"; + }; + }; + }; + + # Inject the tunnel token from the sops secret file + systemd.services."cloudflared-tunnel-pi-main" = { + serviceConfig = { + ExecStart = lib.mkForce (pkgs.writeShellScript "cloudflared-start" '' + exec ${pkgs.cloudflared}/bin/cloudflared tunnel \ + --no-autoupdate \ + run \ + --token "$(cat ${config.sops.secrets."cloudflare/tunnel_token".path})" + ''); + }; + after = lib.mkAfter [ "caddy.service" ]; + wants = lib.mkAfter [ "caddy.service" ]; + }; + }; +} diff --git a/modules/common.nix b/modules/common.nix new file mode 100644 index 0000000..df8aa6b --- /dev/null +++ b/modules/common.nix @@ -0,0 +1,117 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Common configuration shared by every host in the homey ecosystem. +# Hardware-specific settings (disk layout, device trees, etc.) go in +# hosts//hardware.nix instead. + +{ + # ------------------------------------------------------------------------- + # Nix / flakes + # ------------------------------------------------------------------------- + nix = { + settings = { + experimental-features = [ "nix-command" "flakes" ]; + # Save disk space on Pi + auto-optimise-store = true; + }; + # Weekly garbage collection — keeps the system from filling the SD card + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 14d"; + }; + }; + + # Allow unfree packages (e.g. cloudflared binary) + nixpkgs.config.allowUnfree = true; + + # ------------------------------------------------------------------------- + # Boot — set in hardware.nix; this is just a safe default + # ------------------------------------------------------------------------- + # boot.loader is intentionally left to hardware.nix + + # ------------------------------------------------------------------------- + # Locale / timezone + # ------------------------------------------------------------------------- + time.timeZone = homeyConfig.timezone; + i18n.defaultLocale = "en_US.UTF-8"; + + # ------------------------------------------------------------------------- + # Networking + # ------------------------------------------------------------------------- + networking = { + # hostname is set per-host in default.nix + firewall = { + enable = true; + allowedTCPPorts = [ + 22 # SSH + 80 # Caddy HTTP (redirect to HTTPS or ACME challenge) + 443 # Caddy HTTPS + ]; + }; + # Use systemd-resolved for DNS — supports mDNS and local overrides + nameservers = [ "1.1.1.1" "8.8.8.8" ]; + }; + + # ------------------------------------------------------------------------- + # SSH + # ------------------------------------------------------------------------- + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; + + # ------------------------------------------------------------------------- + # Container runtime — podman (rootless-capable, no daemon needed) + # ------------------------------------------------------------------------- + virtualisation.podman = { + enable = true; + dockerCompat = true; # allow `docker` CLI commands against podman + defaultNetwork.settings.dns_enabled = true; + }; + + # ------------------------------------------------------------------------- + # Core packages available on every host + # ------------------------------------------------------------------------- + environment.systemPackages = with pkgs; [ + git + vim + htop + curl + wget + rsync + lsof + sops # secret editing + age # key generation for sops + restic # backup (CLI, also used by services.restic) + podman-compose + ]; + + # ------------------------------------------------------------------------- + # sops-nix global config — point at the secrets file and the host's age key + # ------------------------------------------------------------------------- + sops = { + defaultSopsFile = ../secrets/secrets.yaml; + # The age private key must be present on the host at this path. + # Generate on the Pi with: age-keygen -o /var/lib/sops-nix/key.txt + # Then add the PUBLIC key to secrets/.sops.yaml before encrypting. + age.keyFile = "/var/lib/sops-nix/key.txt"; + }; + + # ------------------------------------------------------------------------- + # Admin user — adjust username / SSH key in hosts//default.nix + # ------------------------------------------------------------------------- + users.mutableUsers = false; # all user config must be declared here + + # The actual admin user is declared in hosts//default.nix so the + # SSH authorized key can be host-specific. + + # ------------------------------------------------------------------------- + # System state version — do not change after first install + # (tracks NixOS backwards-compat markers) + # ------------------------------------------------------------------------- + system.stateVersion = "24.11"; +} diff --git a/modules/services/authelia.nix b/modules/services/authelia.nix new file mode 100644 index 0000000..4b1b453 --- /dev/null +++ b/modules/services/authelia.nix @@ -0,0 +1,200 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Authelia — SSO gateway. +# +# Connects to OpenLDAP on 127.0.0.1:389. +# Exposes port 9091 on localhost; Caddy reverse-proxies it and provides +# the forward_auth endpoint for protected vhosts. +# +# Volume layout: +# /authelia/config/ → /config (sqlite db, notification log, etc.) +# +# The configuration file is rendered by Nix (no Go templates) and written +# to a NixOS-managed path, then bind-mounted read-only into the container. +# +# Secrets consumed from sops: +# authelia/jwt_secret +# authelia/session_secret +# authelia/storage_encryption_key +# openldap/ro_password (shared with openldap module) + +let + cfg = config.homey.authelia; + dataDir = config.homey.storage.mountPoint; + domain = homeyConfig.domain; + + # LDAP base DN derived from domain: home.zakobar.com → dc=home,dc=zakobar,dc=com + ldapBaseDN = lib.concatStringsSep "," + (map (p: "dc=${p}") (lib.splitString "." domain)); + + # The authelia config is written as a Nix string so all values are + # resolved at build time except for secrets, which are injected at + # runtime via a wrapper script (same pattern as openldap). + autheliaConfig = '' + ############################################################### + # Authelia configuration # + # Generated by NixOS — do not edit by hand # + ############################################################### + theme: "light" + log: + level: "info" + + # jwt_secret injected at runtime via env var AUTHELIA_JWT_SECRET_FILE + authentication_backend: + ldap: + implementation: "custom" + url: "ldap://127.0.0.1:389" + timeout: "5s" + start_tls: false + base_dn: "${ldapBaseDN}" + users_filter: "({username_attribute}={input})" + username_attribute: "uid" + additional_users_dn: "ou=users" + groups_filter: "(&(uniquemember=uid={input},ou=users,${ldapBaseDN})(objectclass=groupOfUniqueNames))" + group_name_attribute: "cn" + additional_groups_dn: "ou=groups" + mail_attribute: "mail" + display_name_attribute: "uid" + permit_referrals: false + permit_unauthenticated_bind: false + user: "cn=readonly,${ldapBaseDN}" + # password injected at runtime via AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE + + totp: + issuer: "${domain}" + disable: false + + session: + name: authelia_session + # secret injected at runtime via AUTHELIA_SESSION_SECRET_FILE + expiration: 3600 + inactivity: 7200 + domain: "${domain}" + + storage: + local: + path: "/config/db.sqlite3" + # encryption_key injected at runtime via AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE + + access_control: + default_policy: "deny" + rules: + - domain: + - "auth.${domain}" + policy: "bypass" + - domain: + - "ldapadmin.${domain}" + subject: + - "group:admins" + policy: "two_factor" + - domain: + - "ldapadmin.${domain}" + policy: "deny" + - domain: + - "torrent.${domain}" + subject: + - "group:admins" + policy: "two_factor" + - domain: + - "torrent.${domain}" + policy: "deny" + - domain: + - "git.${domain}" + policy: "one_factor" + - domain: + - "nextcloud.${domain}" + policy: "one_factor" + - domain: + - "jellyfin.${domain}" + policy: "one_factor" + + notifier: + filesystem: + filename: "/config/emails.txt" + + ntp: + address: "udp://time.cloudflare.com:123" + version: 3 + max_desync: "3s" + disable_startup_check: false + disable_failure: true + ''; + +in +{ + options.homey.authelia = { + enable = lib.mkEnableOption "Authelia SSO gateway"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/authelia/authelia:latest"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9091; + description = "Host port Authelia listens on (bound to 127.0.0.1)."; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."authelia/jwt_secret" = { owner = "root"; }; + sops.secrets."authelia/session_secret" = { owner = "root"; }; + sops.secrets."authelia/storage_encryption_key" = { owner = "root"; }; + # openldap/ro_password is declared in openldap.nix; reference it here too + # (sops-nix deduplicates identical declarations) + sops.secrets."openldap/ro_password" = { owner = "root"; }; + + # ----------------------------------------------------------------------- + # Write the config file into /etc (read-only in the container) + # ----------------------------------------------------------------------- + environment.etc."authelia/configuration.yml" = { + text = autheliaConfig; + mode = "0444"; + }; + + # ----------------------------------------------------------------------- + # Container + # ----------------------------------------------------------------------- + virtualisation.oci-containers.containers.authelia = { + image = cfg.image; + + ports = [ "127.0.0.1:${toString cfg.port}:9091" ]; + + environment = { + TZ = homeyConfig.timezone; + # Tell authelia to read secrets from files (its native mechanism) + AUTHELIA_JWT_SECRET_FILE = "/run/secrets/jwt_secret"; + AUTHELIA_SESSION_SECRET_FILE = "/run/secrets/session_secret"; + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = "/run/secrets/storage_encryption_key"; + AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = "/run/secrets/ldap_ro_password"; + }; + + volumes = [ + "/etc/authelia/configuration.yml:/config/configuration.yml:ro" + "${dataDir}/authelia/config:/config" + # Mount sops secret files into the container under /run/secrets/ + "${config.sops.secrets."authelia/jwt_secret".path}:/run/secrets/jwt_secret:ro" + "${config.sops.secrets."authelia/session_secret".path}:/run/secrets/session_secret:ro" + "${config.sops.secrets."authelia/storage_encryption_key".path}:/run/secrets/storage_encryption_key:ro" + "${config.sops.secrets."openldap/ro_password".path}:/run/secrets/ldap_ro_password:ro" + ]; + + extraOptions = [ + "--network=host" + "--hostname=authelia" + ]; + }; + + # ----------------------------------------------------------------------- + # Systemd — wait for openldap and external HD + # ----------------------------------------------------------------------- + systemd.services."podman-authelia" = { + after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; + }; + }; +} diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix new file mode 100644 index 0000000..baaa635 --- /dev/null +++ b/modules/services/gitea.nix @@ -0,0 +1,198 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Gitea — self-hosted Git service. +# +# Auth model: LDAP authentication is configured through Gitea's admin UI +# (or CLI) after first start. Reverse proxy auth headers from Caddy/Authelia +# handle transparent login. +# +# Volume layout: +# /gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.) +# +# The app.ini is rendered by Nix and bind-mounted read-only. +# +# Secrets consumed from sops: +# gitea/admin_password +# gitea/lfs_jwt_secret +# gitea/oauth2_jwt_secret +# gitea/internal_token + +let + cfg = config.homey.gitea; + dataDir = config.homey.storage.mountPoint; + domain = homeyConfig.domain; + + # Gitea app.ini — generated at build time. + # Secrets that Gitea reads from env vars are referenced as env var names here. + # The actual values are injected by the ExecStartPre wrapper below. + giteaAppIni = '' + APP_NAME = ${homeyConfig.organization} + RUN_MODE = prod + RUN_USER = git + WORK_PATH = /data/gitea + + [repository] + ROOT = /data/git/repositories + + [repository.local] + LOCAL_COPY_PATH = /data/gitea/tmp/local-repo + + [repository.upload] + TEMP_PATH = /data/gitea/uploads + + [server] + APP_DATA_PATH = /data/gitea + DOMAIN = git.${domain} + HTTP_PORT = 3000 + ROOT_URL = https://git.${domain}/ + DISABLE_SSH = true + LFS_START_SERVER = true + ; LFS_JWT_SECRET injected at container start via env var / startup script + LFS_JWT_SECRET = __GITEA_LFS_JWT_SECRET__ + OFFLINE_MODE = false + + [lfs] + PATH = /data/git/lfs + + [database] + DB_TYPE = sqlite3 + PATH = /data/gitea/gitea.db + LOG_SQL = false + + [indexer] + ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve + + [session] + PROVIDER_CONFIG = /data/gitea/sessions + PROVIDER = file + + [picture] + AVATAR_UPLOAD_PATH = /data/gitea/avatars + REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars + DISABLE_GRAVATAR = false + + [attachment] + PATH = /data/gitea/attachments + + [log] + MODE = console + LEVEL = info + ROUTER = console + ROOT_PATH = /data/gitea/log + + [security] + INSTALL_LOCK = true + REVERSE_PROXY_LIMIT = 1 + REVERSE_PROXY_TRUSTED_PROXIES = * + ; INTERNAL_TOKEN injected at container start + INTERNAL_TOKEN = __GITEA_INTERNAL_TOKEN__ + + [service] + DISABLE_REGISTRATION = true + REQUIRE_SIGNIN_VIEW = false + REGISTER_EMAIL_CONFIRM = false + ENABLE_NOTIFY_MAIL = false + ALLOW_ONLY_EXTERNAL_REGISTRATION = true + ENABLE_CAPTCHA = false + DEFAULT_ALLOW_CREATE_ORGANIZATION = true + DEFAULT_ENABLE_TIMETRACKING = true + NO_REPLY_ADDRESS = noreply.localhost + ENABLE_REVERSE_PROXY_AUTHENTICATION = true + ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true + + [mailer] + ENABLED = false + + [openid] + ENABLE_OPENID_SIGNIN = false + ENABLE_OPENID_SIGNUP = false + + [oauth2] + ENABLE = false + ; JWT_SECRET injected at container start + JWT_SECRET = __GITEA_OAUTH2_JWT_SECRET__ + ''; + +in +{ + options.homey.gitea = { + enable = lib.mkEnableOption "Gitea Git server"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/gitea/gitea:latest"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + description = "Host port Gitea listens on (bound to 127.0.0.1)."; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."gitea/admin_password" = { owner = "root"; }; + sops.secrets."gitea/lfs_jwt_secret" = { owner = "root"; }; + sops.secrets."gitea/oauth2_jwt_secret" = { owner = "root"; }; + sops.secrets."gitea/internal_token" = { owner = "root"; }; + + # ----------------------------------------------------------------------- + # Write the app.ini template to /etc (will be processed by ExecStartPre) + # ----------------------------------------------------------------------- + environment.etc."gitea/app.ini.tpl" = { + text = giteaAppIni; + mode = "0444"; + }; + + # ----------------------------------------------------------------------- + # Container + # ----------------------------------------------------------------------- + virtualisation.oci-containers.containers.gitea = { + image = cfg.image; + ports = [ "127.0.0.1:${toString cfg.port}:3000" ]; + + environment = { + USER_UID = "1000"; + USER_GID = "1000"; + # Tell gitea where to look for the config + GITEA_CUSTOM = "/data/gitea"; + }; + + volumes = [ + "${dataDir}/gitea/data:/data" + # The processed app.ini is written by ExecStartPre into /run/gitea-conf/ + "/run/gitea-conf/app.ini:/data/gitea/conf/app.ini:ro" + ]; + + extraOptions = [ "--network=host" ]; + }; + + # ----------------------------------------------------------------------- + # ExecStartPre: substitute secret placeholders into the ini template + # ----------------------------------------------------------------------- + systemd.services."podman-gitea" = { + serviceConfig = { + ExecStartPre = [ + (pkgs.writeShellScript "gitea-build-config" '' + set -euo pipefail + install -d -m 700 /run/gitea-conf + LFS=$(cat ${config.sops.secrets."gitea/lfs_jwt_secret".path}) + OAUTH=$(cat ${config.sops.secrets."gitea/oauth2_jwt_secret".path}) + TOKEN=$(cat ${config.sops.secrets."gitea/internal_token".path}) + sed \ + -e "s|__GITEA_LFS_JWT_SECRET__|$LFS|g" \ + -e "s|__GITEA_OAUTH2_JWT_SECRET__|$OAUTH|g" \ + -e "s|__GITEA_INTERNAL_TOKEN__|$TOKEN|g" \ + /etc/gitea/app.ini.tpl > /run/gitea-conf/app.ini + chmod 444 /run/gitea-conf/app.ini + '') + ]; + }; + after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; + }; + }; +} diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix new file mode 100644 index 0000000..e3babaa --- /dev/null +++ b/modules/services/jellyfin.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Jellyfin — media server. (Deferred — enable when ready.) +# +# Volume layout: +# /jellyfin/config/ → /config +# /media/movies/ → /data/movies +# /media/tvshows/ → /data/tvshows + +let + cfg = config.homey.jellyfin; + dataDir = config.homey.storage.mountPoint; + domain = homeyConfig.domain; +in +{ + options.homey.jellyfin = { + enable = lib.mkEnableOption "Jellyfin media server"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/jellyfin/jellyfin:latest"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8096; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers.jellyfin = { + image = cfg.image; + ports = [ "127.0.0.1:${toString cfg.port}:8096" ]; + + environment = { + JELLYFIN_PublishedServerUrl = "https://jellyfin.${domain}"; + PUID = "1000"; + PGID = "1000"; + }; + + volumes = [ + "${dataDir}/jellyfin/config:/config" + "${dataDir}/media/movies:/data/movies:ro" + "${dataDir}/media/tvshows:/data/tvshows:ro" + ]; + + extraOptions = [ "--network=host" ]; + }; + + systemd.services."podman-jellyfin" = { + after = lib.mkAfter [ "mnt-data.mount" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; + }; + }; +} diff --git a/modules/services/nextcloud.nix b/modules/services/nextcloud.nix new file mode 100644 index 0000000..6e86423 --- /dev/null +++ b/modules/services/nextcloud.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Nextcloud + PostgreSQL. +# +# Two containers: +# nextcloud-postgres — PostgreSQL, bound to localhost:5432 +# nextcloud — Nextcloud PHP-FPM + Apache, bound to localhost:8080 +# +# Volume layout: +# /nextcloud/db/ → /var/lib/postgresql/data (postgres) +# /nextcloud/html/ → /var/www/html (nextcloud) +# +# Secrets consumed from sops: +# nextcloud/admin_password +# nextcloud/postgres_password + +let + cfg = config.homey.nextcloud; + dataDir = config.homey.storage.mountPoint; + domain = homeyConfig.domain; +in +{ + options.homey.nextcloud = { + enable = lib.mkEnableOption "Nextcloud file server"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/nextcloud:latest"; + }; + + postgresImage = lib.mkOption { + type = lib.types.str; + default = "docker.io/postgres:16"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "Host port Nextcloud listens on (bound to 127.0.0.1)."; + }; + + postgresPort = lib.mkOption { + type = lib.types.port; + default = 5432; + description = "Host port PostgreSQL listens on (bound to 127.0.0.1)."; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."nextcloud/admin_password" = { owner = "root"; }; + sops.secrets."nextcloud/postgres_password" = { owner = "root"; }; + + # ----------------------------------------------------------------------- + # PostgreSQL container + # ----------------------------------------------------------------------- + virtualisation.oci-containers.containers.nextcloud-postgres = { + image = cfg.postgresImage; + ports = [ "127.0.0.1:${toString cfg.postgresPort}:5432" ]; + + environment = { + POSTGRES_DB = "nextcloud_db"; + POSTGRES_USER = "postgres"; + # Password injected via env file + }; + + volumes = [ + "${dataDir}/nextcloud/db:/var/lib/postgresql/data" + ]; + + extraOptions = [ "--network=host" ]; + }; + + systemd.services."podman-nextcloud-postgres" = { + serviceConfig = { + ExecStartPre = [ + (pkgs.writeShellScript "nc-postgres-secrets-env" '' + set -euo pipefail + install -m 600 /dev/null /run/nc-postgres-secrets.env + echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" \ + >> /run/nc-postgres-secrets.env + '') + ]; + EnvironmentFile = "/run/nc-postgres-secrets.env"; + }; + postStop = "rm -f /run/nc-postgres-secrets.env"; + after = lib.mkAfter [ "mnt-data.mount" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; + }; + + # ----------------------------------------------------------------------- + # Nextcloud container + # ----------------------------------------------------------------------- + virtualisation.oci-containers.containers.nextcloud = { + image = cfg.image; + ports = [ "127.0.0.1:${toString cfg.port}:80" ]; + + environment = { + POSTGRES_HOST = "127.0.0.1"; + POSTGRES_DB = "nextcloud_db"; + POSTGRES_USER = "postgres"; + NEXTCLOUD_ADMIN_USER = "admin"; + NEXTCLOUD_TRUSTED_DOMAINS = "nextcloud.${domain}"; + OVERWRITEPROTOCOL = "https"; + OVERWRITECLIURL = "https://nextcloud.${domain}"; + # Passwords injected via env file + }; + + volumes = [ + "${dataDir}/nextcloud/html:/var/www/html" + ]; + + extraOptions = [ "--network=host" ]; + }; + + systemd.services."podman-nextcloud" = { + serviceConfig = { + ExecStartPre = [ + (pkgs.writeShellScript "nc-secrets-env" '' + set -euo pipefail + install -m 600 /dev/null /run/nc-secrets.env + echo "POSTGRES_PASSWORD=$(cat ${config.sops.secrets."nextcloud/postgres_password".path})" >> /run/nc-secrets.env + echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat ${config.sops.secrets."nextcloud/admin_password".path})" >> /run/nc-secrets.env + '') + ]; + EnvironmentFile = "/run/nc-secrets.env"; + }; + postStop = "rm -f /run/nc-secrets.env"; + after = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ]; + requires = lib.mkAfter [ "mnt-data.mount" "podman-nextcloud-postgres.service" ]; + }; + }; +} diff --git a/modules/services/openldap.nix b/modules/services/openldap.nix new file mode 100644 index 0000000..95f4725 --- /dev/null +++ b/modules/services/openldap.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# OpenLDAP — central identity provider. +# +# Runs as a podman container (osixia/openldap). +# Listens on localhost:389 only — not exposed to the outside world. +# Authelia and other services connect to it over the container network (127.0.0.1). +# +# Volume layout on host: +# /openldap/etc-ldap-slapd.d/ → /etc/ldap/slapd.d (config DB) +# /openldap/var-lib-ldap/ → /var/lib/ldap (data) +# +# Secrets consumed from sops: +# openldap/admin_password +# openldap/config_password +# openldap/ro_password + +let + cfg = config.homey.openldap; + dataDir = config.homey.storage.mountPoint; +in +{ + options.homey.openldap = { + enable = lib.mkEnableOption "OpenLDAP identity provider"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/osixia/openldap:latest"; + description = "Container image to use for OpenLDAP."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 389; + description = "Host port OpenLDAP listens on (bound to 127.0.0.1)."; + }; + }; + + config = lib.mkIf cfg.enable { + # ----------------------------------------------------------------------- + # Secrets + # ----------------------------------------------------------------------- + sops.secrets."openldap/admin_password" = { owner = "root"; }; + sops.secrets."openldap/config_password" = { owner = "root"; }; + sops.secrets."openldap/ro_password" = { owner = "root"; }; + + # ----------------------------------------------------------------------- + # Container + # ----------------------------------------------------------------------- + virtualisation.oci-containers.containers.openldap = { + image = cfg.image; + + # Bind only to localhost — no external exposure + ports = [ "127.0.0.1:${toString cfg.port}:389" ]; + + environment = { + LDAP_ORGANISATION = homeyConfig.organization; + LDAP_DOMAIN = homeyConfig.domain; + LDAP_ADMIN_USERNAME = "admin"; + LDAP_READONLY_USER = "true"; + # TLS disabled — traffic stays on localhost + LDAP_TLS = "false"; + }; + + # Inject passwords from sops-managed secret files + environmentFiles = []; # we use secretFiles below instead + + # sops writes secret values to files; we read them into env vars + # via a wrapper script run as ExecStartPre (see systemd override below). + # Podman's --env-file doesn't support arbitrary paths, so we use + # a secrets tmpfile approach via the systemd unit override. + + volumes = [ + "${dataDir}/openldap/etc-ldap-slapd.d:/etc/ldap/slapd.d" + "${dataDir}/openldap/var-lib-ldap:/var/lib/ldap" + ]; + + extraOptions = [ + "--network=host" # simplest for single-host: services talk on 127.0.0.1 + "--hostname=openldap" + ]; + }; + + # ----------------------------------------------------------------------- + # Systemd override to inject sops secrets as env vars + # ----------------------------------------------------------------------- + # podman containers are managed by systemd units named + # podman-.service + systemd.services."podman-openldap" = { + serviceConfig = { + # Write an env file with secret values before the container starts, + # then pass it to podman run via EnvironmentFile. + ExecStartPre = [ + (pkgs.writeShellScript "openldap-secrets-env" '' + set -euo pipefail + install -m 600 /dev/null /run/openldap-secrets.env + echo "LDAP_ADMIN_PASSWORD=$(cat ${config.sops.secrets."openldap/admin_password".path})" >> /run/openldap-secrets.env + echo "LDAP_CONFIG_PASSWORD=$(cat ${config.sops.secrets."openldap/config_password".path})" >> /run/openldap-secrets.env + echo "LDAP_READONLY_USER_PASSWORD=$(cat ${config.sops.secrets."openldap/ro_password".path})" >> /run/openldap-secrets.env + '') + ]; + EnvironmentFile = "/run/openldap-secrets.env"; + }; + # Clean up the env file on stop + postStop = "rm -f /run/openldap-secrets.env"; + # Wait for the external HD to be mounted before starting + after = lib.mkAfter [ "mnt-data.mount" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; + }; + + # ----------------------------------------------------------------------- + # Firewall — openldap port is NOT opened externally (localhost only) + # ----------------------------------------------------------------------- + # No firewall rule needed; bound to 127.0.0.1. + }; +} diff --git a/modules/services/phpldapadmin.nix b/modules/services/phpldapadmin.nix new file mode 100644 index 0000000..3e5a11a --- /dev/null +++ b/modules/services/phpldapadmin.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# phpLDAPadmin — web UI for OpenLDAP management. +# +# Stateless container (no persistent volumes needed). +# Protected by Authelia two_factor, admins-only policy (defined in authelia.nix). +# Bound to localhost:8081; Caddy reverse-proxies it. + +let + cfg = config.homey.phpldapadmin; +in +{ + options.homey.phpldapadmin = { + enable = lib.mkEnableOption "phpLDAPadmin web interface"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/osixia/phpldapadmin:latest"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8081; + description = "Host port phpLDAPadmin listens on (bound to 127.0.0.1)."; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers.phpldapadmin = { + image = cfg.image; + ports = [ "127.0.0.1:${toString cfg.port}:80" ]; + + environment = { + PHPLDAPADMIN_HTTPS = "false"; + PHPLDAPADMIN_LDAP_HOSTS = "127.0.0.1"; # openldap on host network + }; + + extraOptions = [ "--network=host" ]; + }; + + systemd.services."podman-phpldapadmin" = { + after = lib.mkAfter [ "podman-openldap.service" ]; + wants = lib.mkAfter [ "podman-openldap.service" ]; + }; + }; +} diff --git a/modules/services/transmission.nix b/modules/services/transmission.nix new file mode 100644 index 0000000..74b50c2 --- /dev/null +++ b/modules/services/transmission.nix @@ -0,0 +1,61 @@ +{ config, lib, pkgs, homeyConfig, ... }: + +# Transmission — BitTorrent client. (Deferred — enable when ready.) +# +# NOTE: Transmission's web UI also runs on port 9091. To avoid clashing +# with Authelia (also 9091), this module binds Transmission to 9092. +# +# Volume layout: +# /transmission/config/ → /config +# /media/movies/ → /downloads/movies +# /media/tvshows/ → /downloads/tvshows +# /media/general/ → /downloads/general +# /media/complete/ → /downloads/complete + +let + cfg = config.homey.transmission; + dataDir = config.homey.storage.mountPoint; +in +{ + options.homey.transmission = { + enable = lib.mkEnableOption "Transmission torrent client"; + + image = lib.mkOption { + type = lib.types.str; + default = "docker.io/linuxserver/transmission:latest"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9092; + description = "Host port for Transmission web UI (9092 to avoid clash with authelia@9091)."; + }; + }; + + config = lib.mkIf cfg.enable { + virtualisation.oci-containers.containers.transmission = { + image = cfg.image; + ports = [ "127.0.0.1:${toString cfg.port}:9091" ]; + + environment = { + PUID = "1000"; + PGID = "1000"; + }; + + volumes = [ + "${dataDir}/transmission/config:/config" + "${dataDir}/media/movies:/downloads/movies" + "${dataDir}/media/tvshows:/downloads/tvshows" + "${dataDir}/media/general:/downloads/general" + "${dataDir}/media/complete:/downloads/complete" + ]; + + extraOptions = [ "--network=host" ]; + }; + + systemd.services."podman-transmission" = { + after = lib.mkAfter [ "mnt-data.mount" ]; + requires = lib.mkAfter [ "mnt-data.mount" ]; + }; + }; +} diff --git a/modules/storage.nix b/modules/storage.nix new file mode 100644 index 0000000..e276f5c --- /dev/null +++ b/modules/storage.nix @@ -0,0 +1,105 @@ +{ config, lib, pkgs, ... }: + +# External hard-drive storage module. +# +# Each host sets: +# homey.storage.device = "/dev/disk/by-id/usb-WD_..."; (by-id is stable across reboots) +# homey.storage.mountPoint = "/mnt/data"; (default) +# +# All service data lives under //, so the whole +# dataset can be browsed, backed up, or restored with plain filesystem tools. +# +# Directory layout under mountPoint: +# openldap/ +# etc-ldap-slapd.d/ ← /etc/ldap/slapd.d in container +# var-lib-ldap/ ← /var/lib/ldap in container +# authelia/ +# config/ ← /config in container (sqlite db etc.) +# gitea/ +# data/ ← /data in container +# nextcloud/ +# html/ ← /var/www/html in container +# db/ ← /var/lib/postgresql/data in postgres container +# jellyfin/ +# config/ +# media/ +# movies/ +# tvshows/ +# general/ +# complete/ +# transmission/ +# config/ +# restic-cache/ ← restic local cache (not the backup destination) + +let + cfg = config.homey.storage; +in +{ + options.homey.storage = { + device = lib.mkOption { + type = lib.types.str; + example = "/dev/disk/by-id/usb-WD_Elements_12345-0:0"; + description = '' + Block device for the external hard drive. + Use /dev/disk/by-id/ paths for stable identification across reboots. + Leave empty to skip automount (useful during initial setup). + ''; + default = ""; + }; + + mountPoint = lib.mkOption { + type = lib.types.str; + default = "/mnt/data"; + description = "Where the external HD is mounted. All service data lives here."; + }; + + fsType = lib.mkOption { + type = lib.types.str; + default = "ext4"; + description = "Filesystem type of the external drive."; + }; + }; + + config = lib.mkIf (cfg.device != "") { + # Mount the external drive + fileSystems."${cfg.mountPoint}" = { + device = cfg.device; + fsType = cfg.fsType; + options = [ + "defaults" + "nofail" # Don't block boot if drive is absent + "noatime" # Better performance / less SD wear + "x-systemd.automount" + "x-systemd.idle-timeout=0" + ]; + }; + + # Ensure the mount point directory exists + systemd.tmpfiles.rules = [ + "d ${cfg.mountPoint} 0755 root root -" + + # Service subdirectories — created on boot so containers can start + # even before any data is restored into them. + "d ${cfg.mountPoint}/openldap 0750 root root -" + "d ${cfg.mountPoint}/openldap/etc-ldap-slapd.d 0750 root root -" + "d ${cfg.mountPoint}/openldap/var-lib-ldap 0750 root root -" + "d ${cfg.mountPoint}/authelia 0750 root root -" + "d ${cfg.mountPoint}/authelia/config 0750 root root -" + "d ${cfg.mountPoint}/gitea 0750 root root -" + "d ${cfg.mountPoint}/gitea/data 0750 root root -" + "d ${cfg.mountPoint}/nextcloud 0750 root root -" + "d ${cfg.mountPoint}/nextcloud/html 0750 root root -" + "d ${cfg.mountPoint}/nextcloud/db 0750 root root -" + "d ${cfg.mountPoint}/jellyfin 0750 root root -" + "d ${cfg.mountPoint}/jellyfin/config 0750 root root -" + "d ${cfg.mountPoint}/media 0755 root root -" + "d ${cfg.mountPoint}/media/movies 0755 root root -" + "d ${cfg.mountPoint}/media/tvshows 0755 root root -" + "d ${cfg.mountPoint}/media/general 0755 root root -" + "d ${cfg.mountPoint}/media/complete 0755 root root -" + "d ${cfg.mountPoint}/transmission 0750 root root -" + "d ${cfg.mountPoint}/transmission/config 0750 root root -" + "d ${cfg.mountPoint}/restic-cache 0700 root root -" + ]; + }; +} diff --git a/scripts/backup-longhorn-to-disk.sh b/scripts/backup-longhorn-to-disk.sh deleted file mode 100755 index 21e2713..0000000 --- a/scripts/backup-longhorn-to-disk.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SRC="${SRC:-/mnt/replicas}" -DEST="${DEST:-/mnt2/homey-backup}" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -MANIFEST="$DEST/manifest.json" - -PVC_MAPPING=( - "pvc-0310a337-9642-464b-a458-fcb3439328e7-fbc07d5a:ldap-pvc" - "pvc-1cdc51ee-b965-4cab-baf7-077cc6df6f11-0fcfb9cd:authelia-pvc" - "pvc-4888bf84-62c8-4340-adbc-cb31073d8fd2-d065d20b:gitea-pvc" - "pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815:nextcloud-data-pvc" - "pvc-c5b28179-1b9c-462a-be5b-05c4f0bb36ca-5f2dbf4d:nextcloud-postgres-pvc" - "pvc-7f73ee94-5583-4e4a-9788-cba054214b1c-f767850a:radicale-pvc" - "pvc-9e75f35a-27c3-4251-b25a-1a876f82f6c7-c9c8b185:jellyfin-config-pvc" - "pvc-dfe2aa08-bbb8-423b-9001-fb6aea181597-baf06a7f:jellyfin-data-pvc" - "pvc-dd4a069a-a638-49c0-8c95-f954510816e5-7e81a6f6:transmission-config-pvc" - "pvc-e4ba414d-d9c2-4927-b0ae-f6bfb90ce311-a0963101:unknown-pvc-1" - "pvc-ec6afe10-aca3-42ce-9d89-32fc4ac77f9a-8d6baa34:unknown-pvc-2" -) - -progress_bar() { - local current=$1 - local total=$2 - local width=40 - local percent=$((current * 100 / total)) - local filled=$((current * width / total)) - local empty=$((width - filled)) - printf "\r[" - printf "%${filled}s" | tr ' ' '=' - printf "%${empty}s" | tr ' ' ' ' - printf "] %3d%% (%d/%d)" "$percent" "$current" "$total" -} - -get_pvc_name() { - local pvc_id="$1" - for mapping in "${PVC_MAPPING[@]}"; do - if [[ "$mapping" == "$pvc_id:"* ]]; then - echo "${mapping#*:}" - return - fi - done - echo "unknown" -} - -echo "========================================" -echo " Longhorn Volume Backup Tool" -echo "========================================" -echo "" -echo "Source: $SRC" -echo "Destination: $DEST" -echo "Timestamp: $TIMESTAMP" -echo "" - -mkdir -p "$DEST/volumes" -mkdir -p "$DEST/metadata" - -VOLUMES=() -TOTAL_SIZE=0 - -echo "Scanning volumes..." -for pvc_dir in "$SRC"/*/; do - pvc_name=$(basename "$pvc_dir") - friendly_name=$(get_pvc_name "$pvc_name") - VOLUMES+=("$pvc_name:$friendly_name") - - size=$(sudo du -sb "$pvc_dir" 2>/dev/null | awk '{print $1}' || echo "0") - TOTAL_SIZE=$((TOTAL_SIZE + size)) - - printf " %-50s %s\n" "$friendly_name" "$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")" -done - -TOTAL_VOLUMES=${#VOLUMES[@]} -echo "" -echo "Found $TOTAL_VOLUMES volumes, total size: $(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE")" -echo "" -read -p "Continue with backup? [y/N] " -n 1 -r -echo "" -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 1 -fi - -echo "" -echo "Starting backup..." -echo "" - -COPIED_SIZE=0 -START_TIME=$(date +%s) - -for i in "${!VOLUMES[@]}"; do - volume="${VOLUMES[$i]}" - pvc_name="${volume%%:*}" - friendly_name="${volume#*:}" - - CURRENT=$((i + 1)) - progress_bar "$CURRENT" "$TOTAL_VOLUMES" - echo " - $friendly_name" - - sudo rsync -a --no-owner --no-group --info=progress2 \ - "$SRC/$pvc_name/" \ - "$DEST/volumes/$pvc_name/" 2>&1 | while read -r line; do - if [[ "$line" =~ to-chk=*([0-9]+)/([0-9]+) ]]; then - printf "\r %s" "$line" - fi - done - - sudo chown -R "$USER:$USER" "$DEST/volumes/$pvc_name" 2>/dev/null || true - - if [[ -f "$SRC/$pvc_name/volume.meta" ]]; then - sudo cp "$SRC/$pvc_name/volume.meta" "$DEST/metadata/${pvc_name}.meta" 2>/dev/null || true - fi - - echo "" -done - -echo "" -echo "Generating manifest..." - -cat > "$MANIFEST" << EOF -{ - "backup_timestamp": "$TIMESTAMP", - "source_path": "$SRC", - "destination_path": "$DEST", - "total_volumes": $TOTAL_VOLUMES, - "total_size_bytes": $TOTAL_SIZE, - "volumes": [ -EOF - -FIRST=true -for volume in "${VOLUMES[@]}"; do - pvc_name="${volume%%:*}" - friendly_name="${volume#*:}" - - vol_size=$(sudo du -sb "$SRC/$pvc_name" 2>/dev/null | awk '{print $1}' || echo "0") - vol_size_hr=$(numfmt --to=iec-i --suffix=B "$vol_size" 2>/dev/null || echo "${vol_size}B") - - head_file=$(sudo find "$DEST/volumes/$pvc_name" -name "volume-head-*.img" 2>/dev/null | head -1) - head_file=$(basename "$head_file" 2>/dev/null || echo "") - - if [[ "$FIRST" == "true" ]]; then - FIRST=false - else - echo "," >> "$MANIFEST" - fi - - cat >> "$MANIFEST" << EOF - { - "pvc_id": "$pvc_name", - "friendly_name": "$friendly_name", - "size_bytes": $vol_size, - "size_human": "$vol_size_hr", - "volume_head": "$head_file", - "backup_path": "volumes/$pvc_name" - } -EOF -done - -cat >> "$MANIFEST" << EOF - ] -} -EOF - -END_TIME=$(date +%s) -DURATION=$((END_TIME - START_TIME)) - -echo "" -echo "========================================" -echo " Backup Complete!" -echo "========================================" -echo "" -echo "Duration: $((DURATION / 60))m $((DURATION % 60))s" -echo "Location: $DEST" -echo "Manifest: $MANIFEST" -echo "" -echo "Backup size:" -sudo du -sh "$DEST/volumes" -echo "" -echo "To mount a volume, run:" -echo " ./scripts/mount-longhorn-volume.sh " -echo "" -echo "To restore a volume, run:" -echo " ./scripts/restore-longhorn-volume.sh " diff --git a/scripts/get-pvc-mapping.sh b/scripts/get-pvc-mapping.sh deleted file mode 100644 index 87f72f4..0000000 --- a/scripts/get-pvc-mapping.sh +++ /dev/null @@ -1,16 +0,0 @@ -for dir in /mnt/replicas/pvc-*/; do - name=$(basename "$dir") - head=$(sudo find "$dir" -name "volume-head-*.img" | head -1) - - sudo mkdir -p /tmp/inspect - loop=$(sudo losetup -fP --show "$head") - - echo "=== $name ===" - sudo mount "$loop" /tmp/inspect 2>/dev/null && { - sudo ls -la /tmp/inspect | head -10 - sudo umount /tmp/inspect - } || echo "(mount failed)" - - sudo losetup -d "$loop" - echo "" -done diff --git a/scripts/list-longhorn-backups.sh b/scripts/list-longhorn-backups.sh deleted file mode 100755 index 661ff08..0000000 --- a/scripts/list-longhorn-backups.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}" -MANIFEST="$BACKUP_DIR/manifest.json" - -echo "========================================" -echo " Longhorn Volume Backup List" -echo "========================================" -echo "" - -if [[ ! -f "$MANIFEST" ]]; then - echo "No manifest found at $MANIFEST" - echo "Run backup-longhorn-to-disk.sh first." - exit 1 -fi - -echo "Backup timestamp: $(grep -oP '"backup_timestamp":\s*"\K[^"]+' "$MANIFEST")" -echo "Source: $(grep -oP '"source_path":\s*"\K[^"]+' "$MANIFEST")" -echo "Total volumes: $(grep -oP '"total_volumes":\s*\K[0-9]+' "$MANIFEST")" -echo "Total size: $(grep -oP '"total_size_bytes":\s*\K[0-9]+' "$MANIFEST" | numfmt --to=iec-i --suffix=B)" -echo "" -echo "Volumes:" -echo "----------------------------------------" - -grep -A5 '"volumes"' "$MANIFEST" | grep -E '"friendly_name"|"size_human"' | \ - while read -r name_line; read -r size_line; do - name=$(echo "$name_line" | grep -oP '"friendly_name":\s*"\K[^"]+') - size=$(echo "$size_line" | grep -oP '"size_human":\s*"\K[^"]+') - pvc=$(grep -B1 "$name_line" "$MANIFEST" | grep -oP '"pvc_id":\s*"\K[^"]+' || echo "") - printf " %-30s %10s %s\n" "$name" "$size" "$pvc" - done - -echo "" -echo "Commands:" -echo " Mount: ./scripts/mount-longhorn-volume.sh " -echo " Restore: ./scripts/restore-longhorn-volume.sh " diff --git a/scripts/longhorn-fuse.py b/scripts/longhorn-fuse.py deleted file mode 100644 index 863252b..0000000 --- a/scripts/longhorn-fuse.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import sys -from fuse import FUSE, FuseOSError, Operations - -class LonghornBackupFS(Operations): - def __init__(self, backup_dir): - self.backup_dir = backup_dir - self.blocks_dir = f"{backup_dir}/blocks" - - backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg" - with open(backup_cfg) as f: - data = json.load(f) - - self.size = int(data['Size']) - self.block_map = {b['Offset']: b['BlockChecksum'] for b in data['Blocks']} - self.block_size = 2097152 # 2MB - - print(f"Volume size: {self.size}") - print(f"Blocks: {len(self.block_map)}") - - def getattr(self, path, fh=None): - return {'st_size': self.size, 'st_mode': 0o100644, 'st_nlink': 1} - - def read(self, path, size, offset, fh): - result = bytearray() - remaining = size - current_offset = offset - - while remaining > 0: - block_start = (current_offset // self.block_size) * self.block_size - block_offset = current_offset - block_start - read_size = min(remaining, self.block_size - block_offset) - - if block_start in self.block_map: - checksum = self.block_map[block_start] - block_path = f"{self.blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk" - - if os.path.exists(block_path): - with open(block_path, 'rb') as f: - f.seek(block_offset) - result.extend(f.read(read_size)) - else: - result.extend(b'\x00' * read_size) - else: - result.extend(b'\x00' * read_size) - - current_offset += read_size - remaining -= read_size - - return bytes(result) - -if __name__ == '__main__': - if len(sys.argv) < 3: - print(f"Usage: {sys.argv[0]} ") - print(f"Example: {sys.argv[0]} /mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842 /tmp/longhorn-fuse") - sys.exit(1) - - backup_dir = sys.argv[1] - mount_point = sys.argv[2] - - os.makedirs(mount_point, exist_ok=True) - - print(f"Mounting {backup_dir} at {mount_point}") - print("This creates a virtual block device file at the mount point") - print("Then run: sudo losetup -fP {mount_point}/volume.img && sudo mount /dev/loopX /mnt/point") - - fs = LonghornBackupFS(backup_dir) - fuse = FUSE(fs, mount_point, nothreads=True, foreground=True, allow_other=True) diff --git a/scripts/longhorn-nbdkit.py b/scripts/longhorn-nbdkit.py deleted file mode 100644 index 114953e..0000000 --- a/scripts/longhorn-nbdkit.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -import nbdkit -import json -import os - -backup_dir = "/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842" -blocks_dir = f"{backup_dir}/blocks" -backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg" - -with open(backup_cfg) as f: - data = json.load(f) - -size = int(data['Size']) -block_map = {b['Offset']: b['BlockChecksum'] for b in data['Blocks']} -block_size = 2097152 - -def thread_model(): - return nbdkit.THREAD_MODEL_SERIALIZE_ALL_REQUESTS - -def get_size(): - return size - -def pread(h, count, offset, flags): - result = bytearray() - remaining = count - current_offset = offset - - while remaining > 0: - block_start = (current_offset // block_size) * block_size - block_offset = current_offset - block_start - read_size = min(remaining, block_size - block_offset) - - if block_start in block_map: - checksum = block_map[block_start] - block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk" - - if os.path.exists(block_path): - with open(block_path, 'rb') as f: - f.seek(block_offset) - result.extend(f.read(read_size)) - else: - result.extend(b'\x00' * read_size) - else: - result.extend(b'\x00' * read_size) - - current_offset += read_size - remaining -= read_size - - return bytes(result) diff --git a/scripts/mount-longhorn-volume.sh b/scripts/mount-longhorn-volume.sh deleted file mode 100755 index 58b35f7..0000000 --- a/scripts/mount-longhorn-volume.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}" -MOUNT_BASE="${MOUNT_BASE:-/mnt/longhorn-volumes}" - -usage() { - echo "Usage: $0 [mount-point]" - echo "" - echo "Mounts a Longhorn volume backup for exploration." - echo "" - echo "Arguments:" - echo " pvc-name-or-friendly-name The PVC ID or friendly name (e.g., 'nextcloud-data-pvc')" - echo " mount-point Optional custom mount point (default: $MOUNT_BASE/)" - echo "" - echo "Examples:" - echo " $0 nextcloud-data-pvc" - echo " $0 pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842-96d72815" - echo " $0 nextcloud-data-pvc /mnt/my-mount" - echo "" - echo "To unmount, run:" - echo " sudo umount " - echo " sudo losetup -d /dev/loopX" - exit 1 -} - -if [[ $# -lt 1 ]]; then - usage -fi - -SEARCH_NAME="$1" -CUSTOM_MOUNT="${2:-}" - -MANIFEST="$BACKUP_DIR/manifest.json" - -if [[ ! -f "$MANIFEST" ]]; then - echo "Error: Manifest not found at $MANIFEST" - echo "Make sure you've run the backup script first." - exit 1 -fi - -find_volume() { - local search="$1" - local found="" - - while IFS= read -r line; do - pvc_id=$(echo "$line" | grep -oP '"pvc_id":\s*"\K[^"]+') - friendly=$(echo "$line" | grep -oP '"friendly_name":\s*"\K[^"]+') - - if [[ "$pvc_id" == "$search" ]] || [[ "$friendly" == "$search" ]]; then - echo "$pvc_id:$friendly" - return 0 - fi - done < <(grep -A6 '"volumes"' "$MANIFEST" | grep -E '"pvc_id"|"friendly_name"') - - return 1 -} - -VOLUME_INFO=$(find_volume "$SEARCH_NAME") - -if [[ -z "$VOLUME_INFO" ]]; then - echo "Error: Volume '$SEARCH_NAME' not found in manifest." - echo "" - echo "Available volumes:" - grep -oP '"friendly_name":\s*"\K[^"]+' "$MANIFEST" | while read -r name; do - echo " - $name" - done - exit 1 -fi - -PVC_ID="${VOLUME_INFO%%:*}" -FRIENDLY_NAME="${VOLUME_INFO#*:}" - -VOLUME_DIR="$BACKUP_DIR/volumes/$PVC_ID" - -if [[ ! -d "$VOLUME_DIR" ]]; then - echo "Error: Volume directory not found: $VOLUME_DIR" - exit 1 -fi - -VOLUME_HEAD=$(find "$VOLUME_DIR" -name "volume-head-*.img" | head -1) - -if [[ -z "$VOLUME_HEAD" ]]; then - echo "Error: No volume-head-*.img file found in $VOLUME_DIR" - echo "Contents:" - ls -la "$VOLUME_DIR" - exit 1 -fi - -if [[ -n "$CUSTOM_MOUNT" ]]; then - MOUNT_POINT="$CUSTOM_MOUNT" -else - MOUNT_POINT="$MOUNT_BASE/$FRIENDLY_NAME" -fi - -echo "========================================" -echo " Mount Longhorn Volume" -echo "========================================" -echo "" -echo "PVC ID: $PVC_ID" -echo "Name: $FRIENDLY_NAME" -echo "Volume file: $(basename "$VOLUME_HEAD")" -echo "Mount point: $MOUNT_POINT" -echo "" - -LOOP_DEV=$(sudo losetup -fP --show "$VOLUME_HEAD") -echo "Attached to: $LOOP_DEV" - -sudo mkdir -p "$MOUNT_POINT" - -echo "" -echo "Mounting..." -if sudo mount "$LOOP_DEV" "$MOUNT_POINT" 2>/dev/null; then - echo "" - echo "========================================" - echo " Mounted Successfully!" - echo "========================================" - echo "" - echo "Mount point: $MOUNT_POINT" - echo "Loop device: $LOOP_DEV" - echo "" - echo "Contents:" - ls -la "$MOUNT_POINT" 2>/dev/null | head -20 - echo "" - echo "To unmount:" - echo " sudo umount $MOUNT_POINT" - echo " sudo losetup -d $LOOP_DEV" -else - echo "Mount failed. Trying with filesystem detection..." - FS_TYPE=$(sudo blkid -o value -s TYPE "$LOOP_DEV" 2>/dev/null || echo "") - - if [[ -n "$FS_TYPE" ]]; then - echo "Detected filesystem: $FS_TYPE" - sudo mount -t "$FS_TYPE" "$LOOP_DEV" "$MOUNT_POINT" - echo "" - echo "Mounted successfully at $MOUNT_POINT" - else - echo "Could not detect filesystem. Volume may be empty or corrupted." - echo "" - echo "Loop device: $LOOP_DEV" - echo "Run 'sudo blkid $LOOP_DEV' to inspect." - echo "" - echo "To detach:" - echo " sudo losetup -d $LOOP_DEV" - fi -fi diff --git a/scripts/restore-fast.py b/scripts/restore-fast.py deleted file mode 100644 index 85f8745..0000000 --- a/scripts/restore-fast.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -import sys -import gzip -from concurrent.futures import ThreadPoolExecutor, as_completed - -backup_dir = "/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842" -output_img = "/mnt/nextcloud-restored.img" - -backup_cfg = f"{backup_dir}/backups/backup_backup-eac0221d1cab4a9c.cfg" -blocks_dir = f"{backup_dir}/blocks" - -with open(backup_cfg) as f: - data = json.load(f) - -blocks = data['Blocks'] -total = len(blocks) -size = int(data['Size']) - -print(f"Volume size: {size // 1024 // 1024 // 1024} GB") -print(f"Block count: {total}") - -os.makedirs(os.path.dirname(output_img) if os.path.dirname(output_img) else '.', exist_ok=True) - -if not os.path.exists(output_img): - import subprocess - subprocess.run(['truncate', '-s', str(size), output_img], check=True) - -with open(output_img, 'r+b') as img: - for i, block in enumerate(blocks): - offset = block['Offset'] - checksum = block['BlockChecksum'] - - block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk" - - if os.path.exists(block_path): - with gzip.open(block_path, 'rb') as bf: - img.seek(offset) - img.write(bf.read()) - - if (i + 1) % 500 == 0: - percent = (i + 1) * 100 // total - bar = '=' * (percent // 2) + ' ' * (50 - percent // 2) - sys.stdout.write(f"\r[{bar}] {percent}% ({i + 1}/{total})") - sys.stdout.flush() - -print(f"\nDone! Image: {output_img}") diff --git a/scripts/restore-longhorn-backup.sh b/scripts/restore-longhorn-backup.sh deleted file mode 100755 index 21784d5..0000000 --- a/scripts/restore-longhorn-backup.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="${1:-/mnt2/backed-up-drive/backupstore/volumes/2c/df/pvc-5c1f48e3-346f-4c35-8e6a-8fc0c4c3a842}" -OUTPUT_IMG="${2:-./nextcloud-data-restored.img}" - -BACKUP_CFG="$BACKUP_DIR/backups/backup_backup-eac0221d1cab4a9c.cfg" -BLOCKS_DIR="$BACKUP_DIR/blocks" - -if [[ ! -f "$BACKUP_CFG" ]]; then - echo "Error: Backup config not found at $BACKUP_CFG" - exit 1 -fi - -echo "========================================" -echo " Longhorn Backup Restore Tool" -echo "========================================" -echo "" -echo "Backup: $BACKUP_DIR" -echo "Output: $OUTPUT_IMG" -echo "" - -SIZE=$(python3 -c "import json; print(json.load(open('$BACKUP_CFG'))['Size'])") -BLOCK_COUNT=$(python3 -c "import json; print(len(json.load(open('$BACKUP_CFG'))['Blocks']))") - -echo "Volume size: $((SIZE / 1024 / 1024 / 1024)) GB ($SIZE bytes)" -echo "Block count: $BLOCK_COUNT" -echo "" - -read -p "Continue? [y/N] " -n 1 -r -echo "" -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 1 -fi - -echo "" -echo "Creating sparse image..." -truncate -s "$SIZE" "$OUTPUT_IMG" - -echo "Restoring blocks..." -python3 << 'PYEOF' -import json -import os -import sys - -backup_cfg = os.environ['BACKUP_CFG'] -blocks_dir = os.environ['BLOCKS_DIR'] -output_img = os.environ['OUTPUT_IMG'] - -with open(backup_cfg) as f: - data = json.load(f) - -blocks = data['Blocks'] -total = len(blocks) - -with open(output_img, 'r+b') as img: - for i, block in enumerate(blocks): - offset = block['Offset'] - checksum = block['BlockChecksum'] - - block_path = f"{blocks_dir}/{checksum[:2]}/{checksum[2:4]}/{checksum}.blk" - - if not os.path.exists(block_path): - print(f"Warning: Block not found: {checksum}") - continue - - with open(block_path, 'rb') as bf: - img.seek(offset) - img.write(bf.read()) - - if (i + 1) % 1000 == 0 or i + 1 == total: - percent = (i + 1) * 100 // total - bar = '=' * (percent // 2) + ' ' * (50 - percent // 2) - sys.stdout.write(f"\r[{bar}] {percent}% ({i + 1}/{total})") - sys.stdout.flush() - -print() -PYEOF - -echo "" -echo "========================================" -echo " Restore Complete!" -echo "========================================" -echo "" -echo "Image: $OUTPUT_IMG" -echo "Size: $(du -sh "$OUTPUT_IMG" | cut -f1)" -echo "" -echo "To mount:" -echo " sudo losetup -fP $OUTPUT_IMG" -echo " sudo mount /dev/loopX /mnt/point" -echo "" -echo "Or directly:" -echo " sudo mount -o loop $OUTPUT_IMG /mnt/point" diff --git a/scripts/restore-longhorn-volume.sh b/scripts/restore-longhorn-volume.sh deleted file mode 100755 index 5d78145..0000000 --- a/scripts/restore-longhorn-volume.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BACKUP_DIR="${BACKUP_DIR:-/mnt2/homey-backup}" -RESTORE_BASE="${RESTORE_BASE:-/mnt/replicas}" - -usage() { - echo "Usage: $0 [--dry-run]" - echo "" - echo "Restores a Longhorn volume backup to the replicas directory." - echo "" - echo "Arguments:" - echo " pvc-name-or-friendly-name The PVC ID or friendly name" - echo " --dry-run Show what would be done without copying" - echo "" - echo "Examples:" - echo " $0 nextcloud-data-pvc" - echo " $0 nextcloud-data-pvc --dry-run" - echo "" - echo "WARNING: This will overwrite existing data in $RESTORE_BASE" - exit 1 -} - -if [[ $# -lt 1 ]]; then - usage -fi - -SEARCH_NAME="$1" -DRY_RUN=false - -if [[ "${2:-}" == "--dry-run" ]]; then - DRY_RUN=true -fi - -MANIFEST="$BACKUP_DIR/manifest.json" - -if [[ ! -f "$MANIFEST" ]]; then - echo "Error: Manifest not found at $MANIFEST_DIR" - exit 1 -fi - -find_volume() { - local search="$1" - - while IFS= read -r line; do - pvc_id=$(echo "$line" | grep -oP '"pvc_id":\s*"\K[^"]+') - friendly=$(echo "$line" | grep -oP '"friendly_name":\s*"\K[^"]+') - - if [[ "$pvc_id" == "$search" ]] || [[ "$friendly" == "$search" ]]; then - echo "$pvc_id:$friendly" - return 0 - fi - done < <(grep -A6 '"volumes"' "$MANIFEST" | grep -E '"pvc_id"|"friendly_name"') - - return 1 -} - -VOLUME_INFO=$(find_volume "$SEARCH_NAME") - -if [[ -z "$VOLUME_INFO" ]]; then - echo "Error: Volume '$SEARCH_NAME' not found in manifest." - echo "" - echo "Available volumes:" - grep -oP '"friendly_name":\s*"\K[^"]+' "$MANIFEST" | while read -r name; do - echo " - $name" - done - exit 1 -fi - -PVC_ID="${VOLUME_INFO%%:*}" -FRIENDLY_NAME="${VOLUME_INFO#*:}" - -BACKUP_VOLUME_DIR="$BACKUP_DIR/volumes/$PVC_ID" -RESTORE_VOLUME_DIR="$RESTORE_BASE/$PVC_ID" - -echo "========================================" -echo " Restore Longhorn Volume" -echo "========================================" -echo "" -echo "PVC ID: $PVC_ID" -echo "Name: $FRIENDLY_NAME" -echo "Source: $BACKUP_VOLUME_DIR" -echo "Destination: $RESTORE_VOLUME_DIR" -echo "Dry run: $DRY_RUN" -echo "" - -if [[ "$DRY_RUN" == "true" ]]; then - echo "[DRY RUN] Would copy:" - du -sh "$BACKUP_VOLUME_DIR" 2>/dev/null || echo " (size unknown)" - echo "" - echo "Files to copy:" - find "$BACKUP_VOLUME_DIR" -type f | head -20 - exit 0 -fi - -if [[ -d "$RESTORE_VOLUME_DIR" ]]; then - echo "WARNING: Destination already exists!" - echo "" - read -p "Overwrite existing data? [y/N] " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 1 - fi - echo "" - echo "Removing existing data..." - sudo rm -rf "$RESTORE_VOLUME_DIR" -fi - -echo "Creating destination directory..." -sudo mkdir -p "$RESTORE_VOLUME_DIR" - -echo "Copying volume data..." -sudo rsync -a --no-owner --no-group --info=progress2 \ - "$BACKUP_VOLUME_DIR/" \ - "$RESTORE_VOLUME_DIR/" - -echo "" -echo "Setting permissions..." -sudo chmod 700 "$RESTORE_VOLUME_DIR" - -echo "" -echo "========================================" -echo " Restore Complete!" -echo "========================================" -echo "" -echo "Restored to: $RESTORE_VOLUME_DIR" -echo "" -echo "Size:" -sudo du -sh "$RESTORE_VOLUME_DIR" -echo "" -echo "Next steps:" -echo "1. Ensure Longhorn is configured to use $RESTORE_BASE" -echo "2. Restart Longhorn or the affected pod" -echo "3. Verify data integrity" diff --git a/secrets/.gitignore b/secrets/.gitignore new file mode 100644 index 0000000..2288976 --- /dev/null +++ b/secrets/.gitignore @@ -0,0 +1,10 @@ +# Never commit an unencrypted secrets file. +# The encrypted version (produced by `sops -e -i secrets.yaml`) IS committed. +# +# If you accidentally add the plaintext version, sops-encrypted files +# contain a `sops:` key at the top — check before committing. +# +# Paranoia: ignore any plaintext variants you might create while editing. +secrets.yaml.plaintext +secrets.yaml.bak +*.plain diff --git a/secrets/.sops.yaml b/secrets/.sops.yaml new file mode 100644 index 0000000..1d37ce0 --- /dev/null +++ b/secrets/.sops.yaml @@ -0,0 +1,24 @@ +# sops configuration — controls which keys can decrypt secrets.yaml. +# +# SETUP STEPS (do this once on the Pi): +# +# 1. Install age: nix-shell -p age +# 2. Generate a key: age-keygen -o /var/lib/sops-nix/key.txt +# 3. Print the pubkey: age-keygen -y /var/lib/sops-nix/key.txt +# 4. Replace AGE-PUBLIC-KEY-PI-MAIN below with the output of step 3. +# 5. (Optional) add your own age key or GPG key as a second recipient so +# you can edit secrets from your workstation without the Pi being on. +# +# To encrypt / edit secrets.yaml: +# sops secrets/secrets.yaml +# +# sops will re-encrypt the file for all keys listed here every time you save. + +creation_rules: + - path_regex: secrets/secrets\.yaml$ + key_groups: + - age: + # Pi main host key — replace with output of `age-keygen -y /var/lib/sops-nix/key.txt` + - AGE-PUBLIC-KEY-PI-MAIN-REPLACE-ME + # (Optional) your workstation key for offline editing: + # - AGE-PUBLIC-KEY-YOUR-WORKSTATION diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml new file mode 100644 index 0000000..0056fe0 --- /dev/null +++ b/secrets/secrets.yaml @@ -0,0 +1,54 @@ +# ============================================================================= +# Homey secrets — managed by sops-nix +# +# THIS FILE MUST BE ENCRYPTED WITH SOPS BEFORE COMMITTING. +# It is shown here as a plaintext template so you know what to fill in. +# +# Workflow: +# 1. Complete the .sops.yaml age key setup. +# 2. Fill in the values below. +# 3. Run: sops -e -i secrets/secrets.yaml +# This encrypts the file in-place. The encrypted version is safe to commit. +# 4. To edit later: sops secrets/secrets.yaml +# +# Ports from old deployment: +# - openldap/admin_password ← from k8s secret openldap-admin +# - openldap/config_password ← from k8s secret openldap-config +# - openldap/ro_password ← from k8s secret openldap-ro +# - gitea/admin_password ← from k8s secret gitea-admin-pass +# - nextcloud/admin_password ← from k8s secret nextcloud-admin-pass +# - nextcloud/postgres_password← from k8s secret nextcloud-postgres-pass +# The remaining secrets (authelia JWT, session key, encryption key, gitea +# LFS/OAuth2/internal tokens) are regenerated fresh — see notes below. +# ============================================================================= + +# --- OpenLDAP --- +openldap/admin_password: "REPLACE-WITH-OLD-VALUE" +openldap/config_password: "REPLACE-WITH-OLD-VALUE" +openldap/ro_password: "REPLACE-WITH-OLD-VALUE" + +# --- Authelia (regenerated fresh — these are random strings) --- +authelia/jwt_secret: "GENERATE-random-64-chars" +authelia/session_secret: "GENERATE-random-64-chars" +authelia/storage_encryption_key: "GENERATE-random-64-chars" + +# --- Gitea --- +gitea/admin_password: "REPLACE-WITH-OLD-VALUE" +# These three are regenerated — gitea will re-derive on first start: +gitea/lfs_jwt_secret: "GENERATE-random-43-chars-base64url" +gitea/oauth2_jwt_secret: "GENERATE-random-43-chars-base64url" +gitea/internal_token: "GENERATE-random-100-alphanum" + +# --- Nextcloud --- +nextcloud/admin_password: "REPLACE-WITH-OLD-VALUE" +nextcloud/postgres_password: "REPLACE-WITH-OLD-VALUE" + +# --- Cloudflare (DNS-01 ACME + tunnel) --- +cloudflare/api_token: "REPLACE-WITH-CF-DNS-EDIT-TOKEN" +cloudflare/tunnel_token: "REPLACE-WITH-CF-TUNNEL-TOKEN" + +# --- Restic backup --- +restic/password: "GENERATE-random-passphrase" +# Repository destination — e.g. "sftp:user@nas:/backups/homey" +# or "b2:bucketname:homey" for Backblaze B2 +# Set the actual repo URL in modules/backup.nix or override per-host. diff --git a/templates/_definitions.yaml b/templates/_definitions.yaml deleted file mode 100644 index 8aadb54..0000000 --- a/templates/_definitions.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -{{- define "homey.lookuporgensecret" -}} -{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace .secretname ) | default dict -}} -{{- $secretData := (get $secretObj "data") | default dict -}} -{{- $ret := (get $secretData "password" | b64dec ) | default (randAlphaNum 32 ) -}} -{{ $ret -}} -{{- end -}} ---- -{{- define "homey.randomsecret"}} -apiVersion: v1 -kind: Secret -metadata: - name: {{ (replace "\"" "" .secretname ) }} -type: Opaque -data: - password: {{ .secretval | b64enc | quote }} -{{- end }} ---- -{{- define "homey.randHex"}} - {{- $result := "" }} - {{- range $i := until . }} - {{- $rand_hex_char := mod (randNumeric 4 | atoi) 16 | printf "%x" }} - {{- $result = print $result $rand_hex_char }} - {{- end }} - {{- $result }} -{{- end -}} ---- diff --git a/templates/auth.yaml b/templates/auth.yaml deleted file mode 100644 index 84e37fc..0000000 --- a/templates/auth.yaml +++ /dev/null @@ -1,668 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: ldap-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 100Mi - storageClassName: longhorn ---- -{{- $_ := set $ "homey_openldap_admin" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-admin") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-admin" "secretval" .homey_openldap_admin) $) }} -# --- -{{- $_ := set $ "homey_openldap_config" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-config") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-config" "secretval" .homey_openldap_config) $) }} -# --- -{{- $_ := set $ "homey_openldap_ro" (include "homey.lookuporgensecret" (merge (dict "secretname" "openldap-ro") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "openldap-ro" "secretval" .homey_openldap_ro) $) }} ---- -{{- $_ := set $ "homey_authelia_jwt" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-jwt") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-jwt" "secretval" .homey_authelia_jwt) $) }} ---- -{{- $_ := set $ "homey_authelia_session" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-session") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-session" "secretval" .homey_authelia_session) $) }} ---- -{{- $_ := set $ "homey_authelia_encryption_key" (include "homey.lookuporgensecret" (merge (dict "secretname" "authelia-encryption-key") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "authelia-encryption-key" "secretval" .homey_authelia_encryption_key) $) }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: openldap - labels: - app.kubernetes.io/name: openldap -spec: - selector: - matchLabels: - app.kubernetes.io/name: openldap - replicas: 1 - template: - metadata: - labels: - app.kubernetes.io/name: openldap - spec: - # securityContext: - # fsGroup: 0 - containers: - - name: openldap - image: osixia/openldap - env: - - name: LDAP_ORGANISATION - value: {{ .Values.homey.organization }} - - name: LDAP_DOMAIN - value: {{ .Values.homey.url | quote}} - - name: LDAP_ADMIN_USERNAME - value: "admin" - - name: LDAP_READONLY_USER - value: "true" - - name: LDAP_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - key: password - name: openldap-admin - - name: LDAP_CONFIG_PASSWORD - valueFrom: - secretKeyRef: - key: password - name: openldap-config - - name: LDAP_READONLY_USER_PASSWORD - valueFrom: - secretKeyRef: - key: password - name: openldap-ro - ports: - - name: tcp-ldap - containerPort: 389 - - name: ssl-ldap - containerPort: 636 - volumeMounts: - - mountPath: /etc/ldap/slapd.d - subPath: openldap/etc/ldap/slapd.d - name: openldap-volume - - mountPath: /var/lib/ldap - subPath: openldap/var/lib/ldap - name: openldap-volume - volumes: - - name: openldap-volume - persistentVolumeClaim: - claimName: ldap-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: openldap - labels: - app.kubernetes.io/name: openldap -spec: - type: ClusterIP - ports: - - name: tcp-ldap - port: 389 - targetPort: tcp-ldap - - name: ssl-ldap - port: 636 - targetPort: ssl-ldap - selector: - app.kubernetes.io/name: openldap ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: authelia-conf -data: - configuration.yml: |- -{{ tpl (.Files.Get "files/authelia-config.yaml" | indent 4) . }} ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: authelia-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 100Mi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authelia - labels: - app.kubernetes.io/name: authelia -spec: - selector: - matchLabels: - app.kubernetes.io/name: authelia - replicas: 1 - template: - metadata: - labels: - app.kubernetes.io/name: authelia - spec: - enableServiceLinks: false - containers: - - name: authelia - image: authelia/authelia - imagePullPolicy: "IfNotPresent" - env: - - name: TZ - value: "Jerusalem/Israel" - ports: - - name: tcp - containerPort: 9091 - volumeMounts: - - mountPath: /config/configuration.yml - name: authelia-conf - subPath: configuration.yml - readOnly: true - - mountPath: /config - subPath: authelia/config - name: authelia-volume - volumes: - - name: authelia-conf - configMap: - name: authelia-conf - items: - - key: configuration.yml - path: configuration.yml - - name: authelia-volume - persistentVolumeClaim: - claimName: authelia-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: authelia - labels: - app.kubernetes.io/name: authelia -spec: - type: ClusterIP - ports: - - name: tcp - port: 9091 - targetPort: tcp - selector: - app.kubernetes.io/name: authelia ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: authelia -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - auth.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: auth.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: authelia - port: - number: 9091 ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: gitea-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 5Gi - storageClassName: longhorn ---- -{{- $_ := set $ "homey_gitea_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-admin-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-admin-pass" "secretval" .homey_gitea_admin_pass) $) }} ---- -{{- $_ := set $ "homey_gitea_lfs_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-lfs-jwt-secret") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-lfs-jwt-secret" "secretval" .homey_gitea_lfs_jwt_secret) $) }} ---- -{{- $_ := set $ "homey_gitea_oauth2_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret" "secretval" .homey_gitea_oauth2_jwt_secret) $) }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: gitea-random-internal-token - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: -{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "gitea-random-internal-token") | default dict -}} -{{- $secretData := (get $secretObj "data") | default dict -}} -{{- $pass := (get $secretData "password") | default (randAlphaNum 100 | b64enc) -}} -{{- $_ := set $ "homey_gitea_random_internal_token" ($pass | b64dec) }} - password: {{ $pass | quote }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: gitea-conf -data: - app.ini: |- -{{ tpl (.Files.Get "files/gitea-app.ini" | indent 4) . }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: gitea -spec: - replicas: 1 - selector: - matchLabels: - app: gitea - template: - metadata: - labels: - app: gitea - spec: - containers: - - name: gitea - image: gitea/gitea:latest - ports: - - containerPort: 3000 - name: http - volumeMounts: - - name: gitea-persistent-storage - mountPath: /data - subPath: gitea/gitea/data - - name: gitea-conf - mountPath: /data/gitea/conf/app.ini - subPath: app.ini - readOnly: true - # startProbe: - # httpGet: - # path: / - # port: 3000 - # initialDelaySeconds: 15 - # lifecycle: - # postStart: - # exec: - # {{- $gitea_cmd := (printf "gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host ldap --port 389 --user-search-base ou=users,%s --user-filter \\\"(&(objectClass=inetOrgPerson)(|(uid=%[1]s)(mail=kk[1]s)))\\\" --email-attribute mail --bind-dn=\\\"cn=readonly,%s\\\" --bind-password=\\\"%s\\\"" ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) (.homey_openldap_ro | replace "\"" ""))}} - # command: ["/bin/sh", "-c", "{{$gitea_cmd}}"] - volumes: - - name: gitea-persistent-storage - persistentVolumeClaim: - claimName: gitea-pvc - - name: gitea-conf - configMap: - name: gitea-conf - items: - - key: app.ini - path: app.ini ---- -apiVersion: v1 -kind: Service -metadata: - name: gitea-svc -spec: - selector: - app: gitea - ports: - - name: http-port - protocol: TCP - port: 3000 - targetPort: http - selector: - app: gitea ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: gitea-ingress -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - git.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: git.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: gitea-svc - port: - number: 3000 ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: nextcloud-postgres-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 5Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: nextcloud-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Ti - storageClassName: longhorn ---- -apiVersion: v1 -kind: Secret -metadata: - name: nextcloud-postgres-pass - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: - {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "nextcloud-postgres-pass") | default dict }} - {{- $secretData := (get $secretObj "data") | default dict }} - {{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) }} - password: {{ $pass | quote }} ---- -{{- $_ := set $ "homey_nextcloud_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "nextcloud-postgres-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "nextcloud-postgres-pass" "secretval" .homey_nextcloud_postgres_pass) $) }} ---- -{{- $_ := set $ "homey_nextcloud_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "nextcloud-admin-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "nextcloud-admin-pass" "secretval" .homey_nextcloud_admin_pass) $) }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: nextcloud-postgres-config - labels: - app: nextcloud-postgres -data: - POSTGRES_DB: nextcloud_db - POSTGRES_USER: postgres ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nextcloud-postgres - labels: - app: nextcloud-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: nextcloud-postgres - template: - metadata: - labels: - app: nextcloud-postgres - name: nextcloud-postgres - spec: - containers: - - name: nextcloud-postgres - image: postgres - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - envFrom: - - configMapRef: - name: nextcloud-postgres-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: nextcloud-postgres-pass - key: password - volumeMounts: - - mountPath: /var/lib/postgresql/data - subPath: nextcloud/db - name: nextcloud-postgredb - volumes: - - name: nextcloud-postgredb - persistentVolumeClaim: - claimName: nextcloud-postgres-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: nextcloud-postgres - labels: - app: nextcloud-postgres -spec: - ports: - - port: 5432 - selector: - app: nextcloud-postgres ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: nextcloud-configmap - labels: - app: nextcloud -data: - POSTGRES_HOST: nextcloud-postgres - OVERWRITEPROTOCOL: https - NEXTCLOUD_ADMIN_USER: admin - NEXTCLOUD_TRUSTED_DOMAINS: nextcloud.{{ .Values.homey.url }} nextcloud.admin.home ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nextcloud - labels: - app: nextcloud -spec: - replicas: 1 - selector: - matchLabels: - app: nextcloud - template: - metadata: - labels: - app: nextcloud - name: nextcloud - spec: - containers: - - name: nextcloud - image: nextcloud - imagePullPolicy: Always - volumeMounts: - - name: nextcloud-volume - mountPath: "/var/www/html" - subPath: html - envFrom: - - configMapRef: - name: nextcloud-postgres-config - - configMapRef: - name: nextcloud-configmap - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: nextcloud-postgres-pass - key: password - - name: NEXTCLOUD_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: nextcloud-admin-pass - key: password - volumes: - - name: nextcloud-volume - persistentVolumeClaim: - claimName: nextcloud-data-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: nextcloud -spec: - selector: - app: nextcloud - ports: - - port: 80 - targetPort: 80 - name: nextcloud ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nextcloud-ingress - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: 5g - nginx.ingress.kubernetes.io/server-snippet: | - # Make a regex exception for `/.well-known` so that clients can still - # access it despite the existence of the regex rule - # `location ~ /(\.|autotest|...)` which would otherwise handle requests - # for `/.well-known`. - location = /.well-known/carddav { return 301 https://nextcloud.{{ .Values.homey.url }}/remote.php/dav/; } - location = /.well-known/caldav { return 301 https://nextcloud.{{ .Values.homey.url }}/remote.php/dav/; } -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - nextcloud.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: nextcloud.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: nextcloud - port: - number: 80 ---- -#START RADICALE -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: radicale-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: radicale-conf - labels: - app: radicale -data: - config: |- -{{ tpl (.Files.Get "files/radicale-configmap.ini" | indent 4) . }} ---- -{{- $_ := set $ "homey_radicale_basic_auth" (include "homey.lookuporgensecret" (merge (dict "secretname" "radicale-basic-auth") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "radicale-basic-auth" "secretval" .homey_radicale_basic_auth) $) }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: radicale - labels: - app.kubernetes.io/name: radicale -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: radicale - template: - metadata: - labels: - app.kubernetes.io/name: radicale - spec: - containers: - - name: radicale - image: tomsquest/docker-radicale - imagePullPolicy: IfNotPresent - ports: - - name: dav - containerPort: 5232 - protocol: TCP - volumeMounts: - - name: collections - mountPath: /data/collections - - name: config - mountPath: /config/config - subPath: config - readOnly: true - restartPolicy: Always - volumes: - - name: collections - persistentVolumeClaim: - claimName: radicale-pvc - - name: config - configMap: - name: radicale-conf ---- -apiVersion: v1 -kind: Service -metadata: - name: radicale - labels: - app.kubernetes.io/name: radicale -spec: - type: ClusterIP - ports: - - name: dav - port: 5232 - targetPort: dav - selector: - app.kubernetes.io/name: radicale ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: radicale-dav - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Remote-User $user; - proxy_set_header X-Remote-Fullname $name; - proxy_set_header X-Remote-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: radicale - port: - number: 5232 diff --git a/templates/media.yaml b/templates/media.yaml deleted file mode 100644 index 79948a0..0000000 --- a/templates/media.yaml +++ /dev/null @@ -1,211 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: jellyfin-config-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: jellyfin-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 700Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: transmission-config-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jellyfin -spec: - replicas: 1 - selector: - matchLabels: - app: jellyfin - template: - metadata: - labels: - app: jellyfin - spec: - containers: - - name: jellyfin - image: jellyfin/jellyfin:10.11.6 - ports: - - containerPort: 8096 - name: http - env: - - name: PGUID - value: "1000" - - name: PUID - value: "1000" - - name: JELLYFIN_PublishedServerUrl - value: jellyfin.{{ .Values.homey.url }} - imagePullPolicy: "Always" - volumeMounts: - - name: jellyfin-volume-config - mountPath: "/config" - subPath: jellyfin/config - - name: jellyfin-volume-data - mountPath: "/data/movies" - subPath: downloads/movies - - name: jellyfin-volume-data - mountPath: "/data/tvshows" - subPath: downloads/tvshows - volumes: - - name: jellyfin-volume-config - persistentVolumeClaim: - claimName: jellyfin-config-pvc - - name: jellyfin-volume-data - persistentVolumeClaim: - claimName: jellyfin-data-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: jellyfin-web -spec: - selector: - app: jellyfin - ports: - - port: 80 - targetPort: 8096 - name: jellyfin-web ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: jellyfin-ingress -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - jellyfin.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: jellyfin.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jellyfin-web - port: - number: 80 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: transmission - labels: - app: transmission -spec: - replicas: 1 - selector: - matchLabels: - app: transmission - template: - metadata: - labels: - app: transmission - name: transmission - spec: - containers: - - name: transmission - image: linuxserver/transmission - imagePullPolicy: Always - volumeMounts: - - name: transmission-volume-config - mountPath: "/config" - subPath: transmission/config - - name: transmission-volume-data - mountPath: "/downloads/movies" - subPath: downloads/movies - - name: transmission-volume-data - mountPath: "/downloads/tvshows" - subPath: downloads/tvshows - - name: transmission-volume-data - mountPath: "/downloads/general" - subPath: downloads/general - - name: transmission-volume-data - mountPath: "/downloads/complete" - subPath: downloads/complete - volumes: - - name: transmission-volume-config - persistentVolumeClaim: - claimName: transmission-config-pvc - - name: transmission-volume-data - persistentVolumeClaim: - claimName: jellyfin-data-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: transmission-web -spec: - selector: - app: transmission - ports: - - port: 80 - targetPort: 9091 - name: transmission-web ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: torrent - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Webauth-User $user; - proxy_set_header X-Webauth-Fullname $name; - proxy_set_header X-Webauth-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - torrent.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: torrent.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: transmission-web - port: - number: 80 diff --git a/templates/phpldapadmin.yaml b/templates/phpldapadmin.yaml deleted file mode 100644 index 108983f..0000000 --- a/templates/phpldapadmin.yaml +++ /dev/null @@ -1,80 +0,0 @@ ---- -#_PHPADMIN________ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: phpldapadmin - labels: - app: phpldapadmin -spec: - replicas: 1 - selector: - matchLabels: - app: phpldapadmin - template: - metadata: - labels: - app: phpldapadmin - spec: - containers: - - env: - - name: PHPLDAPADMIN_HTTPS - value: "false" - - name: PHPLDAPADMIN_LDAP_HOSTS - value: ldap://openldap:389 - image: osixia/phpldapadmin - name: phpldapadmin - ports: - - containerPort: 80 - name: http - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: phpldapadmin -spec: - ports: - - port: 80 - targetPort: 80 - name: http - selector: - app: phpldapadmin ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: phpldapadmin - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Webauth-User $user; - proxy_set_header X-Webauth-Fullname $name; - proxy_set_header X-Webauth-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - ldapadmin.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: ldapadmin.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: phpldapadmin - port: - number: 80 diff --git a/unused/auth-templates.yaml b/unused/auth-templates.yaml deleted file mode 100644 index a67a65b..0000000 --- a/unused/auth-templates.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -{{- define "homey.auth.ingress.annotations" }} - # nginx.ingress.kubernetes.io/auth-signin: "https://auth.zakobar.com" - nginx.ingress.kubernetes.io/auth-url: "http://ldap-auth-internal.{{ .Release.Namespace }}.svc.cluster.local:80" - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Email - nginx.ingress.kubernetes.io/location-snippets: |- - auth_request /auth - nginx.ingress.kubernetes.io/configuration-snippet: |- - location /auth { - # proxy_pass http://ldap-auth-internal; - proxy_pass_request_body off; - #THIS NEEDS TO BE SET BY ACTUAL SOMETHING LOGIN SHIT - # proxy_set_header X-Target http://ldap-auth-internal.{{ .Release.Namespace }}.svc.cluster.local:80; - proxy_set_header X-Ldap-URL "ldap://openldap"; - proxy_set_header X-Ldap-BaseDN "ou=users,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}"; - proxy_set_header X-Ldap-BindDN "cn=readonly,{{ .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim }}"; - proxy_set_header X-Ldap-BindPass {{ (get (get (lookup "v1" "Secret" .Release.Namespace "openldap-ro") "data") "password") | b64dec | quote}}; - proxy_set_header X-CookieName "homey.auth.cookie"; - proxy_set_header Cookie $cookie_homey.auth.cookie; - proxy_set_header X-Remote-User $remote_user; - proxy_set_header X-Forwarded-Method $request_method; - proxy_set_header X-Ldap-Template "(uid=%(username)s)"; - } -{{- end }} diff --git a/unused/baikal.yaml b/unused/baikal.yaml deleted file mode 100644 index 6f1366c..0000000 --- a/unused/baikal.yaml +++ /dev/null @@ -1,117 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: baikal-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: baikal-config-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: baikal - labels: - app: baikal -spec: - replicas: 1 - selector: - matchLabels: - app: baikal - template: - metadata: - labels: - app: baikal - spec: - containers: - - name: baikal - image: ckulka/baikal - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - name: dav - volumeMounts: - - name: config - mountPath: /var/www/baikal/config - subPath: config - - name: data - mountPath: /var/www/baikal/Specific - subPath: Specific - restartPolicy: Always - volumes: - - name: data - persistentVolumeClaim: - claimName: baikal-data-pvc - - name: config - persistentVolumeClaim: - claimName: baikal-config-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: baikal -spec: - selector: - app: baikal - ports: - - name: dav - protocol: TCP - port: 80 - targetPort: 80 - selector: - app: baikal ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: baikal - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Remote-User $user; - proxy_set_header X-Remote-Fullname $name; - proxy_set_header X-Remote-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: baikal - port: - number: 80 ---- diff --git a/unused/dav.yaml b/unused/dav.yaml deleted file mode 100644 index 566a3c3..0000000 --- a/unused/dav.yaml +++ /dev/null @@ -1,71 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: baikal - labels: - app: baikal -spec: - replicas: 1 - selector: - matchLabels: - app: baikal - template: - metadata: - labels: - app: baikal - spec: - containers: - - name: baikal - image: ckulka/baikal - ports: - - name: dav - containerPort: 80 - protocol: TCP - volumeMounts: - - name: baikal-volume - mountPath: /var/www/baikal/Specific - subPath: baikal/data - - name: baikal-volume - mountPath: /var/www/baikal/config - subPath: baikal/config - restartPolicy: Always - volumes: - - name: baikal-volume - persistentVolumeClaim: - claimName: homey-pvc-longhorn ---- -apiVersion: v1 -kind: Service -metadata: - name: baikal -spec: - ports: - - name: dav - targetPort: 80 - port: 80 - selector: - app: baikal ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: baikal -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: baikal - port: - name: dav ---- diff --git a/unused/davical.yaml b/unused/davical.yaml deleted file mode 100644 index e0affd2..0000000 --- a/unused/davical.yaml +++ /dev/null @@ -1,213 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: davical-postgres-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn - ---- -{{- $_ := set $ "homey_davical_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "davical-postgres-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "davical-postgres-pass" "secretval" .homey_davical_postgres_pass) $) }} ---- -# apiVersion: extensions/v1beta1 -apiVersion: v1 -kind: ConfigMap -metadata: - name: davical-postgres-config - labels: - app: davical-postgres -data: - POSTGRES_DB: postgres - POSTGRES_USER: postgres ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: davical-postgres - labels: - app: davical-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: davical-postgres - template: - metadata: - labels: - app: davical-postgres - name: davical-postgres - spec: - containers: - - name: davical-postgres - image: postgres - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - envFrom: - - configMapRef: - name: davical-postgres-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: davical-postgres-pass - key: password - volumeMounts: - - mountPath: /var/lib/postgresql/data - subPath: data - name: davical-postgredb - volumes: - - name: davical-postgredb - persistentVolumeClaim: - claimName: davical-postgres-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: davical-postgres - labels: - app: davical-postgres -spec: - ports: - - port: 5432 - selector: - app: davical-postgres ---- -{{- $_ := set $ "homey_davical_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "davical-admin-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "davical-admin-pass" "secretval" .homey_davical_admin_pass) $) }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: davical-conf -data: - config.php: |- -{{ tpl (.Files.Get "files/davical-config.php" | indent 4) . }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: davical - labels: - app: davical -spec: - replicas: 1 - selector: - matchLabels: - app: davical - template: - metadata: - labels: - app: davical - spec: - containers: - - name: davical - image: anerisgreat/davical-multiarch-docker:latest - imagePullPolicy: "Always" - ports: - - containerPort: 80 - name: dav - env: - - name: PGHOST - value: "davical-postgres" - - name: PGUSER - value: "postgres" - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: davical-postgres-pass - key: password - - name: PGDATABASE - value: "davical" - - name: PGPORT - value: "5432" - - name: HOST_NAME - value: - "dav.{{ .Values.homey.url }}" - - name: DAVICAL_ADMIN_PASS - valueFrom: - secretKeyRef: - name: davical-admin-pass - key: password - - name: ROOT_PGUSER - value: "postgres" - - name: ROOT_PGPASSWORD - valueFrom: - secretKeyRef: - name: davical-postgres-pass - key: password - - name: RUN_MIGRATIONS_AT_STARTUP - value: "true" - volumeMounts: - - name: davical-conf - mountPath: /etc/davical/config.php - subPath: config.php - readOnly: true - volumes: - - name: davical-conf - configMap: - name: davical-conf - items: - - key: config.php - path: config.php ---- -apiVersion: v1 -kind: Service -metadata: - name: davical -spec: - selector: - app: davical - ports: - - name: dav - protocol: TCP - port: 80 - targetPort: 80 - selector: - app: davical ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: davical - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header Remote-User $user; - proxy_set_header Remote-Fullname $name; - proxy_set_header Remote-Email $email; - proxy_set_header Redirect-Remote-User $user; - proxy_set_header Redirect-Remote-Fullname $name; - proxy_set_header Redirect-Remote-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: davical - port: - number: 80 diff --git a/unused/gitea.yaml b/unused/gitea.yaml deleted file mode 100644 index bdd658f..0000000 --- a/unused/gitea.yaml +++ /dev/null @@ -1,131 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: gitea-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 5Gi - storageClassName: longhorn ---- -{{- $_ := set $ "homey_gitea_admin_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-admin-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-admin-pass" "secretval" .homey_gitea_admin_pass) $) }} ---- -{{- $_ := set $ "homey_gitea_lfs_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-lfs-jwt-secret") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-lfs-jwt-secret" "secretval" .homey_gitea_lfs_jwt_secret) $) }} ---- -{{- $_ := set $ "homey_gitea_oauth2_jwt_secret" (include "homey.lookuporgensecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "gitea-oauth2-jwt-secret" "secretval" .homey_gitea_oauth2_jwt_secret) $) }} - ---- -apiVersion: v1 -kind: Secret -metadata: - name: gitea-random-internal-token - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: -{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "gitea-random-internal-token") | default dict -}} -{{- $secretData := (get $secretObj "data") | default dict -}} -{{- $pass := (get $secretData "password") | default (randAlphaNum 100 | b64enc) -}} -{{- $_ := set $ "homey_gitea_random_internal_token" ($pass | b64dec) }} - password: {{ $pass | quote }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: gitea-conf -data: - app.ini: |- -{{ tpl (.Files.Get "files/gitea-app.ini" | indent 4) . }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: gitea -spec: - replicas: 1 - selector: - matchLabels: - app: gitea - template: - metadata: - labels: - app: gitea - spec: - containers: - - name: gitea - image: gitea/gitea:latest - ports: - - containerPort: 3000 - name: http - volumeMounts: - - name: gitea-persistent-storage - mountPath: /data - subPath: gitea/gitea/data - - name: gitea-conf - mountPath: /data/gitea/conf/app.ini - subPath: app.ini - readOnly: true - # startProbe: - # httpGet: - # path: / - # port: 3000 - # initialDelaySeconds: 15 - # lifecycle: - # postStart: - # exec: - # {{- set $gitea-cmd (printf "gitea admin auth add-ldap --name ldap --security-protocol unencrypted --host ldap --port 389 --user-search-base ou=users,%s --user-filter \\\"(&(objectClass=inetOrgPerson)(|(uid=\%[1]s)(mail=\%[1]s)))\\\" --email-attribute mail --bind-dn=\\\"cn=readonly,%s\\\" --bind-password=\\\"%s\\\"" ( .Values.homey.url | replace "." ",dc=" | printf "dc=%s " | trim) () (.homey_openldap_ro | replace "\"" ""))}} - # command: ["/bin/sh", "-c", "{{cmd}}"] - volumes: - - name: gitea-persistent-storage - persistentVolumeClaim: - claimName: gitea-pvc - - name: gitea-conf - configMap: - name: gitea-conf - items: - - key: app.ini - path: app.ini ---- -apiVersion: v1 -kind: Service -metadata: - name: gitea-svc -spec: - selector: - app: gitea - ports: - - name: http-port - protocol: TCP - port: 3000 - targetPort: http - selector: - app: gitea ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: gitea-ingress -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - git.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: git.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: gitea-svc - port: - number: 3000 ---- diff --git a/unused/jellyfin.yaml b/unused/jellyfin.yaml deleted file mode 100644 index 53d231a..0000000 --- a/unused/jellyfin.yaml +++ /dev/null @@ -1,92 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: jellyfin-config-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 100Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: jellyfin-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 700Gi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: jellyfin -spec: - replicas: 1 - selector: - matchLabels: - app: jellyfin - template: - metadata: - labels: - app: jellyfin - spec: - containers: - - name: jellyfin - image: docker.io/jellyfin/jellyfin - volumeMounts: - - name: jellyfin-volume-config - mountPath: "/config" - subPath: jellyfin/config - - name: jellyfin-volume-data - mountPath: "/data/movies" - subPath: downloads/movies - - name: jellyfin-volume-data - mountPath: "/data/tvshows" - subPath: downloads/tvshows - - env: - - name: JELLYFIN_PublishedServerUrl - value: jellyfin.{{ .Values.homey.url }} - volumes: - - name: jellyfin-volume-config - persistentVolumeClaim: - claimName: jellyfin-config-pvc - - name: jellyfin-volume-data - persistentVolumeClaim: - claimName: jellyfin-data-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: jellyfin-web - namespace: homecenter -spec: - selector: - app: jellyfin - ports: - - port: 80 - targetPort: 8096 - name: jellyfin-web ---- -ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - jellyfin.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: jellyfin.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: jellyfin-web - port: - number: 80 ---- diff --git a/unused/ldap-auth.yaml b/unused/ldap-auth.yaml deleted file mode 100644 index e9adf30..0000000 --- a/unused/ldap-auth.yaml +++ /dev/null @@ -1,70 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ldap-auth - labels: - app: ldap-auth -spec: - replicas: 1 - selector: - matchLabels: - app: ldap-auth - template: - metadata: - labels: - app: ldap-auth - name: ldap-auth - spec: - containers: - - name: ldap-auth - image: linuxserver/ldap-auth - imagePullPolicy: Always ---- -#https://stackoverflow.com/questions/51149921/how-to-authenticate-nginx-with-ldap -apiVersion: v1 -kind: Service -metadata: - name: ldap-auth -spec: - selector: - app: ldap-auth - ports: - - port: 80 - targetPort: 9000 ---- -apiVersion: v1 -kind: Service -metadata: - name: ldap-auth-internal -spec: - selector: - app: ldap-auth - ports: - - port: 80 - targetPort: 8888 ---- -# apiVersion: networking.k8s.io/v1 -# kind: Ingress -# metadata: -# name: ldap-auth-ingress -# annotations: -# spec: -# ingressClassName: {{ .Values.homey.ingress_class }} -# tls: -# - hosts: -# - auth.{{ .Values.homey.url }} -# secretName: {{ .Values.homey.certname }} -# rules: -# - host: auth.{{ .Values.homey.url }} -# http: -# paths: -# - path: / -# pathType: Prefix -# backend: -# service: -# name: ldap-auth -# port: -# number: 80 -# --- - diff --git a/unused/nextcloud.yaml b/unused/nextcloud.yaml deleted file mode 100644 index d35f048..0000000 --- a/unused/nextcloud.yaml +++ /dev/null @@ -1,206 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: nextcloud-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 30Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: nextcloud-postgres-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 5Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: nextcloud-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 30Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: Secret -metadata: - name: nextcloud-postgres-pass - annotations: - "helm.sh/resource-policy": "keep" -type: Opaque -data: - {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "nextcloud-postgres-pass") | default dict }} - {{- $secretData := (get $secretObj "data") | default dict }} - {{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) }} - password: {{ $pass | quote }} ---- -# apiVersion: extensions/v1beta1 -apiVersion: v1 -kind: ConfigMap -metadata: - name: nextcloud-postgres-config - labels: - app: nextcloud-postgres -data: - POSTGRES_DB: nextcloud_db - POSTGRES_USER: postgres ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nextcloud-postgres - labels: - app: nextcloud-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: nextcloud-postgres - template: - metadata: - labels: - app: nextcloud-postgres - name: nextcloud-postgres - spec: - containers: - - name: nextcloud-postgres - image: postgres - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - envFrom: - - configMapRef: - name: nextcloud-postgres-config - env: - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: nextcloud-postgres-pass - key: password - volumeMounts: - - mountPath: /var/lib/postgresql/data - subPath: nextcloud/db - name: nextcloud-postgredb - volumes: - - name: nextcloud-postgredb - persistentVolumeClaim: - claimName: nextcloud-postgres-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: nextcloud-postgres - labels: - app: nextcloud-postgres -spec: - ports: - - port: 5432 - selector: - app: nextcloud-postgres ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nextcloud - labels: - app: nextcloud -spec: - replicas: 1 - selector: - matchLabels: - app: nextcloud - template: - metadata: - labels: - app: nextcloud - name: nextcloud - spec: - containers: - - name: nextcloud - image: nextcloud - imagePullPolicy: Always - volumeMounts: - - name: nextcloud-volume - mountPath: "/var/www/html" - subPath: nextcloud/html - - name: nextcloud-media - mountPath: "/var/www/html/data" - subPath: nextcloud/html/data - envFrom: - - configMapRef: - name: nextcloud-postgres-config - env: - - name: POSTGRES_HOST - value: "nextcloud-postgres" - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: nextcloud-postgres-pass - key: password - - name: OVERWRITEPROTOCOL - value: "https" - volumes: - - name: nextcloud-volume - persistentVolumeClaim: - claimName: nextcloud-pvc - - name: nextcloud-media - persistentVolumeClaim: - claimName: nextcloud-data-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: nextcloud -spec: - selector: - app: nextcloud - ports: - - port: 80 - targetPort: 80 - name: nextcloud ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nextcloud-ingress - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: 5g - nginx.ingress.kubernetes.io/server-snippet: | - # Make a regex exception for `/.well-known` so that clients can still - # access it despite the existence of the regex rule - # `location ~ /(\.|autotest|...)` which would otherwise handle requests - # for `/.well-known`. - location = /.well-known/carddav { return 301 https://nextcloud.zakobar.com/remote.php/dav/; } - location = /.well-known/caldav { return 301 https://nextcloud.zakobar.com/remote.php/dav/; } -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - nextcloud.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: nextcloud.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: nextcloud - port: - number: 80 ---- diff --git a/unused/paperless.yaml b/unused/paperless.yaml deleted file mode 100644 index 281a77a..0000000 --- a/unused/paperless.yaml +++ /dev/null @@ -1,230 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: paperless-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 50Gi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: paperless-redis -spec: - replicas: 1 - selector: - matchLabels: - app: paperless-redis - template: - metadata: - labels: - app: paperless-redis - spec: - containers: - - name: paperless - image: redis - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 6379 - name: redis - volumeMounts: - - name: paperless-volume - mountPath: "/data" - subPath: paperless/redis-data - volumes: - - name: paperless-volume - persistentVolumeClaim: - claimName: paperless-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: paperless-redis -spec: - selector: - app: paperless-redis - ports: - - port: 80 - targetPort: 8000 - name: paperless-web - ---- -{{- $_ := set $ "homey_paperless_postgres_pass" (include "homey.lookuporgensecret" (merge (dict "secretname" "paperless-postgres-pass") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "paperless-postgres-pass" "secretval" .homey_paperless_postgres_pass) $) }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: paperless-postgres-config - labels: - app: paperless-postgres -data: - POSTGRES_DB: paperless - POSTGRES_USER: paperless - POSTGRES_PASSWORD: paperless ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: paperless-postgres - labels: - app: paperless-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: paperless-postgres - template: - metadata: - labels: - app: paperless-postgres - name: paperless-postgres - spec: - containers: - - name: paperless-postgres - image: postgres - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - envFrom: - - configMapRef: - name: paperless-postgres-config - volumeMounts: - - mountPath: /var/lib/postgresql/data - subPath: paperless/db - name: paperless-volume - volumes: - - name: paperless-volume - persistentVolumeClaim: - claimName: paperless-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: paperless-postgres - labels: - app: paperless-postgres -spec: - ports: - - port: 5432 - selector: - app: paperless-postgres ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: paperless -spec: - replicas: 1 - selector: - matchLabels: - app: paperless - template: - metadata: - labels: - app: paperless - spec: - containers: - - name: paperless - image: ghcr.io/paperless-ngx/paperless-ngx:latest - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 8000 - name: paperless-web - volumeMounts: - - name: paperless-volume - mountPath: "/usr/src/paperless/data" - subPath: paperless/data - - name: paperless-volume - mountPath: "/usr/src/paperless/media" - subPath: paperless/media - - name: paperless-volume - mountPath: "/usr/src/paperless/export" - subPath: paperless/export - - name: paperless-volume - mountPath: "/usr/src/paperless/consume" - subPath: paperless/consume - env: - - name: PAPERLESS_REDIS - value: redis://paperless-redis:6379 - - name: PAPERLESS_DBHOST - value: paperless-postgres - - name: PAPERLESS_DEBUG - value: "true" - - name: PAPERLESS_ENABLE_HTTP_REMOTE_USER - value: "true" - - name: PAPERLESS_ENABLE_HTTP_REMOTE_USER_API - value: "true" - - name: PAPERLESS_DISABLE_REGULAR_LOGIN - value: "true" - - name: PAPERLESS_LOGOUT_REDIRECT_URL - value: "https://auth.{{ .Values.homey.url }}/logout" - - name: PAPERLESS_URL - value: "https://paperless.{{ .Values.homey.url }}" - - name: PAPERLESS_DBPASSWORD - valueFrom: - secretKeyRef: - name: paperless-postgres-pass - key: password - volumes: - - name: paperless-volume - persistentVolumeClaim: - claimName: paperless-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: paperless-web - labels: - app: paperless-web -spec: - selector: - app: paperless - ports: - - port: 80 - targetPort: 8000 - name: paperless-web ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: paperless-ingress - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header REMOTE_USER $remote_user; - proxy_set_header REMOTE_EMAIL $email; - proxy_set_header REMOTE_NAME $name; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - paperless.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: paperless.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: paperless-web - port: - number: 80 ---- diff --git a/unused/radicale.yaml b/unused/radicale.yaml deleted file mode 100644 index ca5b27c..0000000 --- a/unused/radicale.yaml +++ /dev/null @@ -1,122 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: radicale-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: radicale-conf - labels: - app: radicale -data: - config: |- -{{ tpl (.Files.Get "files/radicale-configmap.ini" | indent 4) . }} ---- -{{- $_ := set $ "homey_radicale_basic_auth" (include "homey.lookuporgensecret" (merge (dict "secretname" "radicale-basic-auth") $))}} -{{ include "homey.randomsecret" (merge (dict "secretname" "radicale-basic-auth" "secretval" .homey_radicale_basic_auth) $) }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: radicale - labels: - app: radicale -spec: - replicas: 1 - selector: - matchLabels: - app: radicale - template: - metadata: - labels: - app: radicale - spec: - containers: - - name: radicale - image: tomsquest/docker-radicale - imagePullPolicy: IfNotPresent - ports: - - name: dav - containerPort: 5232 - protocol: TCP - volumeMounts: - - name: collections - mountPath: /data/collections - - name: config - mountPath: /config/config - subPath: config - readOnly: true - restartPolicy: Always - volumes: - - name: collections - persistentVolumeClaim: - claimName: radicale-pvc - - name: config - configMap: - name: radicale-conf ---- -apiVersion: v1 -kind: Service -metadata: - name: radicale - labels: - app.kubernetes.io/name: radicale -spec: - type: ClusterIP - ports: - - name: dav - port: 5232 - targetPort: 5232 - - name: http - port:80 - targetPort: 80 - selector: - app.kubernetes.io/name: radicale ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: radicale - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Remote-User $user; - proxy_set_header X-Remote-Fullname $name; - proxy_set_header X-Remote-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: radicale - port: - number: 5232 ---- diff --git a/unused/sabre.yaml b/unused/sabre.yaml deleted file mode 100644 index 19d45aa..0000000 --- a/unused/sabre.yaml +++ /dev/null @@ -1,118 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: baikal-data-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: baikal-config-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: baikal - labels: - app: baikal -spec: - replicas: 1 - selector: - matchLabels: - app: baikal - template: - metadata: - labels: - app: baikal - spec: - containers: - - name: baikal - image: ckulka/baikal-docker - imagePullPolicy: IfNotPresent - ports: - - name: dav - containerPort: 80 - protocol: TCP - volumeMounts: - - name: config - mountPath: /var/www/baikal/config - subPath: config - - name: data - mountPath: /var/www/baikal/Specific - subPath: Specific - restartPolicy: Always - volumes: - - name: data - persistentVolumeClaim: - claimName: baikal-data-pvc - - name: config - persistentVolumeClaim: - claimName: baikal-config-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: baikal - labels: - app.kubernetes.io/name: baikal -spec: - type: ClusterIP - ports: - - name: dav - port: 80 - targetPort: 80 - selector: - app.kubernetes.io/name: baikal ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: baikal - annotations: - kubernetes.io/ingress.allow-http: "false" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/auth-method: GET - nginx.ingress.kubernetes.io/auth-url: http://authelia.{{ .Release.Namespace }}.svc.cluster.local:9091/api/verify - nginx.ingress.kubernetes.io/auth-signin: https://auth.{{ .Values.homey.url }}?rm=$request_method - nginx.ingress.kubernetes.io/auth-response-headers: Remote-User,Remote-Name,Remote-Groups,Remote-Email - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Method $request_method; - auth_request_set $user $upstream_http_remote_user; - auth_request_set $groups $upstream_http_remote_groups; - auth_request_set $name $upstream_http_remote_name; - auth_request_set $email $upstream_http_remote_email; - proxy_set_header X-Remote-User $user; - proxy_set_header X-Remote-Fullname $name; - proxy_set_header X-Remote-Email $email; -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - dav.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: dav.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: baikal - port: - number: 80 ---- diff --git a/unused/sogo.yaml b/unused/sogo.yaml deleted file mode 100644 index e7162df..0000000 --- a/unused/sogo.yaml +++ /dev/null @@ -1,162 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: sogo-postgres-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi - storageClassName: longhorn ---- -apiVersion: v1 -kind: Secret -metadata: - name: sogo-db-pass -type: Opaque -data: - password: sogo ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sogo-postgres-config - labels: - app: sogo-postgres -data: - POSTGRES_DB: sogo - POSTGRES_USER: sogo - POSTGRES_PASSWORD: sogo ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sogo-postgres - labels: - app: sogo-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: sogo-postgres - template: - metadata: - labels: - app: sogo-postgres - name: sogo-postgres - spec: - containers: - - name: postgres - image: postgres - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - envFrom: - - configMapRef: - name: sogo-postgres-config - volumeMounts: - - mountPath: /var/lib/postgresql/data - subPath: sogo/db/data - name: sogo-postgresdb - volumes: - - name: sogo-postgresdb - persistentVolumeClaim: - claimName: sogo-postgres-pvc ---- -apiVersion: v1 -kind: Service -metadata: - name: sogo-postgres - labels: - app: sogo-postgres -spec: - ports: - - port: 5432 - selector: - app: sogo-postgres ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sogo-conf -data: - sogo.conf: |- -{{ tpl (.Files.Get "files/sogo.conf" | indent 4) . }} ---- -apiVersion: v1 -kind: Service -metadata: - name: sogo - labels: - app: sogo -spec: - ports: - - port: 80 - targetPort: 80 - selector: - app: sogo ---- - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sogo -spec: - # Stop old container before starting new one. - # No known upgrade policy know. Save to stop and start a new one. - strategy: - type: Recreate - rollingUpdate: null - selector: - matchLabels: - app: sogo - replicas: 1 - template: - metadata: - labels: - app: sogo - spec: - containers: - - name: sogo - image: mailcow/sogo:nightly-1.119 - resources: - requests: - cpu: 100m - memory: 400Mi - ports: - - containerPort: 80 - volumeMounts: - - mountPath: /etc/sogo/sogo.conf - name: sogo-conf - subPath: sogo.conf - readOnly: true - volumes: - - name: sogo-conf - configMap: - name: sogo-conf - optional: false ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: sogo-ingress -spec: - ingressClassName: {{ .Values.homey.ingress_class }} - tls: - - hosts: - - git.{{ .Values.homey.url }} - secretName: {{ .Values.homey.certname }} - rules: - - host: sogo.{{ .Values.homey.url }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: soo - port: - number: 80 - ---- diff --git a/values.yaml b/values.yaml deleted file mode 100644 index 4106407..0000000 --- a/values.yaml +++ /dev/null @@ -1,65 +0,0 @@ -replicaCount: 1 - -homeyNamespace: homey - -imagePullSecrets: [] -nameOverride: "homey-app" -fullnameOverride: "homey-chart" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "homey" - -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 80 - -resources: {} # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -homey: - organization: "Zakobar Home Server" - url: zakobar.com - ip: 192.168.1.100 - certname: zakobarcert - ingress_class: nginx -