Merge nixos-port: complete NixOS port of homey selfhosted stack
Replaces Helm/k8s deployment with flake-based NixOS config. All core services working: Caddy, Authelia, OpenLDAP, phpLDAPadmin, Gitea. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
charts
|
charts
|
||||||
*.lock
|
*.lock
|
||||||
.agent-shell
|
.agent-shell
|
||||||
|
result
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# 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:
|
||||||
|
- pgp:
|
||||||
|
- 076AA297579A0064
|
||||||
|
age:
|
||||||
|
- age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
|
||||||
@@ -1,255 +1,287 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
This is a Helm chart for deploying a self-hosted home environment (Homey) on Kubernetes.
|
Self-hosted home server configuration for a Raspberry Pi 4 (8 GB), managed
|
||||||
|
entirely through NixOS. Services run as podman containers under systemd.
|
||||||
|
Remote access is via Cloudflare Tunnel; local access goes through Caddy
|
||||||
|
with Let's Encrypt TLS (DNS-01, Cloudflare API).
|
||||||
|
|
||||||
## Project Overview
|
The original Kubernetes/Helm setup is preserved on the `main` branch.
|
||||||
|
This branch (`nixos-port`) is the active NixOS port.
|
||||||
|
|
||||||
- **Type**: Helm Chart (Kubernetes package manager)
|
---
|
||||||
- **Language**: YAML + Go template syntax (Helm templating)
|
|
||||||
- **Key Files**:
|
|
||||||
- `Chart.yaml` - Chart metadata
|
|
||||||
- `values.yaml` - Default configuration values
|
|
||||||
- `templates/` - Kubernetes manifest templates (auth.yaml, media.yaml, phpldapadmin.yaml, _definitions.yaml)
|
|
||||||
- `files/` - Configuration file templates (processed by Helm with `tpl` function)
|
|
||||||
|
|
||||||
## Build/Lint/Test Commands
|
## Project Structure
|
||||||
|
|
||||||
### Helm Validation
|
```
|
||||||
```bash
|
flake.nix # Entry point — defines all hosts
|
||||||
# Lint the Helm chart for errors
|
modules/
|
||||||
helm lint .
|
common.nix # Shared system config (nix, podman, sops, SSH)
|
||||||
|
storage.nix # External HD mount + per-service directory layout
|
||||||
# Template rendering (dry-run install)
|
caddy.nix # Caddy reverse proxy (DNS-01 ACME, forward_auth)
|
||||||
helm template test-release . --debug
|
cloudflared.nix # Cloudflare Tunnel for remote access
|
||||||
|
backup.nix # Restic daily backups (S3 primary + manual offload)
|
||||||
# Install/upgrade in cluster
|
services/
|
||||||
helm upgrade --install homey . -n homey
|
openldap.nix # OpenLDAP — central identity provider
|
||||||
|
authelia.nix # Authelia — SSO gateway
|
||||||
# Verify chart against Kubernetes API
|
gitea.nix # Gitea — Git server
|
||||||
helm kubeval .
|
nextcloud.nix # Nextcloud + PostgreSQL
|
||||||
|
phpldapadmin.nix # phpLDAPadmin — LDAP web UI
|
||||||
# Check schema validation of values.yaml
|
jellyfin.nix # Jellyfin — media server (disabled by default)
|
||||||
helm schema generate
|
transmission.nix # Transmission — torrent client (disabled by default)
|
||||||
|
hosts/
|
||||||
|
pi-main/
|
||||||
|
default.nix # Service selection + host-specific overrides
|
||||||
|
hardware.nix # Pi 4 boot, SD card labels, ARM platform
|
||||||
|
secrets/
|
||||||
|
.sops.yaml # Age key configuration
|
||||||
|
secrets.yaml # sops-encrypted secrets (commit only after encrypting)
|
||||||
|
PORTING.md # Step-by-step migration guide from the old Helm setup
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Template Testing
|
## Services and URLs
|
||||||
```bash
|
|
||||||
# Render templates locally with custom values
|
|
||||||
helm template homey . -f values.yaml --set homey.url=example.com
|
|
||||||
|
|
||||||
# Template with debug output
|
All services live under `zakobar.com`.
|
||||||
helm template homey . --debug 2>&1 | less
|
|
||||||
|
| Service | URL | Auth |
|
||||||
|
|---------|-----|------|
|
||||||
|
| Authelia | `auth.zakobar.com` | Public (it is the auth portal) |
|
||||||
|
| Gitea | `git.zakobar.com` | Gitea-native (LDAP) |
|
||||||
|
| Nextcloud | `nextcloud.zakobar.com` | Nextcloud-native |
|
||||||
|
| phpLDAPadmin | `ldapadmin.zakobar.com` | Authelia two_factor, admins only |
|
||||||
|
| Jellyfin | `jellyfin.zakobar.com` | Jellyfin-native |
|
||||||
|
| Transmission | `torrent.zakobar.com` | Authelia two_factor, admins only |
|
||||||
|
|
||||||
|
Internal ports (all bound to `127.0.0.1`):
|
||||||
|
|
||||||
|
| Container | Port |
|
||||||
|
|-----------|------|
|
||||||
|
| openldap | 389 |
|
||||||
|
| authelia | 9091 |
|
||||||
|
| gitea | 3000 |
|
||||||
|
| nextcloud | 8080 |
|
||||||
|
| nextcloud-postgres | 5432 |
|
||||||
|
| phpldapadmin | 8081 |
|
||||||
|
| jellyfin | 8096 |
|
||||||
|
| transmission | 9092 (not 9091 — avoids clash with authelia) |
|
||||||
|
|
||||||
|
## Storage Layout
|
||||||
|
|
||||||
|
All persistent data lives on the external HD at `/mnt/data/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/data/
|
||||||
|
openldap/
|
||||||
|
etc-ldap-slapd.d/ → /etc/ldap/slapd.d in container
|
||||||
|
var-lib-ldap/ → /var/lib/ldap in container
|
||||||
|
authelia/config/ → /config
|
||||||
|
gitea/data/ → /data
|
||||||
|
nextcloud/
|
||||||
|
html/ → /var/www/html
|
||||||
|
db/ → /var/lib/postgresql/data
|
||||||
|
db-dump/ → pg_dump output (pre-backup)
|
||||||
|
jellyfin/config/ → /config
|
||||||
|
media/movies|tvshows|... → shared media (read-only to jellyfin)
|
||||||
|
transmission/config/ → /config
|
||||||
|
restic-cache/ → restic local cache
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kubectl Validation
|
The drive device path is set per-host in `hosts/<name>/default.nix` via
|
||||||
```bash
|
`homey.storage.device`. Use a `/dev/disk/by-id/` path for stability.
|
||||||
# Dry-run apply to validate manifests
|
|
||||||
kubectl apply -f templates/auth.yaml --dry-run=server
|
|
||||||
|
|
||||||
# Get rendered template directly
|
## Build / Validate Commands
|
||||||
helm template homey . | kubectl apply --dry-run=server -f -
|
|
||||||
|
```bash
|
||||||
|
# Check flake structure and evaluate all hosts (no build)
|
||||||
|
nix flake check
|
||||||
|
|
||||||
|
# Dry-run: show what would change without applying
|
||||||
|
sudo nixos-rebuild dry-activate --flake .#pi-main
|
||||||
|
|
||||||
|
# Apply configuration
|
||||||
|
sudo nixos-rebuild switch --flake .#pi-main
|
||||||
|
|
||||||
|
# Build without switching (e.g. cross-compile on workstation)
|
||||||
|
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel
|
||||||
|
|
||||||
|
# Show diff between running system and new config
|
||||||
|
nvd diff /run/current-system $(nix build --no-link --print-out-paths .#nixosConfigurations.pi-main.config.system.build.toplevel)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Secret Management
|
||||||
|
|
||||||
|
Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix) and
|
||||||
|
age keys. The encrypted `secrets/secrets.yaml` is committed to the repo; the
|
||||||
|
age private key lives on the Pi at `/var/lib/sops-nix/key.txt`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit secrets (decrypts, opens $EDITOR, re-encrypts on save)
|
||||||
|
sops secrets/secrets.yaml
|
||||||
|
|
||||||
|
# Encrypt a plaintext secrets.yaml for the first time
|
||||||
|
sops --encrypt --in-place secrets/secrets.yaml
|
||||||
|
|
||||||
|
# Add a new host key (after generating it on the new machine)
|
||||||
|
# 1. Add the public key to secrets/.sops.yaml
|
||||||
|
# 2. Run:
|
||||||
|
sops updatekeys secrets/secrets.yaml
|
||||||
|
|
||||||
|
# Generate a new age key on a host
|
||||||
|
age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
age-keygen -y /var/lib/sops-nix/key.txt # print public key
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets that must come from the old deployment (see `PORTING.md` for how to
|
||||||
|
extract them from the old k8s cluster):
|
||||||
|
|
||||||
|
- `openldap/admin_password`, `openldap/config_password`, `openldap/ro_password`
|
||||||
|
- `gitea/admin_password`
|
||||||
|
- `nextcloud/admin_password`, `nextcloud/postgres_password`
|
||||||
|
|
||||||
|
Everything else (authelia JWT/session/encryption keys, gitea JWT tokens,
|
||||||
|
restic password, Cloudflare tokens) can be generated fresh.
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
### YAML Structure
|
### Nix
|
||||||
|
|
||||||
1. **Document Separators**: Use `---` at the start of each YAML document
|
1. **Module pattern** — every service is an opt-in module with an `enable` option:
|
||||||
```yaml
|
```nix
|
||||||
---
|
options.homey.myservice.enable = lib.mkEnableOption "My service";
|
||||||
apiVersion: v1
|
config = lib.mkIf config.homey.myservice.enable { ... };
|
||||||
kind: ConfigMap
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Indentation**: Use 2 spaces (not tabs)
|
2. **`homeyConfig` specialArgs** — top-level site config (domain, org name,
|
||||||
```yaml
|
timezone) is passed via `specialArgs` in `flake.nix` and accessed as
|
||||||
spec:
|
`homeyConfig` in every module. Do not read domain/org from hardcoded strings.
|
||||||
containers:
|
|
||||||
- name: app
|
|
||||||
image: nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Trailing Commas**: Optional but preferred for multi-line lists
|
3. **No secrets in the Nix store** — secrets are always read from sops-managed
|
||||||
```yaml
|
files at runtime, never embedded in the built config. Use
|
||||||
accessModes:
|
`config.sops.secrets."key".path` to get the runtime path of a secret file.
|
||||||
- ReadWriteMany
|
|
||||||
- ReadOnlyMany
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Quotes**: Use quotes for strings that might be interpreted as other types
|
4. **Secret injection pattern** — because `oci-containers` `environmentFiles`
|
||||||
- Always quote: `.Values.homey.url | quote`
|
is limited, use a `systemd ExecStartPre` script to write an ephemeral env
|
||||||
- Optional for simple strings like names
|
file at `/run/<service>-secrets.env` and reference it via `EnvironmentFile`.
|
||||||
|
Clean it up in `postStop`.
|
||||||
|
|
||||||
### Kubernetes Resources
|
5. **`--network=host`** — all containers use host networking for simplicity on
|
||||||
|
a single-node setup. Services communicate via `127.0.0.1:<port>`.
|
||||||
|
|
||||||
1. **Labels**: Use Kubernetes recommended labels
|
6. **Systemd ordering** — always express `after`/`requires` dependencies
|
||||||
```yaml
|
explicitly. The external HD mount unit is `mnt-data.mount`; containers that
|
||||||
labels:
|
need storage must depend on it.
|
||||||
app.kubernetes.io/name: openldap
|
|
||||||
app.kubernetes.io/component: auth
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Naming**: Use kebab-case for resource names
|
|
||||||
```yaml
|
|
||||||
name: openldap-admin
|
|
||||||
name: nextcloud-postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Storage**: Always specify `storageClassName: longhorn`
|
|
||||||
```yaml
|
|
||||||
spec:
|
|
||||||
storageClassName: longhorn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Helm Template Syntax
|
|
||||||
|
|
||||||
1. **Variable Assignment**: Use `$_ := set` for complex assignments
|
|
||||||
```yaml
|
|
||||||
{{- $_ := set $ "varname" (include "homey.lookuporgensecret" (merge (dict "secretname" "secret-name") $)) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Include with Merge**: Always pass `$` as the last argument
|
|
||||||
```yaml
|
|
||||||
{{ include "homey.randomsecret" (merge (dict "secretname" "secret-name" "secretval" $secretval) $) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Quote Values from .Values**: Use `quote` filter
|
|
||||||
```yaml
|
|
||||||
value: {{ .Values.homey.url | quote }}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Template Definitions**: Define reusable templates in `_definitions.yaml`
|
|
||||||
- `homey.lookuporgensecret` - Look up existing secrets or generate random
|
|
||||||
- `homey.randomsecret` - Generate a random secret
|
|
||||||
- `homey.randHex` - Generate random hex string
|
|
||||||
|
|
||||||
5. **Template Spacing**: Use whitespace control to avoid extra newlines
|
|
||||||
```yaml
|
|
||||||
{{- "leading minus" -}} # No newline before
|
|
||||||
{{ "trailing minus" -}} # No newline after
|
|
||||||
```
|
|
||||||
|
|
||||||
### Secret Management
|
|
||||||
|
|
||||||
1. **Annotations**: Always annotate managed secrets to prevent deletion
|
|
||||||
```yaml
|
|
||||||
annotations:
|
|
||||||
"helm.sh/resource-policy": "keep"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Secret Generation Pattern**:
|
|
||||||
```yaml
|
|
||||||
# Check for existing secret, create if not exists
|
|
||||||
{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace "secret-name") | default dict -}}
|
|
||||||
{{- $secretData := (get $secretObj "data") | default dict -}}
|
|
||||||
{{- $pass := (get $secretData "password") | default (randAlphaNum 32 | b64enc) -}}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Never hardcode secrets** - Use the secret lookup pattern above
|
|
||||||
|
|
||||||
### Config Files (files/ directory)
|
|
||||||
|
|
||||||
1. **Go Templates in Configs**: Use `tpl` function to process config files
|
|
||||||
```yaml
|
|
||||||
data:
|
|
||||||
config.yml: |-
|
|
||||||
{{ tpl (.Files.Get "files/authelia-config.yaml" | indent 4) . }}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Accessing Variables**: Config files can access `.Values.*` and custom variables set in templates
|
|
||||||
|
|
||||||
### Ingress Configuration
|
|
||||||
|
|
||||||
1. **TLS**: Always specify TLS with proper hosts and secret
|
|
||||||
```yaml
|
|
||||||
spec:
|
|
||||||
ingressClassName: {{ .Values.homey.ingress_class }}
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- auth.{{ .Values.homey.url }}
|
|
||||||
secretName: {{ .Values.homey.certname }}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Authelia Integration**: Use auth snippets for protected ingresses
|
|
||||||
```yaml
|
|
||||||
annotations:
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Organization
|
|
||||||
|
|
||||||
1. **File Structure**:
|
|
||||||
- `templates/_definitions.yaml` - Helper templates (secrets, utilities)
|
|
||||||
- `templates/auth.yaml` - Authentication services (OpenLDAP, Authelia, Gitea, Nextcloud, Radicale)
|
|
||||||
- `templates/media.yaml` - Media services (Jellyfin, Transmission)
|
|
||||||
- `templates/phpldapadmin.yaml` - LDAP admin interface
|
|
||||||
|
|
||||||
2. **Manifest Order** (within a file):
|
|
||||||
- PersistentVolumeClaim
|
|
||||||
- Secrets
|
|
||||||
- ConfigMaps
|
|
||||||
- Deployments
|
|
||||||
- Services
|
|
||||||
- Ingress
|
|
||||||
|
|
||||||
3. **Unused Resources**: Keep deprecated manifests in `unused/` directory
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
1. **Naming**: Use uppercase with underscores
|
|
||||||
```yaml
|
|
||||||
- name: LDAP_ORGANISATION
|
|
||||||
value: {{ .Values.homey.organization }}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Value Sources**: Prefer `valueFrom.secretKeyRef` over inline values
|
|
||||||
```yaml
|
|
||||||
- name: PASSWORD
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: secret-name
|
|
||||||
key: password
|
|
||||||
```
|
|
||||||
|
|
||||||
### Volume Mounts
|
|
||||||
|
|
||||||
1. **subPath**: Use `subPath` for shared PVCs
|
|
||||||
```yaml
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
subPath: service-name/data
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Read-only ConfigMaps**: Mark config mounts as read-only
|
|
||||||
```yaml
|
|
||||||
readOnly: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Operations
|
|
||||||
|
|
||||||
### Adding a New Service
|
### Adding a New Service
|
||||||
|
|
||||||
1. Add values to `values.yaml`
|
1. Create `modules/services/<name>.nix` following the existing module pattern.
|
||||||
2. Create/extend template in `templates/`
|
2. Add `homey.<name>.enable = false` as the default option.
|
||||||
3. Add PVC if persistent storage needed
|
3. Import the new module in `flake.nix` (in the `modules` list inside `mkHost`).
|
||||||
4. Add Ingress with appropriate annotations
|
4. Enable it in `hosts/pi-main/default.nix`.
|
||||||
5. Test with `helm template .`
|
5. Add a Caddy virtual host block in `modules/caddy.nix`.
|
||||||
|
6. Add the service data directory to `modules/storage.nix` `tmpfiles.rules`.
|
||||||
|
7. Add the data path to the `paths` list in `modules/backup.nix`.
|
||||||
|
8. Add any new secrets to `secrets/secrets.yaml` (plaintext) and document them.
|
||||||
|
|
||||||
### Updating Secrets
|
### Updating or Regenerating Secrets
|
||||||
|
|
||||||
Secrets are generated on first install. To regenerate:
|
|
||||||
```bash
|
|
||||||
kubectl delete secret <secret-name> -n homey
|
|
||||||
helm upgrade --install homey . -n homey
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging Templates
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Show all template variables available
|
# Edit the encrypted file — sops opens $EDITOR
|
||||||
helm template . --show-only templates/_helpers.tpl
|
sops secrets/secrets.yaml
|
||||||
|
|
||||||
# Render single template
|
# Copy updated secrets to the Pi and rebuild
|
||||||
helm template . --show-only templates/auth.yaml
|
rsync secrets/secrets.yaml admin@pi-main:/path/to/homey/secrets/
|
||||||
|
ssh admin@pi-main 'sudo nixos-rebuild switch --flake /path/to/homey#pi-main'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Debugging Containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all running containers
|
||||||
|
podman ps
|
||||||
|
|
||||||
|
# Follow logs for a service
|
||||||
|
journalctl -fu podman-authelia.service
|
||||||
|
|
||||||
|
# Drop into a running container
|
||||||
|
podman exec -it authelia sh
|
||||||
|
|
||||||
|
# Restart a single service
|
||||||
|
sudo systemctl restart podman-gitea.service
|
||||||
|
|
||||||
|
# Check why a service failed to start
|
||||||
|
systemctl status podman-openldap.service
|
||||||
|
journalctl -u podman-openldap.service --since "5 min ago"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outstanding TODOs
|
||||||
|
|
||||||
|
These items are known gaps that need to be addressed before the setup is
|
||||||
|
production-ready:
|
||||||
|
|
||||||
|
- [ ] **`caddy.nix` — fix `vendorHash`**: The Caddy build with the Cloudflare
|
||||||
|
DNS plugin uses `lib.fakeHash` as a placeholder. After the first `nix build`,
|
||||||
|
replace it with the hash Nix reports in the error message.
|
||||||
|
|
||||||
|
- [ ] **`hosts/pi-main/default.nix` — fill in real values**:
|
||||||
|
- SSH public key in `users.users.admin.openssh.authorizedKeys.keys`
|
||||||
|
- External HD device path in `homey.storage.device`
|
||||||
|
- Backup repository URL in `homey.backup.repository` — must be an S3-compatible
|
||||||
|
URL, e.g. `"s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name"`
|
||||||
|
|
||||||
|
- [ ] **`secrets/secrets.yaml` — populate and encrypt**: Fill in all secret
|
||||||
|
values (old passwords from k8s + freshly generated ones, including
|
||||||
|
`restic/s3_access_key_id` and `restic/s3_secret_access_key`), then run
|
||||||
|
`sops --encrypt --in-place secrets/secrets.yaml` before committing.
|
||||||
|
|
||||||
|
- [x] **`secrets/.sops.yaml` — PGP key**: The encryption subkey
|
||||||
|
`076AA297579A0064` is already in `.sops.yaml`.
|
||||||
|
|
||||||
|
- [ ] **Cloudflare Tunnel**: Create the tunnel in the Zero Trust dashboard,
|
||||||
|
copy the tunnel token into secrets, and configure public hostnames. See
|
||||||
|
`modules/cloudflared.nix` and Phase 3 of `PORTING.md` for details.
|
||||||
|
|
||||||
|
- [ ] **Second machine**: When ready, add `hosts/pi-secondary/` and uncomment
|
||||||
|
the `pi-secondary` entry in `flake.nix`. Services communicating cross-machine
|
||||||
|
should reference the primary Pi's LAN IP instead of `127.0.0.1`.
|
||||||
|
|
||||||
|
- [ ] **Jellyfin and Transmission**: Both modules are written and importable
|
||||||
|
but disabled. Enable in `hosts/pi-main/default.nix` when ready:
|
||||||
|
```nix
|
||||||
|
homey.jellyfin.enable = true;
|
||||||
|
homey.transmission.enable = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Backup — S3 credentials**: Add `restic/s3_access_key_id` and
|
||||||
|
`restic/s3_secret_access_key` to secrets, and set `homey.backup.repository`
|
||||||
|
to your S3-compatible bucket URL in `hosts/pi-main/default.nix`.
|
||||||
|
|
||||||
|
- [ ] **Backup — offload script**: Write `scripts/offload-backup.sh` for
|
||||||
|
manually copying snapshots to a local disk (USB attached to Pi, or a disk
|
||||||
|
on your workstation). Uses `restic copy` to clone from the S3 repo into a
|
||||||
|
local restic repo on the target path. See `TODO.org` for design notes.
|
||||||
|
|
||||||
|
### Post- Pi first boot
|
||||||
|
|
||||||
|
These items require the Pi to be built, flashed, and booted at least once.
|
||||||
|
|
||||||
|
- [ ] **`secrets/.sops.yaml` — add Pi age key**: After generating the age key
|
||||||
|
on the Pi (`age-keygen -o /var/lib/sops-nix/key.txt`), add the public key
|
||||||
|
to `.sops.yaml` alongside the existing PGP key, then run
|
||||||
|
`sops updatekeys secrets/secrets.yaml`.
|
||||||
|
|
||||||
|
- [ ] **`hosts/pi-main/hardware.nix` — verify SD card labels**: The file
|
||||||
|
assumes partition labels `NIXOS_SD` (root) and `FIRMWARE` (boot). Relabel
|
||||||
|
after flashing if they differ, or update the `fileSystems` entries.
|
||||||
|
|
||||||
|
- [ ] **Gitea LDAP auth**: After first start, configure LDAP authentication
|
||||||
|
in Gitea's admin panel (Admin → Authentication Sources → Add LDAP source).
|
||||||
|
The old Helm chart had this commented out; it must be done manually once.
|
||||||
|
Relevant settings:
|
||||||
|
- Host: `127.0.0.1`, Port: `389`, Security: Unencrypted
|
||||||
|
- Bind DN: `cn=readonly,dc=zakobar,dc=com`
|
||||||
|
- User search base: `ou=users,dc=zakobar,dc=com`
|
||||||
|
|
||||||
|
- [ ] **Nextcloud LDAP app**: After restoring the Nextcloud volume, verify
|
||||||
|
the LDAP Users and Contacts app is still configured correctly
|
||||||
|
(Admin → LDAP/AD Integration).
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: homey
|
|
||||||
description: Deploy a fancy home environment!
|
|
||||||
type: application
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: "1.16.0"
|
|
||||||
+400
@@ -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@<pi-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Copy the flake to the Pi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From your workstation (repo root)
|
||||||
|
rsync -avz --exclude='.git' . nixos@<pi-ip>:/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@<pi-ip>:/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@<pi-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — Restore Data from Old Volumes
|
||||||
|
|
||||||
|
Mount the external HD (if not auto-mounted):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mount /dev/disk/by-id/<your-drive-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` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
| `git` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
| `nextcloud` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
| `ldapadmin` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
| `jellyfin` | `zakobar.com` | `https://localhost:443` |
|
||||||
|
| `torrent` | `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 `zakobar.com` pointing to your tunnel's UUID (Cloudflare
|
||||||
|
creates this automatically when you add hostnames). You do not need to add
|
||||||
|
`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=zakobar,dc=com -D "cn=admin,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.zakobar.com
|
||||||
|
|
||||||
|
# Gitea login?
|
||||||
|
# Visit https://git.zakobar.com — should redirect to authelia if not logged in
|
||||||
|
|
||||||
|
# Nextcloud?
|
||||||
|
# Visit https://nextcloud.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 zakobar.com
|
||||||
|
192.168.1.100 auth.zakobar.com
|
||||||
|
192.168.1.100 git.zakobar.com
|
||||||
|
192.168.1.100 nextcloud.zakobar.com
|
||||||
|
192.168.1.100 ldapadmin.zakobar.com
|
||||||
|
192.168.1.100 jellyfin.zakobar.com
|
||||||
|
192.168.1.100 torrent.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 <your-repo-url> \
|
||||||
|
--password-file /run/secrets/restic_password \
|
||||||
|
snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore a single service from backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl stop podman-gitea.service
|
||||||
|
sudo restic -r <repo> 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`.
|
||||||
+192
-7
@@ -2,7 +2,146 @@
|
|||||||
|
|
||||||
A home environment for everyone!
|
A home environment for everyone!
|
||||||
|
|
||||||
* Installation
|
* NixOS Deployment (active branch: nixos-port)
|
||||||
|
|
||||||
|
** Prerequisites
|
||||||
|
|
||||||
|
Before building, make sure the following are set in the repo:
|
||||||
|
|
||||||
|
- =hosts/pi-main/default.nix= — SSH public key, static IP, WiFi SSID
|
||||||
|
- =secrets/secrets.yaml= — all secrets populated and sops-encrypted
|
||||||
|
- WiFi password secret formatted as =wifi_psk=YourPassword= (see below)
|
||||||
|
|
||||||
|
** Adding / updating secrets
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
sops secrets/secrets.yaml
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Opens your editor with the decrypted file. Save and quit to re-encrypt.
|
||||||
|
|
||||||
|
The WiFi password entry must use the =wifi_psk== prefix so wpa_supplicant
|
||||||
|
can look up the value by name:
|
||||||
|
|
||||||
|
#+begin_src yaml
|
||||||
|
wifi/psk: "wifi_psk=YourActualWifiPassword"
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Phase 1 — Bootstrap image (flash this first)
|
||||||
|
|
||||||
|
The full =pi-main= config requires sops secrets, which require an age key
|
||||||
|
on the Pi — but the age key doesn't exist until after first boot. To
|
||||||
|
break the chicken-and-egg problem, flash a minimal bootstrap image first.
|
||||||
|
|
||||||
|
Before building, fill in the WiFi password in =flake.nix= in the
|
||||||
|
=pi-main-bootstrap= config (search for =WIFI_PASSWORD_HERE=):
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
networks."Zakobar".psk = "your-actual-wifi-password";
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Build the bootstrap SD image (requires =aarch64-linux= build capability —
|
||||||
|
either =boot.binfmt.emulatedSystems = ["aarch64-linux"]= on your
|
||||||
|
workstation, or an aarch64 remote builder):
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
|
||||||
|
--system aarch64-linux
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Find your SD card device, then flash (double-check =/dev/sdX=!):
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
lsblk
|
||||||
|
|
||||||
|
zstdcat result/sd-image/nixos-sd-image-*.img.zst | \
|
||||||
|
sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
The Pi will boot at =192.168.1.100=, connect to =Zakobar= WiFi, and accept
|
||||||
|
SSH connections with your key. No services run yet.
|
||||||
|
|
||||||
|
** Phase 2 — Generate age key and re-encrypt secrets
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
# SSH into the Pi
|
||||||
|
ssh admin@192.168.1.100
|
||||||
|
|
||||||
|
# Generate the age key
|
||||||
|
sudo age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
|
||||||
|
# Print the public key — copy it
|
||||||
|
sudo age-keygen -y /var/lib/sops-nix/key.txt
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Back on your workstation, add the public key to =secrets/.sops.yaml=
|
||||||
|
alongside the existing PGP key:
|
||||||
|
|
||||||
|
#+begin_src yaml
|
||||||
|
keys:
|
||||||
|
- &pi_main age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: secrets/secrets.yaml$
|
||||||
|
key_groups:
|
||||||
|
- pgp:
|
||||||
|
- 076AA297579A0064
|
||||||
|
age:
|
||||||
|
- *pi_main
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then re-encrypt so the Pi can decrypt its own secrets:
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
sops updatekeys secrets/secrets.yaml
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** Phase 3 — Deploy the full config
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--build-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
The Pi builds its own config natively (no cross-compilation). sops-nix
|
||||||
|
will now decrypt all secrets and start all services.
|
||||||
|
|
||||||
|
** Caddy plugin hash
|
||||||
|
|
||||||
|
The first deploy will fail at the Caddy build step because =lib.fakeHash=
|
||||||
|
is a placeholder. Copy the correct hash from the error output and replace
|
||||||
|
it in =modules/caddy.nix=:
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
|
plugins = [ "github.com/caddy-dns/cloudflare@..." ];
|
||||||
|
hash = "sha256-REPLACE_WITH_REAL_HASH="; # ← paste here
|
||||||
|
};
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then re-run the deploy command from Phase 3.
|
||||||
|
|
||||||
|
** Ongoing deploys from workstation
|
||||||
|
|
||||||
|
All future config changes follow the same pattern:
|
||||||
|
|
||||||
|
1. Edit files on workstation
|
||||||
|
2. Run:
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--build-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
NixOS activates the new config on the Pi immediately, with an automatic
|
||||||
|
rollback if activation fails.
|
||||||
|
|
||||||
|
* Installation (legacy Helm)
|
||||||
|
|
||||||
Install using
|
Install using
|
||||||
|
|
||||||
@@ -12,11 +151,57 @@ helm upgrade --install homey . -n homey
|
|||||||
|
|
||||||
* Backing up
|
* Backing up
|
||||||
|
|
||||||
We must find a better solution
|
Backups use [[https://restic.net/][restic]] and run automatically via systemd on a daily schedule.
|
||||||
|
|
||||||
https://perfectmediaserver.com/day-two/top10apps.html
|
** Strategy — two tiers
|
||||||
|
|
||||||
Nefarious
|
1. *Primary (automatic)*: Daily backup to an S3-compatible bucket (Backblaze B2,
|
||||||
|
Wasabi, AWS S3, etc.). Restic deduplicates and encrypts before upload.
|
||||||
|
Retention: 7 daily, 4 weekly, 6 monthly snapshots.
|
||||||
|
|
||||||
|
2. *Offload (manual)*: Run =scripts/offload-backup.sh --target /path/to/disk=
|
||||||
|
to clone snapshots from the S3 repo onto a local disk (USB plugged into the
|
||||||
|
Pi, or a disk on your workstation). Uses =restic copy= so deduplication is
|
||||||
|
preserved on the target.
|
||||||
|
|
||||||
|
** What is backed up
|
||||||
|
|
||||||
|
All service data under =/mnt/data/=:
|
||||||
|
|
||||||
|
- =openldap/= — LDAP database and config
|
||||||
|
- =authelia/= — Authelia config and state
|
||||||
|
- =gitea/= — Gitea repositories and data
|
||||||
|
- =nextcloud/= — Nextcloud files + a =pg_dump= of the database
|
||||||
|
- =jellyfin/= — Jellyfin metadata (media files are excluded — re-downloadable)
|
||||||
|
- =transmission/= — Torrent client config
|
||||||
|
|
||||||
|
Nextcloud is placed into maintenance mode and postgres is =pg_dump='d before
|
||||||
|
each backup to ensure a consistent snapshot.
|
||||||
|
|
||||||
|
** Configuration
|
||||||
|
|
||||||
|
Repository URL and credentials are set per-host:
|
||||||
|
|
||||||
|
#+begin_src nix
|
||||||
|
# hosts/pi-main/default.nix
|
||||||
|
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
S3 credentials live in =secrets/secrets.yaml= as =restic/s3_access_key_id= and
|
||||||
|
=restic/s3_secret_access_key=.
|
||||||
|
|
||||||
|
** Restore
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
# List snapshots
|
||||||
|
restic -r s3:https://... snapshots
|
||||||
|
|
||||||
|
# Restore latest snapshot to /mnt/data
|
||||||
|
restic -r s3:https://... restore latest --target /mnt/data
|
||||||
|
|
||||||
|
# Restore a single service
|
||||||
|
restic -r s3:https://... restore latest --target /mnt/data --include /mnt/data/gitea
|
||||||
|
#+end_src
|
||||||
|
|
||||||
* LDAP Configuration
|
* LDAP Configuration
|
||||||
|
|
||||||
@@ -24,7 +209,7 @@ Logins are done to PHPLDAPADMIN
|
|||||||
|
|
||||||
DN is like:
|
DN is like:
|
||||||
|
|
||||||
cn=admin,dc=home,dc=,dc=io
|
cn=admin,dc=,dc=io
|
||||||
get-secret-val.sh homey openldap-admin password
|
get-secret-val.sh homey openldap-admin password
|
||||||
|
|
||||||
First thing we do is create an organization unit called users
|
First thing we do is create an organization unit called users
|
||||||
@@ -62,9 +247,9 @@ Add a new LDAP Authentication source
|
|||||||
Authentication name: Home LDAP
|
Authentication name: Home LDAP
|
||||||
Host: openldap
|
Host: openldap
|
||||||
Port: 389
|
Port: 389
|
||||||
Bind DN = cn=readonly,dc=home,dc=,dc=io
|
Bind DN = cn=readonly,dc=,dc=io
|
||||||
Bind Password: openldap-ro password
|
Bind Password: openldap-ro password
|
||||||
User Search Base: ou=users,dc=home,dc=,dc=io
|
User Search Base: ou=users,dc=,dc=io
|
||||||
user search filter = (uid=%s)
|
user search filter = (uid=%s)
|
||||||
Admin filter (title=admin)
|
Admin filter (title=admin)
|
||||||
Username Attribute: uid
|
Username Attribute: uid
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
#+TITLE: Homey NixOS Port — Outstanding Tasks
|
||||||
|
#+DATE: 2026-04-15
|
||||||
|
#+OPTIONS: toc:nil
|
||||||
|
|
||||||
|
* Secrets Setup
|
||||||
|
|
||||||
|
** DONE Configure sops recipients in =secrets/.sops.yaml=
|
||||||
|
GPG encryption subkey =076AA297579A0064= is already configured in
|
||||||
|
=.sops.yaml=. The Pi's age key will be added post-boot (see Deployment section).
|
||||||
|
|
||||||
|
** DONE Populate =secrets/secrets.yaml= with real values
|
||||||
|
Every key in =secrets/secrets.yaml= needs a real value. Fill in each row below.
|
||||||
|
|
||||||
|
*** Recovered from k8s backup
|
||||||
|
| Key | Source k8s secret | Value |
|
||||||
|
|----------------------------------+-------------------------+----------------------------------|
|
||||||
|
| openldap/admin_password | openldap-admin | lfQWQgBZporyJT4xFSJTnu4vQMC7UevW |
|
||||||
|
| openldap/config_password | openldap-config | ZxlWbDAeHLdHi5lgdxmyZOWzsG3qDgrT |
|
||||||
|
| openldap/ro_password | openldap-ro | CZ7JLn23vSzhVjNW7UHGZ2YLFJPDLGsF |
|
||||||
|
| gitea/admin_password | gitea-admin-pass | y5kCPeCP1e1sCzahd7QmLyJqdQvd37ek |
|
||||||
|
| nextcloud/postgres_password | nextcloud-postgres-pass | hEq4zt1B1VKYtVAoiKYDmswcUmTbknSP |
|
||||||
|
| authelia/jwt_secret | jwt-secret | YJZBnQCD4OmhJkgdr6kksmMCatrKLCl3 |
|
||||||
|
| authelia/storage_encryption_key | fek-secret | KYWRYApCWWIN60gpSi7jhLuj1Wcm5z9Q |
|
||||||
|
|
||||||
|
*** Needs generation or manual creation
|
||||||
|
| Key | Action |
|
||||||
|
|----------------------------------+---------------------------------------------------|
|
||||||
|
| authelia/session_secret | Generate fresh (64 random chars) |
|
||||||
|
| gitea/lfs_jwt_secret | Generate fresh (43-char base64url) |
|
||||||
|
| gitea/oauth2_jwt_secret | Generate fresh (43-char base64url) |
|
||||||
|
| gitea/internal_token | Generate fresh (100-char alphanumeric) |
|
||||||
|
| restic/password | Generate fresh (passphrase) |
|
||||||
|
| nextcloud/admin_password | NOT in k8s backup; try old value or reset later |
|
||||||
|
| cloudflare/api_token | Create DNS Edit token in Cloudflare dashboard |
|
||||||
|
| cloudflare/tunnel_token | Create tunnel in Cloudflare Zero Trust dashboard |
|
||||||
|
| restic/s3_access_key_id | Needs S3 provider credentials |
|
||||||
|
| restic/s3_secret_access_key | Needs S3 provider credentials |
|
||||||
|
|
||||||
|
Generate random secrets with:
|
||||||
|
#+begin_src bash
|
||||||
|
# 64-char hex string
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# base64url (for gitea tokens)
|
||||||
|
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
|
||||||
|
|
||||||
|
# 100-char alphanumeric (gitea internal token)
|
||||||
|
openssl rand -base64 75 | tr -dc 'A-Za-z0-9' | head -c 100
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** DONE Encrypt =secrets/secrets.yaml= with sops
|
||||||
|
Encrypted with PGP key =076AA297579A0064=. Safe to commit.
|
||||||
|
|
||||||
|
* Pi Hardware Setup
|
||||||
|
|
||||||
|
** DONE Fill in real values in =hosts/pi-main/default.nix=
|
||||||
|
- [X] =users.users.admin.openssh.authorizedKeys.keys= — SSH public key added
|
||||||
|
- [X] =homey.storage.device= — =/dev/disk/by-id/usb-WD_Ext_HDD_1021_5743415A4146313531393031-0:0-part1=
|
||||||
|
- [X] =homey.backup.repository= — =s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup=
|
||||||
|
|
||||||
|
** DONE Integrate nixos-raspberrypi flake
|
||||||
|
Replaced =nixos-hardware= with =nixos-raspberrypi= for vendor kernel, firmware,
|
||||||
|
u-boot bootloader, and binary cache. Both =pi-main= and =pi-main-bootstrap=
|
||||||
|
now use =nixos-raspberrypi.lib.nixosSystem= and =raspberry-pi-4.base=.
|
||||||
|
=nix flake check= passes.
|
||||||
|
|
||||||
|
** TODO Verify SD card partition labels in =hosts/pi-main/hardware.nix=
|
||||||
|
The config assumes labels =NIXOS_SD= (root) and =FIRMWARE= (boot).
|
||||||
|
After flashing, check with:
|
||||||
|
#+begin_src bash
|
||||||
|
lsblk -o NAME,LABEL
|
||||||
|
#+end_src
|
||||||
|
Update =fileSystems= entries in =hosts/pi-main/hardware.nix= if they differ.
|
||||||
|
|
||||||
|
* Caddy Build
|
||||||
|
|
||||||
|
** TODO Fix =vendorHash= in =modules/caddy.nix=
|
||||||
|
The Caddy build with the Cloudflare DNS plugin currently uses =lib.fakeHash=
|
||||||
|
as a placeholder. After the first =nix build= attempt it will fail with the
|
||||||
|
correct hash in the error message. Replace =lib.fakeHash= with that value.
|
||||||
|
|
||||||
|
* Cloudflare Setup
|
||||||
|
|
||||||
|
** DONE Create Cloudflare Tunnel
|
||||||
|
1. Go to Cloudflare Zero Trust dashboard → Networks → Tunnels → Create tunnel
|
||||||
|
2. Name it (e.g. =homey=)
|
||||||
|
3. Copy the tunnel token into =secrets/secrets.yaml= under =cloudflare/tunnel_token=
|
||||||
|
4. Configure public hostnames for each service (see service/URL table in AGENTS.md)
|
||||||
|
|
||||||
|
** DONE Create Cloudflare DNS API token
|
||||||
|
1. Cloudflare dashboard → My Profile → API Tokens → Create Token
|
||||||
|
2. Use the "Edit zone DNS" template, scope to =zakobar.com=
|
||||||
|
3. Copy into =secrets/secrets.yaml= under =cloudflare/api_token=
|
||||||
|
|
||||||
|
* Deployment
|
||||||
|
|
||||||
|
** TODO Phase 1 — Build and flash bootstrap SD card image
|
||||||
|
|
||||||
|
The bootstrap image is a minimal NixOS with SSH + WiFi only (no sops, no
|
||||||
|
services). Its sole purpose is to boot the Pi so you can generate the age key
|
||||||
|
and then deploy the full config remotely.
|
||||||
|
|
||||||
|
Build on workstation (cross-compiles for aarch64):
|
||||||
|
#+begin_src bash
|
||||||
|
# Accept the nixos-raspberrypi cache config so pre-built kernel/firmware
|
||||||
|
# are fetched instead of compiled. First build still takes ~10-20 min.
|
||||||
|
nix build .#nixosConfigurations.pi-main-bootstrap.config.system.build.sdImage \
|
||||||
|
--accept-flake-config
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Flash to SD card (replace =/dev/sdX= with your card's device):
|
||||||
|
#+begin_src bash
|
||||||
|
# Decompress and write in one step — avoids storing the raw image on disk
|
||||||
|
zstdcat result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
|
sudo sync
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Insert the SD card into the Pi and power it on.
|
||||||
|
It will connect to WiFi (=Zakobar=) with static IP =192.168.1.100=.
|
||||||
|
|
||||||
|
Verify SSH access (wait ~60 s for first boot):
|
||||||
|
#+begin_src bash
|
||||||
|
ssh admin@192.168.1.100
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** TODO Phase 2 — Generate age key and add it to sops
|
||||||
|
|
||||||
|
On the Pi (over SSH):
|
||||||
|
#+begin_src bash
|
||||||
|
sudo mkdir -p /var/lib/sops-nix
|
||||||
|
sudo age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
# Print the public key — copy this output to your workstation clipboard
|
||||||
|
sudo age-keygen -y /var/lib/sops-nix/key.txt
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
On the workstation — edit =secrets/.sops.yaml=, uncomment the age section
|
||||||
|
and replace the placeholder with the public key you just copied:
|
||||||
|
#+begin_src yaml
|
||||||
|
key_groups:
|
||||||
|
- pgp:
|
||||||
|
- 076AA297579A0064
|
||||||
|
age:
|
||||||
|
- age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # paste here
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Re-encrypt =secrets/secrets.yaml= so the Pi's age key can decrypt it:
|
||||||
|
#+begin_src bash
|
||||||
|
sops updatekeys secrets/secrets.yaml
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Commit and push:
|
||||||
|
#+begin_src bash
|
||||||
|
git add secrets/.sops.yaml secrets/secrets.yaml
|
||||||
|
git commit -m "add Pi age key to sops recipients"
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** TODO Phase 3 — Fix Caddy vendorHash, then deploy full config
|
||||||
|
|
||||||
|
The full =pi-main= config includes Caddy built with the Cloudflare DNS
|
||||||
|
plugin. The first build will fail with the correct hash in the error output.
|
||||||
|
|
||||||
|
Attempt the build to get the hash:
|
||||||
|
#+begin_src bash
|
||||||
|
nix build .#nixosConfigurations.pi-main.config.system.build.toplevel \
|
||||||
|
--accept-flake-config 2>&1 | grep 'got:'
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Copy the hash from the error message and replace =lib.fakeHash= in
|
||||||
|
=modules/caddy.nix=, then commit:
|
||||||
|
#+begin_src bash
|
||||||
|
git add modules/caddy.nix
|
||||||
|
git commit -m "fix caddy vendorHash"
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Deploy to the Pi:
|
||||||
|
#+begin_src bash
|
||||||
|
# Dry-run first — shows what will change without applying
|
||||||
|
nixos-rebuild dry-activate \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo \
|
||||||
|
--accept-flake-config
|
||||||
|
|
||||||
|
# Apply when happy with the diff
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--flake .#pi-main \
|
||||||
|
--target-host admin@192.168.1.100 \
|
||||||
|
--use-remote-sudo \
|
||||||
|
--accept-flake-config
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
After a successful switch, subsequent deploys can use the hostname:
|
||||||
|
#+begin_src bash
|
||||||
|
nixos-rebuild switch --flake .#pi-main --target-host admin@pi-main --use-remote-sudo
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Post-Deployment Manual Steps
|
||||||
|
|
||||||
|
** DONE Configure Gitea LDAP authentication
|
||||||
|
Admin → Site Administration → Authentication Sources → Add LDAP (via BindDN):
|
||||||
|
- Host: =127.0.0.1=, Port: =389=, Security: Unencrypted
|
||||||
|
- Bind DN: =cn=readonly,dc=zakobar,dc=com=
|
||||||
|
- Bind Password: see =openldap/ro_password= in sops
|
||||||
|
- User Search Base: =ou=users,dc=zakobar,dc=com=
|
||||||
|
- User Filter: =(&(objectClass=inetOrgPerson)(uid=%s))=
|
||||||
|
- Username attribute: =uid=
|
||||||
|
- First name attribute: =cn=
|
||||||
|
- Surname attribute: =sn=
|
||||||
|
- Email attribute: =mail=
|
||||||
|
|
||||||
|
** TODO Verify Nextcloud LDAP app configuration
|
||||||
|
After restoring the Nextcloud volume, check:
|
||||||
|
Admin → LDAP/AD Integration — confirm the LDAP Users and Contacts app is configured.
|
||||||
|
If reconfiguring from scratch, use the same settings as Gitea above but with
|
||||||
|
Nextcloud's LDAP wizard:
|
||||||
|
- Server: =127.0.0.1=, Port: =389=
|
||||||
|
- Bind DN: =cn=readonly,dc=zakobar,dc=com=
|
||||||
|
- Bind Password: see =openldap/ro_password= in sops
|
||||||
|
- Base DN: =dc=zakobar,dc=com=
|
||||||
|
- Users: filter =objectClass=inetOrgPerson=, search base =ou=users=
|
||||||
|
- Login attribute: =uid=
|
||||||
|
- Email attribute: =mail=
|
||||||
|
|
||||||
|
** TODO (Optional) Enable Jellyfin and Transmission
|
||||||
|
When ready, in =hosts/pi-main/default.nix=:
|
||||||
|
#+begin_src nix
|
||||||
|
homey.jellyfin.enable = true;
|
||||||
|
homey.transmission.enable = true;
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Backup Strategy
|
||||||
|
|
||||||
|
** TODO Configure S3-compatible automatic backup target
|
||||||
|
Update =homey.backup.repository= in =hosts/pi-main/default.nix= to point at
|
||||||
|
your S3-compatible bucket (Backblaze B2, Wasabi, AWS S3, etc.):
|
||||||
|
#+begin_src nix
|
||||||
|
homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket-name";
|
||||||
|
# or for AWS:
|
||||||
|
# homey.backup.repository = "s3:s3.amazonaws.com/your-bucket-name";
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Add the S3 credentials to =secrets/secrets.yaml=:
|
||||||
|
#+begin_src yaml
|
||||||
|
restic/s3_access_key_id: "YOUR_KEY_ID"
|
||||||
|
restic/s3_secret_access_key: "YOUR_SECRET_KEY"
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Then wire them into =modules/backup.nix= via environment variables:
|
||||||
|
=AWS_ACCESS_KEY_ID= and =AWS_SECRET_ACCESS_KEY= (restic reads these natively).
|
||||||
|
|
||||||
|
The existing daily schedule + prune retention in =modules/backup.nix= will
|
||||||
|
handle the rest automatically.
|
||||||
|
|
||||||
|
** TODO Write manual offload script (=scripts/offload-backup.sh=)
|
||||||
|
A standalone script for copying backup data to an external disk — either
|
||||||
|
plugged directly into the Pi or mounted on your workstation.
|
||||||
|
|
||||||
|
Design:
|
||||||
|
- Accepts a =--target= argument: a local path to the mounted disk
|
||||||
|
(e.g. =/media/aner/backup-disk= or =/mnt/usb=)
|
||||||
|
- Uses =restic copy= to clone snapshots from the S3 repo into a local restic
|
||||||
|
repo on the target disk (deduplication is preserved, no double storage)
|
||||||
|
- Alternatively can use =rsync= for a plain directory copy if restic is not
|
||||||
|
available on the target machine
|
||||||
|
- Should be runnable from either the Pi or a workstation (with the Pi's data
|
||||||
|
disk mounted or accessible over SSH)
|
||||||
|
|
||||||
|
Example invocation:
|
||||||
|
#+begin_src bash
|
||||||
|
# On the Pi, with USB disk mounted at /mnt/usb:
|
||||||
|
./scripts/offload-backup.sh --target /mnt/usb/homey-backup
|
||||||
|
|
||||||
|
# On workstation, with Pi data disk mounted locally:
|
||||||
|
./scripts/offload-backup.sh --target /media/aner/backup-disk/homey-backup
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
This script does not exist yet — needs to be written.
|
||||||
|
|
||||||
|
* Future
|
||||||
|
|
||||||
|
** TODO Add second machine (=pi-secondary=)
|
||||||
|
When ready:
|
||||||
|
1. Create =hosts/pi-secondary/= directory with =default.nix= and =hardware.nix=
|
||||||
|
2. Uncomment the =pi-secondary= entry in =flake.nix=
|
||||||
|
3. Services communicating cross-machine should reference the primary Pi's LAN IP
|
||||||
|
instead of =127.0.0.1=
|
||||||
@@ -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."
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
#+TITLE: Caddy, Cloudflare Tunnel & TLS Setup
|
||||||
|
#+DATE: 2026-04-23
|
||||||
|
#+AUTHOR: homey project
|
||||||
|
#+OPTIONS: toc:2 num:t
|
||||||
|
|
||||||
|
* Overview
|
||||||
|
|
||||||
|
This document describes the TLS and reverse-proxy architecture for the homey
|
||||||
|
self-hosted stack, the problems encountered while getting it working, and the
|
||||||
|
final configuration that resolved them. It is intended as a reference for
|
||||||
|
future debugging and for adding new services.
|
||||||
|
|
||||||
|
** Traffic flow
|
||||||
|
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
Browser
|
||||||
|
│ HTTPS (TLS terminated by Cloudflare edge, *.zakobar.com cert)
|
||||||
|
▼
|
||||||
|
Cloudflare edge (anycast IP)
|
||||||
|
│ QUIC/HTTP2 tunnel (outbound from Pi, no open inbound ports)
|
||||||
|
▼
|
||||||
|
cloudflared daemon on Pi (systemd: cloudflared-tunnel.service)
|
||||||
|
│ plain HTTP on loopback http://localhost:80
|
||||||
|
▼
|
||||||
|
Caddy reverse proxy (systemd: caddy.service, port 80 + 443)
|
||||||
|
│ proxies to backend by Host header
|
||||||
|
▼
|
||||||
|
Service container (podman, port on 127.0.0.1)
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- TLS to the browser is provided entirely by Cloudflare's Universal SSL cert
|
||||||
|
(~*.zakobar.com~), not by the Pi's Let's Encrypt cert.
|
||||||
|
- The Pi's Let's Encrypt cert (~*.zakobar.com~ via DNS-01) is used only for
|
||||||
|
direct LAN access (bypassing the tunnel).
|
||||||
|
- The tunnel leg (cloudflared → Caddy) is plain HTTP on loopback — this is
|
||||||
|
safe because both endpoints are the same machine.
|
||||||
|
|
||||||
|
* Components
|
||||||
|
|
||||||
|
** Caddy (~modules/caddy.nix~)
|
||||||
|
|
||||||
|
Caddy runs as a NixOS service (~services.caddy~) using a custom build that
|
||||||
|
includes the ~caddy-dns/cloudflare~ plugin for DNS-01 ACME challenges.
|
||||||
|
|
||||||
|
*** Custom build
|
||||||
|
|
||||||
|
The nixpkgs ~caddy~ package does not include the Cloudflare DNS plugin by
|
||||||
|
default. It is built using the ~withPlugins~ passthru function (backed by
|
||||||
|
xcaddy):
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
|
plugins = [
|
||||||
|
"github.com/caddy-dns/cloudflare@v0.2.4"
|
||||||
|
];
|
||||||
|
hash = "sha256-...";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
The ~hash~ is a fixed-output derivation hash that must be updated whenever
|
||||||
|
the plugin version changes. Use ~lib.fakeHash~ to trigger a build failure
|
||||||
|
that prints the correct hash, then substitute it.
|
||||||
|
|
||||||
|
*** API token injection
|
||||||
|
|
||||||
|
The Cloudflare API token is stored in sops (~cloudflare/api_token~) and
|
||||||
|
injected into the Caddy process via ~systemd LoadCredential~:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
serviceConfig.LoadCredential =
|
||||||
|
"cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
|
||||||
|
ExecStart = lib.mkForce [
|
||||||
|
""
|
||||||
|
(pkgs.writeShellScript "caddy-start" ''
|
||||||
|
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
|
||||||
|
exec caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Virtual hosts — dual HTTP/HTTPS entries
|
||||||
|
|
||||||
|
Each service has *two* Caddyfile vhost entries:
|
||||||
|
|
||||||
|
| Entry | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| ~git.zakobar.com~ | HTTPS — for direct LAN access; Caddy handles TLS |
|
||||||
|
| ~http://git.zakobar.com~ | HTTP — for cloudflared on loopback; no redirect |
|
||||||
|
|
||||||
|
Caddy's default behaviour is to automatically redirect HTTP → HTTPS for any
|
||||||
|
hostname that has a matching HTTPS vhost. By explicitly defining an
|
||||||
|
~http://~ vhost, that redirect is suppressed and cloudflared gets a direct
|
||||||
|
200 response instead of a redirect loop.
|
||||||
|
|
||||||
|
Without the ~http://~ vhost, accessing via the tunnel produces:
|
||||||
|
~ERR_TOO_MANY_REDIRECTS~ in the browser because cloudflared follows the 308
|
||||||
|
back to HTTP indefinitely.
|
||||||
|
|
||||||
|
*** Global config
|
||||||
|
|
||||||
|
#+BEGIN_SRC caddyfile
|
||||||
|
{
|
||||||
|
email admin@zakobar.com
|
||||||
|
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
|
||||||
|
}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
The ~acme_dns~ directive in the global block tells Caddy to use DNS-01
|
||||||
|
challenges for *all* HTTPS vhosts. This allows wildcard and multi-level
|
||||||
|
subdomain certs to be issued without any inbound port 80 requirement.
|
||||||
|
|
||||||
|
** Cloudflare Tunnel (~modules/cloudflared.nix~)
|
||||||
|
|
||||||
|
cloudflared runs as a plain systemd service using the token-based tunnel
|
||||||
|
approach (~cloudflared tunnel run --token~). No local credentials file or
|
||||||
|
config file is needed — just the tunnel token from the Zero Trust dashboard.
|
||||||
|
|
||||||
|
*** Tunnel configuration (Zero Trust dashboard)
|
||||||
|
|
||||||
|
One wildcard public hostname entry covers all services:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Hostname | ~*.zakobar.com~ |
|
||||||
|
| Service | ~http://localhost:80~ |
|
||||||
|
| No TLS Verify | off (not needed for HTTP) |
|
||||||
|
| HTTP Host Header | (empty — cloudflared forwards the real Host header) |
|
||||||
|
| Origin Server Name | (empty — not needed for HTTP) |
|
||||||
|
|
||||||
|
cloudflared automatically forwards the incoming ~Host~ header (e.g.
|
||||||
|
~git.zakobar.com~) to Caddy, which uses it to select the correct vhost and
|
||||||
|
backend.
|
||||||
|
|
||||||
|
*** DNS records
|
||||||
|
|
||||||
|
A single wildcard CNAME record in Cloudflare DNS covers all subdomains:
|
||||||
|
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
*.zakobar.com CNAME <tunnel-id>.cfargotunnel.com (proxied, orange cloud)
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
This means new services require no DNS changes — only a new Caddy vhost.
|
||||||
|
|
||||||
|
*** Cloudflare SSL/TLS mode
|
||||||
|
|
||||||
|
Set to *Full (strict)* in the Cloudflare dashboard (SSL/TLS → Overview).
|
||||||
|
|
||||||
|
| Mode | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| Off | No HTTPS to browser |
|
||||||
|
| Flexible | HTTPS to browser, HTTP to origin |
|
||||||
|
| Full | HTTPS to browser, HTTPS to origin (cert not validated) |
|
||||||
|
| Full (strict) | HTTPS to browser, HTTPS to origin (cert must be valid) |
|
||||||
|
|
||||||
|
Full (strict) works here because Cloudflare terminates TLS at its own edge
|
||||||
|
using its Universal cert, and the origin (cloudflared → Caddy) uses plain
|
||||||
|
HTTP which Cloudflare does not validate in this tunnel architecture.
|
||||||
|
|
||||||
|
* Problems Encountered & How They Were Resolved
|
||||||
|
|
||||||
|
** 1. ~caddy-dns/cloudflare~ rejected ~cfut_~ token format
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
provision dns.providers.cloudflare: API token 'cfut_...' appears invalid;
|
||||||
|
ensure it's correctly entered and not wrapped in braces nor quotes
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
Cloudflare introduced new token formats with a ~cfut_~ (user token) or
|
||||||
|
~cfat_~ (account token) prefix. These tokens are 54 characters long. The
|
||||||
|
~caddy-dns/cloudflare~ plugin had a validation regex ~{35,50}~ that rejected
|
||||||
|
tokens longer than 50 characters, failing before even making an API call.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
The fix was merged into the plugin's master branch as commit ~a8737d0~ and
|
||||||
|
included in the ~v0.2.4~ tag (despite the tag previously being associated
|
||||||
|
with an older tree — the proxy confirmed ~v0.2.4~ resolves to ~a8737d0~).
|
||||||
|
|
||||||
|
Updating the ~hash~ in ~caddy.nix~ to the value produced by ~lib.fakeHash~
|
||||||
|
forced a fresh fetch of the corrected ~v0.2.4~ tree:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
plugins = [ "github.com/caddy-dns/cloudflare@v0.2.4" ];
|
||||||
|
hash = lib.fakeHash; # replace with hash from build error output
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
Run ~nix build .#nixosConfigurations.pi-main.config.system.build.toplevel~,
|
||||||
|
copy the ~got:~ hash from the error, substitute it, and rebuild.
|
||||||
|
|
||||||
|
** 2. cloudflared ~tls: internal error~ (SNI mismatch)
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
Unable to reach the origin service: remote error: tls: internal error
|
||||||
|
originService=https://localhost:443
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
cloudflared connected to ~https://localhost:443~ without sending an SNI
|
||||||
|
(Server Name Indication) hostname in the TLS ClientHello. Caddy could not
|
||||||
|
match any vhost, had no certificate for ~localhost~, and aborted the
|
||||||
|
handshake with a TLS internal error.
|
||||||
|
|
||||||
|
Setting the ~HTTP Host Header~ override in the dashboard fixes the HTTP
|
||||||
|
layer but does *not* affect the TLS SNI, which is negotiated before HTTP
|
||||||
|
headers are exchanged.
|
||||||
|
|
||||||
|
Setting the ~Origin Server Name~ field does set the SNI, but for a wildcard
|
||||||
|
rule (~*.zakobar.com~) the dashboard only accepts a static value, not a
|
||||||
|
dynamic placeholder — so it cannot be used for a catch-all.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Switch the tunnel service from ~https://localhost:443~ to
|
||||||
|
~http://localhost:80~. The internal leg does not need TLS (loopback
|
||||||
|
interface, same machine). Caddy's HTTP vhosts handle the requests directly.
|
||||||
|
|
||||||
|
** 3. Cloudflare edge TLS handshake failure (~*.home.zakobar.com~)
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
#+BEGIN_EXAMPLE
|
||||||
|
TLS connect error: error:0A000410:SSL routines::ssl/tls alert handshake failure
|
||||||
|
#+END_EXAMPLE
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
The domain was originally configured as ~home.zakobar.com~ (base domain),
|
||||||
|
making all services two levels deep: ~git.home.zakobar.com~,
|
||||||
|
~auth.home.zakobar.com~, etc. Cloudflare's free Universal SSL certificate
|
||||||
|
covers only one level of wildcard: ~*.zakobar.com~. It does *not* cover
|
||||||
|
~*.home.zakobar.com~ (two levels). The Cloudflare edge had no certificate to
|
||||||
|
present to browsers for these hostnames, causing a TLS handshake failure
|
||||||
|
before the request ever reached the tunnel.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Move all services to single-level subdomains under ~zakobar.com~
|
||||||
|
(~git.zakobar.com~, ~auth.zakobar.com~, etc.). In the NixOS config this
|
||||||
|
required only one line change — the ~domain~ field in ~flake.nix~:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
domain = "zakobar.com"; # was "home.zakobar.com"
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
All modules reference ~homeyConfig.domain~ and updated automatically on
|
||||||
|
rebuild. Tunnel hostnames and DNS records in the Cloudflare dashboard were
|
||||||
|
updated to match.
|
||||||
|
|
||||||
|
** 4. ~ERR_TOO_MANY_REDIRECTS~ via tunnel
|
||||||
|
|
||||||
|
*Symptom:*
|
||||||
|
Browser shows ~ERR_TOO_MANY_REDIRECTS~ when accessing any service through
|
||||||
|
the Cloudflare tunnel.
|
||||||
|
|
||||||
|
*Cause:*
|
||||||
|
cloudflared was talking to Caddy over plain HTTP (~http://localhost:80~).
|
||||||
|
Caddy's default behaviour is to issue a 308 permanent redirect from HTTP to
|
||||||
|
HTTPS for any hostname that has a matching HTTPS vhost. cloudflared followed
|
||||||
|
the redirect back to ~http://localhost:80~, which redirected again,
|
||||||
|
indefinitely.
|
||||||
|
|
||||||
|
*Fix:*
|
||||||
|
Add explicit ~http://~ vhost entries in ~caddy.nix~ for every service. When
|
||||||
|
Caddy has an explicit HTTP vhost for a hostname, it serves it directly
|
||||||
|
without redirecting:
|
||||||
|
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
"git.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:3000";
|
||||||
|
};
|
||||||
|
"http://git.${domain}" = { # ← suppresses HTTP→HTTPS redirect
|
||||||
|
extraConfig = "reverse_proxy localhost:3000";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
* Adding a New Service
|
||||||
|
|
||||||
|
To expose a new service through the tunnel:
|
||||||
|
|
||||||
|
1. Create ~modules/services/<name>.nix~ following the module pattern.
|
||||||
|
2. Add both a plain and ~http://~ vhost in ~modules/caddy.nix~:
|
||||||
|
#+BEGIN_SRC nix
|
||||||
|
"<name>.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:<port>";
|
||||||
|
};
|
||||||
|
"http://<name>.${domain}" = {
|
||||||
|
extraConfig = "reverse_proxy localhost:<port>";
|
||||||
|
};
|
||||||
|
#+END_SRC
|
||||||
|
3. No DNS or tunnel changes needed — the wildcard CNAME and wildcard tunnel
|
||||||
|
rule (~*.zakobar.com~) cover new subdomains automatically.
|
||||||
|
4. Rebuild and switch: ~sudo nixos-rebuild switch --flake .#pi-main~
|
||||||
|
|
||||||
|
* Certificate Details
|
||||||
|
|
||||||
|
** Let's Encrypt cert (LAN access)
|
||||||
|
|
||||||
|
- Issued per-hostname by Caddy via DNS-01 ACME using the Cloudflare API.
|
||||||
|
- Covers each hostname individually (e.g. ~git.zakobar.com~).
|
||||||
|
- Stored in ~/var/lib/caddy/.local/share/caddy/certificates/~.
|
||||||
|
- Used only when accessing services directly on the LAN (bypassing tunnel).
|
||||||
|
- Auto-renewed by Caddy.
|
||||||
|
|
||||||
|
** Cloudflare Universal SSL cert (tunnel / remote access)
|
||||||
|
|
||||||
|
- Issued by Google Trust Services for ~*.zakobar.com~.
|
||||||
|
- Managed entirely by Cloudflare — no action required on the Pi.
|
||||||
|
- Covers all single-level subdomains (~git.zakobar.com~, ~auth.zakobar.com~, etc.).
|
||||||
|
- Does *not* cover two-level subdomains (~git.home.zakobar.com~) — this was
|
||||||
|
the root cause of problem #3 above.
|
||||||
|
|
||||||
|
* Quick Reference: Debugging Checklist
|
||||||
|
|
||||||
|
| Symptom | Where to look | Command |
|
||||||
|
|---|---|---|
|
||||||
|
| 502 Bad Gateway | cloudflared logs | ~journalctl -u cloudflared-tunnel -n 50~ |
|
||||||
|
| 502 Bad Gateway | Caddy → backend | ~curl http://localhost:<port>/~ |
|
||||||
|
| TLS internal error | SNI / cert issue | ~curl -sv --resolve host:443:127.0.0.1 https://host/~ |
|
||||||
|
| Too many redirects | HTTP vhost missing | check ~http://~ entries in caddy.nix |
|
||||||
|
| Handshake failure at edge | Cloudflare cert scope | check SSL/TLS → Edge Certificates |
|
||||||
|
| Token appears invalid | plugin version | check ~caddy-dns/cloudflare~ version vs token format |
|
||||||
|
| Caddy won't start | token / config error | ~journalctl -u caddy --since "5 min ago"~ |
|
||||||
@@ -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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use
|
|
||||||
Sabre\DAV;
|
|
||||||
|
|
||||||
// The autoloader
|
|
||||||
require 'vendor/autoload.php';
|
|
||||||
|
|
||||||
// Now we're creating a whole bunch of objects
|
|
||||||
$rootDirectory = new DAV\FS\Directory('public');
|
|
||||||
|
|
||||||
// The server object is responsible for making sense out of the WebDAV protocol
|
|
||||||
$server = new DAV\Server($rootDirectory);
|
|
||||||
|
|
||||||
// If your server is not on your webroot, make sure the following line has the
|
|
||||||
// correct information
|
|
||||||
$server->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();
|
|
||||||
@@ -1,580 +0,0 @@
|
|||||||
<?php
|
|
||||||
$c->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');
|
|
||||||
|
|
||||||
@@ -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 "=" "" }}
|
|
||||||
@@ -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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use
|
|
||||||
Sabre\DAV;
|
|
||||||
|
|
||||||
// The autoloader
|
|
||||||
require 'vendor/autoload.php';
|
|
||||||
|
|
||||||
// Now we're creating a whole bunch of objects
|
|
||||||
$rootDirectory = new DAV\FS\Directory('public');
|
|
||||||
|
|
||||||
// The server object is responsible for making sense out of the WebDAV protocol
|
|
||||||
$server = new DAV\Server($rootDirectory);
|
|
||||||
|
|
||||||
// If your server is not on your webroot, make sure the following line has the
|
|
||||||
// correct information
|
|
||||||
$server->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();
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
description = "Homey - self-hosted home server NixOS configuration";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
|
||||||
|
# sops-nix for secret management
|
||||||
|
sops-nix = {
|
||||||
|
url = "github:Mic92/sops-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
# nixos-hardware provides RPi4 wireless firmware.
|
||||||
|
# We use only the minimal pieces needed for a headless server —
|
||||||
|
# no display, audio, or bluetooth modules.
|
||||||
|
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, sops-nix, nixos-hardware, ... }@inputs:
|
||||||
|
let
|
||||||
|
# Shared specialArgs passed to every host
|
||||||
|
commonArgs = {
|
||||||
|
inherit inputs;
|
||||||
|
# Top-level site config — override per-host if needed
|
||||||
|
homeyConfig = {
|
||||||
|
domain = "zakobar.com"; # base domain for all services
|
||||||
|
organization = "Zakobar Home Server";
|
||||||
|
timezone = "Asia/Jerusalem";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Minimal RPi4 hardware module for a headless server.
|
||||||
|
# Provides only: bootloader, initrd modules, wireless firmware, DTB filter.
|
||||||
|
# Deliberately excludes display, audio, bluetooth from the full nixos-hardware module.
|
||||||
|
rpi4Headless = { pkgs, ... }: {
|
||||||
|
boot.loader.grub.enable = false;
|
||||||
|
boot.loader.generic-extlinux-compatible.enable = true;
|
||||||
|
boot.initrd.availableKernelModules = [
|
||||||
|
"pcie-brcmstb" # PCIe bus (USB3, NVMe)
|
||||||
|
"reset-raspberrypi" # required for vl805 firmware
|
||||||
|
"usb-storage"
|
||||||
|
"usbhid"
|
||||||
|
"vc4" # VideoCore (needed even headless for boot)
|
||||||
|
];
|
||||||
|
# sd-image-aarch64.nix lists modules for many SoCs (including sun4i-drm
|
||||||
|
# for Allwinner boards) that don't exist in linux_rpi4. Allow missing.
|
||||||
|
boot.initrd.includeDefaultModules = false;
|
||||||
|
hardware.deviceTree.filter = "bcm2711-rpi-*.dtb";
|
||||||
|
hardware.firmware = [
|
||||||
|
(pkgs.callPackage "${nixos-hardware}/raspberry-pi/common/raspberry-pi-wireless-firmware.nix" {})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
mkHost = { hostPath, extraModules ? [] }:
|
||||||
|
nixpkgs.lib.nixosSystem {
|
||||||
|
specialArgs = commonArgs;
|
||||||
|
modules = [
|
||||||
|
sops-nix.nixosModules.sops
|
||||||
|
rpi4Headless
|
||||||
|
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 = {
|
||||||
|
|
||||||
|
# Bootstrap image — flash this first, then deploy pi-main.
|
||||||
|
# See hosts/pi-main-bootstrap/default.nix for details.
|
||||||
|
pi-main-bootstrap = nixpkgs.lib.nixosSystem {
|
||||||
|
specialArgs = commonArgs;
|
||||||
|
modules = [
|
||||||
|
rpi4Headless
|
||||||
|
({ modulesPath, ... }: {
|
||||||
|
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
|
||||||
|
})
|
||||||
|
./hosts/pi-main/hardware.nix
|
||||||
|
./hosts/pi-main-bootstrap/default.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Primary Raspberry Pi 4
|
||||||
|
pi-main = mkHost {
|
||||||
|
hostPath = ./hosts/pi-main/default.nix;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Future second machine (placeholder — uncomment and configure when ready)
|
||||||
|
# pi-secondary = mkHost {
|
||||||
|
# hostPath = ./hosts/pi-secondary/default.nix;
|
||||||
|
# };
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
kubectl get secret -n $1 $2 --template={{.data.$3}} | base64 -d | xclip -selection c
|
|
||||||
@@ -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"
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{ pkgs, lib, homeyConfig, ... }:
|
||||||
|
|
||||||
|
# Bootstrap image for the primary Raspberry Pi 4.
|
||||||
|
#
|
||||||
|
# Flash this image first. Its only purpose is to boot the Pi so you can:
|
||||||
|
# 1. Generate the age key: sudo age-keygen -o /var/lib/sops-nix/key.txt
|
||||||
|
# 2. Print the pubkey: sudo age-keygen -y /var/lib/sops-nix/key.txt
|
||||||
|
# 3. Add the pubkey to .sops.yaml, re-encrypt secrets, then deploy pi-main.
|
||||||
|
#
|
||||||
|
# No sops, no services, no external HD — just SSH + WiFi.
|
||||||
|
#
|
||||||
|
# WiFi PSK: uncomment and fill in before building. Do not commit the password.
|
||||||
|
# networks."YourSSID".psk = "your-wifi-password";
|
||||||
|
|
||||||
|
{
|
||||||
|
networking.hostName = "pi-main";
|
||||||
|
time.timeZone = homeyConfig.timezone;
|
||||||
|
i18n.defaultLocale = "en_US.UTF-8";
|
||||||
|
system.stateVersion = "25.05";
|
||||||
|
|
||||||
|
nix.settings = {
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
"https://nix-community.cachix.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
|
||||||
|
# linux_rpi4 is pre-built in cache.nixos.org — fetched, not compiled.
|
||||||
|
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
|
||||||
|
|
||||||
|
networking.wireless = {
|
||||||
|
enable = true;
|
||||||
|
# networks."Zakobar".psk = "your-wifi-password";
|
||||||
|
};
|
||||||
|
networking.interfaces.wlan0.ipv4.addresses = [{
|
||||||
|
address = "192.168.1.100";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
networking.useDHCP = false;
|
||||||
|
networking.interfaces.wlan0.useDHCP = false;
|
||||||
|
networking.defaultGateway = "192.168.1.1";
|
||||||
|
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
|
||||||
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "no";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.mutableUsers = false;
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.age pkgs.vim ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
{ 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
|
||||||
|
];
|
||||||
|
|
||||||
|
# linux_rpi4 is the Raspberry Pi Foundation's kernel, sourced from nixpkgs
|
||||||
|
# and pre-built in cache.nixos.org. Avoids a multi-hour native compilation.
|
||||||
|
boot.kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Identity
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
networking.hostName = "pi-main";
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# WiFi — static IP, always connect to home network
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
networking.wireless = {
|
||||||
|
enable = true;
|
||||||
|
# secretsFile is read by wpa_supplicant at runtime; values are literal
|
||||||
|
# (not env vars). The key name after "ext:" must match a line in the file
|
||||||
|
# formatted as: key_name=the-actual-password
|
||||||
|
secretsFile = config.sops.secrets."wifi/psk".path;
|
||||||
|
networks."Zakobar".pskRaw = "ext:wifi_psk";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Static IP on wlan0
|
||||||
|
networking.interfaces.wlan0.ipv4.addresses = [{
|
||||||
|
address = "192.168.1.100";
|
||||||
|
prefixLength = 24;
|
||||||
|
}];
|
||||||
|
networking.defaultGateway = "192.168.1.1";
|
||||||
|
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
|
||||||
|
|
||||||
|
# Disable DHCP on wlan0 — we're using a static address
|
||||||
|
networking.useDHCP = false;
|
||||||
|
networking.interfaces.wlan0.useDHCP = false;
|
||||||
|
|
||||||
|
# The secret file must contain exactly one line: wifi_psk=<your-password>
|
||||||
|
# Add it with: sops secrets/secrets.yaml → wifi/psk: "wifi_psk=YourPassword"
|
||||||
|
sops.secrets."wifi/psk" = { owner = "root"; mode = "0400"; };
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Admin user
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" "podman" ];
|
||||||
|
# Paste your SSH public key here
|
||||||
|
openssh.authorizedKeys.keys = [
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDfzDDO5juINctECmWlsYtGghEiX/RnTJ1cazLvOWSrPfsTyEd+B1+Ig8kFefNryjkpApfRXqj5KtLPNlpLfdVBrOIfhIveEp2MGqhgOGZFNVxQyXnZgii8Zdh4cqZ2O3pZpMsaAQBaJ9nH6dK0dJjicWT5f6TqwrVcInywRc5SuyizoSxoFmg7ch2rnlVi0j5XMVqdh8XLzHXZ7yWCzXy7+hWl/d7pwpyuzoK8dBw2EU9TauhgRDruom5Q9vWJTLStALC9pAIb0v9UFj9y+1zwx7pXsXp5F1g73EYrE4QR+QQ6z2LebuK280W0t+VA/fSCEB13DnkmofgqZQxX5MSCmrxZ5lTFp1FjW6yJo7As9FheF/GECowYkMRIx4IiQsjjHjZqlLRpLas11yAp6tGoZnw59hFo6Lu0Kva39jGVVmioYHtAeE5rD5w+v5kseJR4jlQ8aKB5yOjYUQOIz2AHQyoidgaeR2jPWqZUeRQbACI+/p3CHO45r3hrjATtGloBg0xF95Qws7Be3mjHVhbBLOoob8MdZ8nYAGnhlWrZphlkvXsHC6OUkuDJW00tmMjWXRlFwhFJ+nqUQCgLVjxVHQJ5rq9GeXBUuNXAeCm5BKBsdq+9qqVlt7D9iGyfr0lcZ7peKz/96KwPCWpG2En1Ur0/cVcbWnXEfG/xWO10tQ== cardno:24_758_470"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
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-label/homey-data";
|
||||||
|
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 = "s3:https://s3.us-east-005.backblazeb2.com/zakobar-home-backup";
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Local DNS overrides (optional — makes LAN clients hit the Pi directly
|
||||||
|
# instead of going through Cloudflare for *.zakobar.com)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# If you run Pi-hole or Adguard, add these records there instead.
|
||||||
|
# networking.extraHosts = ''
|
||||||
|
# 192.168.1.100 zakobar.com
|
||||||
|
# 192.168.1.100 auth.zakobar.com
|
||||||
|
# 192.168.1.100 git.zakobar.com
|
||||||
|
# 192.168.1.100 nextcloud.zakobar.com
|
||||||
|
# 192.168.1.100 ldapadmin.zakobar.com
|
||||||
|
# '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{ config, lib, pkgs, modulesPath, ... }:
|
||||||
|
|
||||||
|
# Hardware configuration for the primary Raspberry Pi 4 (8 GB).
|
||||||
|
#
|
||||||
|
# nixos-raspberrypi's raspberry-pi-4.base module (imported in flake.nix)
|
||||||
|
# provides everything that nixos-hardware.raspberry-pi-4 previously did:
|
||||||
|
# - linuxPackages_rpi4 vendor kernel + matching firmware
|
||||||
|
# - u-boot bootloader with /boot/firmware partition management
|
||||||
|
# - initrd modules (xhci_pci, usbhid, usb_storage, vc4, pcie_brcmstb, etc.)
|
||||||
|
# - config.txt generation
|
||||||
|
#
|
||||||
|
# This file adds only host-specific overrides on top of that.
|
||||||
|
#
|
||||||
|
# External HD:
|
||||||
|
# Set homey.storage.device to the by-id path of your USB drive.
|
||||||
|
# Find it with: ls -la /dev/disk/by-id/
|
||||||
|
#
|
||||||
|
# TODO: Verify SD card partition labels after first flash.
|
||||||
|
# The config assumes labels NIXOS_SD (root) and FIRMWARE (boot).
|
||||||
|
# Check with: lsblk -o NAME,LABEL
|
||||||
|
# Update fileSystems entries below if they differ.
|
||||||
|
|
||||||
|
{
|
||||||
|
# tmpfs for /tmp — keep the SD card writes down
|
||||||
|
boot.tmp.useTmpfs = true;
|
||||||
|
|
||||||
|
# Filesystems
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "/dev/disk/by-label/NIXOS_SD";
|
||||||
|
fsType = "ext4";
|
||||||
|
options = [ "noatime" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."/boot/firmware" = {
|
||||||
|
device = "/dev/disk/by-label/FIRMWARE";
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
# Platform
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
|
||||||
|
|
||||||
|
# Power management
|
||||||
|
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
{ 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.
|
||||||
|
#
|
||||||
|
# Backup strategy — two tiers:
|
||||||
|
#
|
||||||
|
# 1. Automatic daily backup to an S3-compatible bucket (primary offsite copy).
|
||||||
|
# Set the repository URL to your bucket in hosts/pi-main/default.nix, e.g.:
|
||||||
|
# homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket";
|
||||||
|
# S3 credentials are injected via environment variables from sops secrets:
|
||||||
|
# restic/s3_access_key_id → AWS_ACCESS_KEY_ID
|
||||||
|
# restic/s3_secret_access_key → AWS_SECRET_ACCESS_KEY
|
||||||
|
#
|
||||||
|
# 2. Manual offload to a local disk (USB drive plugged into Pi, or workstation disk).
|
||||||
|
# Use scripts/offload-backup.sh --target /path/to/mounted/disk
|
||||||
|
# That script uses `restic copy` to clone snapshots from the S3 repo into a
|
||||||
|
# local restic repo on the target disk, preserving deduplication.
|
||||||
|
#
|
||||||
|
# Secrets consumed from sops:
|
||||||
|
# restic/password
|
||||||
|
# restic/s3_access_key_id (if using S3 backend)
|
||||||
|
# restic/s3_secret_access_key (if using S3 backend)
|
||||||
|
#
|
||||||
|
# The backup repository URL is set per-host in default.nix:
|
||||||
|
# homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/bucket";
|
||||||
|
#
|
||||||
|
# Restore:
|
||||||
|
# restic -r <repo> 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"; };
|
||||||
|
sops.secrets."restic/s3_access_key_id" = { owner = "root"; };
|
||||||
|
sops.secrets."restic/s3_secret_access_key" = { 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;
|
||||||
|
|
||||||
|
# Runtime env file written by ExecStartPre (see systemd override below)
|
||||||
|
environmentFile = "/run/restic-homey-secrets.env";
|
||||||
|
|
||||||
|
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 and inject secrets
|
||||||
|
systemd.services."restic-backups-homey" = {
|
||||||
|
requires = [ "homey-backup-pre.service" ];
|
||||||
|
after = [ "homey-backup-pre.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
# Write runtime env file with actual secret values (restic needs the
|
||||||
|
# raw values; it does not support _FILE suffix env vars).
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "restic-inject-secrets" ''
|
||||||
|
install -m 0600 /dev/null /run/restic-homey-secrets.env
|
||||||
|
{
|
||||||
|
printf 'AWS_ACCESS_KEY_ID=%s\n' \
|
||||||
|
"$(cat ${config.sops.secrets."restic/s3_access_key_id".path})"
|
||||||
|
printf 'AWS_SECRET_ACCESS_KEY=%s\n' \
|
||||||
|
"$(cat ${config.sops.secrets."restic/s3_secret_access_key".path})"
|
||||||
|
printf 'RESTIC_CACHE_DIR=%s\n' "${dataDir}/restic-cache"
|
||||||
|
} >> /run/restic-homey-secrets.env
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
ExecStopPost = [
|
||||||
|
(pkgs.writeShellScript "restic-cleanup-secrets" ''
|
||||||
|
rm -f /run/restic-homey-secrets.env
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."homey-backup-post" = {
|
||||||
|
after = [ "restic-backups-homey.service" ];
|
||||||
|
wantedBy = [ "restic-backups-homey.service" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
{ config, lib, pkgs, homeyConfig, ... }:
|
||||||
|
|
||||||
|
# Caddy reverse proxy.
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
# - DNS-01 ACME via Cloudflare API → real wildcard cert for *.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 using the nixos-25.05 API.
|
||||||
|
# `withPlugins` is a passthru function on the caddy package; it uses xcaddy
|
||||||
|
# under the hood to produce a fixed-output derivation.
|
||||||
|
caddyWithCloudflare = pkgs.caddy.withPlugins {
|
||||||
|
plugins = [
|
||||||
|
# v0.2.4 tag points to commit a8737d0 which includes the fix for
|
||||||
|
# cfut_/cfat_ token format validation (PR #123).
|
||||||
|
"github.com/caddy-dns/cloudflare@v0.2.4"
|
||||||
|
];
|
||||||
|
hash = "sha256-pRrLBlYRaAyMYwPXeTy4WqWNRu/L9K6Mn2src11dGh8=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Reverse-proxy snippet for cloudflared http:// vhosts.
|
||||||
|
# Cloudflare terminates TLS; cloudflared connects to Caddy over plain HTTP.
|
||||||
|
# We must override X-Forwarded-Proto so upstream services (especially
|
||||||
|
# Authelia) know the client is actually on HTTPS.
|
||||||
|
cfProxy = port: ''
|
||||||
|
reverse_proxy localhost:${toString port} {
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# Always tell Authelia the scheme is https (cloudflared terminates TLS
|
||||||
|
# externally; Caddy's http:// vhosts are only for the tunnel loopback).
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Each service gets two vhost entries:
|
||||||
|
# - "host" (no scheme) → Caddy handles HTTPS + auto cert (for LAN access)
|
||||||
|
# - "http://host" → plain HTTP for cloudflared on loopback (no redirect)
|
||||||
|
#
|
||||||
|
# Caddy auto-redirects HTTP→HTTPS only when no explicit http:// vhost exists.
|
||||||
|
# By defining http:// explicitly we suppress that redirect so cloudflared
|
||||||
|
# (which talks plain HTTP on port 80) gets a direct response.
|
||||||
|
virtualHosts = {
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Authelia — public, no auth gate (it IS the auth gate)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"auth.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
reverse_proxy localhost:9091
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"http://auth.${domain}" = {
|
||||||
|
extraConfig = cfProxy 9091;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Gitea — no forward_auth; git HTTP clients can't handle SSO redirects.
|
||||||
|
# Access control is handled by Gitea itself (LDAP auth + private repos).
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"git.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"http://git.${domain}" = {
|
||||||
|
extraConfig = cfProxy 3000;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Nextcloud — public auth (Nextcloud manages its own users + LDAP)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"http://nextcloud.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
redir /.well-known/carddav /remote.php/dav/ 301
|
||||||
|
redir /.well-known/caldav /remote.php/dav/ 301
|
||||||
|
request_body {
|
||||||
|
max_size 5GB
|
||||||
|
}
|
||||||
|
reverse_proxy localhost:8080 {
|
||||||
|
header_up X-Forwarded-Proto https
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# phpLDAPadmin — two_factor, admins only (enforced by authelia policy)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"ldapadmin.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
reverse_proxy localhost:8081
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"http://ldapadmin.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
${cfProxy 8081}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Jellyfin — no forward_auth; Jellyfin has its own login UI and
|
||||||
|
# native app clients can't handle SSO redirects.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"jellyfin.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
reverse_proxy localhost:8096
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"http://jellyfin.${domain}" = {
|
||||||
|
extraConfig = cfProxy 8096;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Transmission — two_factor, admins only (enforced by authelia policy)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
"torrent.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
reverse_proxy localhost:9092
|
||||||
|
'';
|
||||||
|
# NOTE: transmission is bound to 9092 to avoid clash with authelia on 9091.
|
||||||
|
};
|
||||||
|
"http://torrent.${domain}" = {
|
||||||
|
extraConfig = ''
|
||||||
|
${autheliaForwardAuth}
|
||||||
|
${cfProxy 9092}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Pass Cloudflare token as env var to the caddy systemd unit.
|
||||||
|
#
|
||||||
|
# The caddy-dns/cloudflare plugin reads CLOUDFLARE_API_TOKEN directly.
|
||||||
|
# sops decrypts the secret to a file at runtime; we write a transient
|
||||||
|
# env file to /run/ in ExecStartPre so systemd picks it up via
|
||||||
|
# EnvironmentFile. The file is removed in ExecStopPost.
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
systemd.services.caddy = {
|
||||||
|
serviceConfig = {
|
||||||
|
# LoadCredential stages the sops-decrypted secret into a
|
||||||
|
# per-invocation directory ($CREDENTIALS_DIRECTORY) before any
|
||||||
|
# Exec* step. ExecStart then reads the file contents and exports
|
||||||
|
# CLOUDFLARE_API_TOKEN before exec-ing caddy, so there is no
|
||||||
|
# intermediate env file and no ordering race with EnvironmentFile.
|
||||||
|
LoadCredential = "cloudflare_api_token:${config.sops.secrets."cloudflare/api_token".path}";
|
||||||
|
# Systemd requires clearing ExecStart= before setting a new value for
|
||||||
|
# non-oneshot services. The empty string resets the list; the second
|
||||||
|
# entry is the actual start command.
|
||||||
|
ExecStart = lib.mkForce [
|
||||||
|
""
|
||||||
|
(pkgs.writeShellScript "caddy-start" ''
|
||||||
|
export CLOUDFLARE_API_TOKEN=$(cat "$CREDENTIALS_DIRECTORY/cloudflare_api_token")
|
||||||
|
exec ${caddyWithCloudflare}/bin/caddy run --environ --config /etc/caddy/caddy_config --adapter caddyfile
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
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 ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{ 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.zakobar.com → http://localhost:80 (or https://localhost:443)
|
||||||
|
# git.zakobar.com → https://localhost:443
|
||||||
|
# nextcloud.zakobar.com → https://localhost:443
|
||||||
|
# ldapadmin.zakobar.com → https://localhost:443
|
||||||
|
# jellyfin.zakobar.com → https://localhost:443
|
||||||
|
# torrent.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
|
||||||
|
#
|
||||||
|
# We use the token-based tunnel approach (cloudflared tunnel run --token).
|
||||||
|
# This needs no credentials file and no local tunnel config — just the
|
||||||
|
# token from the Cloudflare dashboard.
|
||||||
|
#
|
||||||
|
# Rather than using services.cloudflared.tunnels (which requires a
|
||||||
|
# credentialsFile), we create a plain systemd service that runs cloudflared
|
||||||
|
# directly with the token read from the sops secret.
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
users.users.cloudflared = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "cloudflared";
|
||||||
|
description = "cloudflared tunnel daemon";
|
||||||
|
};
|
||||||
|
users.groups.cloudflared = {};
|
||||||
|
|
||||||
|
systemd.services."cloudflared-tunnel" = {
|
||||||
|
description = "Cloudflare Tunnel (token-based)";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" "caddy.service" ];
|
||||||
|
wants = [ "network-online.target" "caddy.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = "cloudflared";
|
||||||
|
Group = "cloudflared";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
ExecStart = pkgs.writeShellScript "cloudflared-start" ''
|
||||||
|
exec ${pkgs.cloudflared}/bin/cloudflared tunnel \
|
||||||
|
--no-autoupdate \
|
||||||
|
run \
|
||||||
|
--token "$(cat ${config.sops.secrets."cloudflare/tunnel_token".path})"
|
||||||
|
'';
|
||||||
|
# Hardening
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{ 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/<name>/hardware.nix instead.
|
||||||
|
|
||||||
|
{
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Nix / flakes
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
nix = {
|
||||||
|
settings = {
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
auto-optimise-store = true;
|
||||||
|
# Extra binary caches — speeds up aarch64-linux builds significantly
|
||||||
|
substituters = [
|
||||||
|
"https://cache.nixos.org"
|
||||||
|
"https://nix-community.cachix.org"
|
||||||
|
];
|
||||||
|
trusted-public-keys = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
"nix-community.cachix.org-1:mB9FkXj6Q3Q4ohOcbM4FJ9Z1X2kCrVK4vZOqsDqqNqk="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
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/<name>/default.nix
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
users.mutableUsers = false; # all user config must be declared here
|
||||||
|
|
||||||
|
# The actual admin user is declared in hosts/<name>/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 = "25.05";
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
# <dataDir>/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: zakobar.com → 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;
|
||||||
|
|
||||||
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
|
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" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
{ 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:
|
||||||
|
# <dataDir>/gitea/data/ → /data (repos, sqlite db, avatars, lfs, etc.)
|
||||||
|
#
|
||||||
|
# Configuration strategy: all settings are passed as GITEA__<SECTION>__<KEY>
|
||||||
|
# environment variables. Gitea writes its own app.ini into /data/gitea/conf/
|
||||||
|
# on first start; the env vars override every key at runtime without touching
|
||||||
|
# that file. This avoids the bind-mount / read-only-fs problem where Gitea
|
||||||
|
# needs to rewrite its own config file on startup.
|
||||||
|
#
|
||||||
|
# Non-secret settings go in the `environment` block (they are fine in the
|
||||||
|
# Nix store). Secret settings go into /run/gitea-secrets.env via ExecStartPre
|
||||||
|
# (never in the store).
|
||||||
|
#
|
||||||
|
# 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;
|
||||||
|
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"; };
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Container
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
virtualisation.oci-containers.containers.gitea = {
|
||||||
|
image = cfg.image;
|
||||||
|
# No ports mapping — --network=host means the container shares the host
|
||||||
|
# network stack directly. Gitea binds to 0.0.0.0:3000 on the host.
|
||||||
|
|
||||||
|
# All non-secret settings via GITEA__<SECTION>__<KEY> env vars.
|
||||||
|
# These are safe to store in the Nix store.
|
||||||
|
environment = {
|
||||||
|
USER_UID = "1000";
|
||||||
|
USER_GID = "1000";
|
||||||
|
GITEA_CUSTOM = "/data/gitea";
|
||||||
|
|
||||||
|
# [DEFAULT]
|
||||||
|
GITEA____APP_NAME = homeyConfig.organization;
|
||||||
|
GITEA____RUN_MODE = "prod";
|
||||||
|
|
||||||
|
# [repository]
|
||||||
|
GITEA__repository__ROOT = "/data/git/repositories";
|
||||||
|
|
||||||
|
# [server]
|
||||||
|
GITEA__server__APP_DATA_PATH = "/data/gitea";
|
||||||
|
GITEA__server__DOMAIN = "git.${domain}";
|
||||||
|
GITEA__server__HTTP_PORT = toString cfg.port;
|
||||||
|
GITEA__server__ROOT_URL = "https://git.${domain}/";
|
||||||
|
GITEA__server__DISABLE_SSH = "true";
|
||||||
|
GITEA__server__START_SSH_SERVER = "false";
|
||||||
|
GITEA__server__SSH_PORT = "2222";
|
||||||
|
GITEA__server__SSH_LISTEN_PORT = "2222";
|
||||||
|
GITEA__server__LFS_START_SERVER = "true";
|
||||||
|
GITEA__server__OFFLINE_MODE = "false";
|
||||||
|
|
||||||
|
# [lfs]
|
||||||
|
GITEA__lfs__PATH = "/data/git/lfs";
|
||||||
|
|
||||||
|
# [database]
|
||||||
|
GITEA__database__DB_TYPE = "sqlite3";
|
||||||
|
GITEA__database__PATH = "/data/gitea/gitea.db";
|
||||||
|
GITEA__database__LOG_SQL = "false";
|
||||||
|
|
||||||
|
# [indexer]
|
||||||
|
GITEA__indexer__ISSUE_INDEXER_PATH = "/data/gitea/indexers/issues.bleve";
|
||||||
|
|
||||||
|
# [session]
|
||||||
|
GITEA__session__PROVIDER = "file";
|
||||||
|
GITEA__session__PROVIDER_CONFIG = "/data/gitea/sessions";
|
||||||
|
|
||||||
|
# [picture]
|
||||||
|
GITEA__picture__AVATAR_UPLOAD_PATH = "/data/gitea/avatars";
|
||||||
|
GITEA__picture__REPOSITORY_AVATAR_UPLOAD_PATH = "/data/gitea/repo-avatars";
|
||||||
|
GITEA__picture__DISABLE_GRAVATAR = "false";
|
||||||
|
|
||||||
|
# [attachment]
|
||||||
|
GITEA__attachment__PATH = "/data/gitea/attachments";
|
||||||
|
|
||||||
|
# [log]
|
||||||
|
GITEA__log__MODE = "console";
|
||||||
|
GITEA__log__LEVEL = "info";
|
||||||
|
GITEA__log__ROOT_PATH = "/data/gitea/log";
|
||||||
|
|
||||||
|
# [security]
|
||||||
|
GITEA__security__INSTALL_LOCK = "true";
|
||||||
|
GITEA__security__REVERSE_PROXY_LIMIT = "1";
|
||||||
|
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES = "*";
|
||||||
|
|
||||||
|
# [service]
|
||||||
|
GITEA__service__DISABLE_REGISTRATION = "true";
|
||||||
|
GITEA__service__REQUIRE_SIGNIN_VIEW = "false";
|
||||||
|
GITEA__service__REGISTER_EMAIL_CONFIRM = "false";
|
||||||
|
GITEA__service__ENABLE_NOTIFY_MAIL = "false";
|
||||||
|
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION = "true";
|
||||||
|
GITEA__service__ENABLE_CAPTCHA = "false";
|
||||||
|
GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION = "true";
|
||||||
|
GITEA__service__DEFAULT_ENABLE_TIMETRACKING = "true";
|
||||||
|
GITEA__service__NO_REPLY_ADDRESS = "noreply.localhost";
|
||||||
|
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION = "true";
|
||||||
|
GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = "true";
|
||||||
|
|
||||||
|
# [mailer]
|
||||||
|
GITEA__mailer__ENABLED = "false";
|
||||||
|
|
||||||
|
# [openid]
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNIN = "false";
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNUP = "false";
|
||||||
|
|
||||||
|
# [oauth2]
|
||||||
|
GITEA__oauth2__ENABLED = "false";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Secret env vars written at runtime by ExecStartPre — never in store.
|
||||||
|
environmentFiles = [ "/run/gitea-secrets.env" ];
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"${dataDir}/gitea/data:/data"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [ "--network=host" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# ExecStartPre: write ephemeral secrets env file
|
||||||
|
# ExecStopPost: clean it up
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
systemd.services."podman-gitea" = {
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "gitea-write-secrets" ''
|
||||||
|
set -euo pipefail
|
||||||
|
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})
|
||||||
|
printf '%s\n' \
|
||||||
|
"GITEA__server__LFS_JWT_SECRET=$LFS" \
|
||||||
|
"GITEA__security__INTERNAL_TOKEN=$TOKEN" \
|
||||||
|
"GITEA__oauth2__JWT_SECRET=$OAUTH" \
|
||||||
|
> /run/gitea-secrets.env
|
||||||
|
chmod 600 /run/gitea-secrets.env
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
ExecStopPost = [
|
||||||
|
(pkgs.writeShellScript "gitea-cleanup-secrets" ''
|
||||||
|
rm -f /run/gitea-secrets.env
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
after = lib.mkAfter [ "mnt-data.mount" "podman-openldap.service" ];
|
||||||
|
requires = lib.mkAfter [ "mnt-data.mount" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Ensure the Gitea admin user exists with the correct password after start.
|
||||||
|
# Runs as a oneshot after podman-gitea; idempotent (create or update).
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
systemd.services."gitea-admin-setup" = {
|
||||||
|
description = "Ensure Gitea admin user exists with correct password";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "podman-gitea.service" ];
|
||||||
|
requires = [ "podman-gitea.service" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
PASS=$(cat ${config.sops.secrets."gitea/admin_password".path})
|
||||||
|
|
||||||
|
# Wait until Gitea's HTTP endpoint is up (max 60 s)
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if ${pkgs.curl}/bin/curl -sf http://127.0.0.1:${toString cfg.port}/ -o /dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync password if admin exists; create if not.
|
||||||
|
if ! ${pkgs.podman}/bin/podman exec -u 1000 gitea \
|
||||||
|
gitea admin user change-password --username admin --password "$PASS" 2>/dev/null; then
|
||||||
|
${pkgs.podman}/bin/podman exec -u 1000 gitea \
|
||||||
|
gitea admin user create \
|
||||||
|
--username admin \
|
||||||
|
--password "$PASS" \
|
||||||
|
--email "admin@${domain}" \
|
||||||
|
--admin
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{ config, lib, pkgs, homeyConfig, ... }:
|
||||||
|
|
||||||
|
# Jellyfin — media server. (Deferred — enable when ready.)
|
||||||
|
#
|
||||||
|
# Volume layout:
|
||||||
|
# <dataDir>/jellyfin/config/ → /config
|
||||||
|
# <dataDir>/media/movies/ → /data/movies
|
||||||
|
# <dataDir>/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;
|
||||||
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
|
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" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
{ 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:
|
||||||
|
# <dataDir>/nextcloud/db/ → /var/lib/postgresql/data (postgres)
|
||||||
|
# <dataDir>/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;
|
||||||
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
POSTGRES_DB = "nextcloud_db";
|
||||||
|
POSTGRES_USER = "postgres";
|
||||||
|
# Password injected via env file
|
||||||
|
};
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"${dataDir}/nextcloud/db:/var/lib/postgresql/data"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
"--env-file=/run/nc-postgres-secrets.env"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."podman-nextcloud-postgres" = {
|
||||||
|
serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
|
||||||
|
];
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "nc-postgres-secrets-env" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 600 /dev/null /run/nc-postgres-secrets.env
|
||||||
|
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" \
|
||||||
|
>> /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;
|
||||||
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
|
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}";
|
||||||
|
# With --network=host, port mappings are ignored and the container's
|
||||||
|
# Apache binds directly on the host. Force it onto port 8080 so Caddy
|
||||||
|
# can own 80/443.
|
||||||
|
APACHE_HTTP_PORT_NUMBER = toString cfg.port;
|
||||||
|
# Passwords injected via env file
|
||||||
|
};
|
||||||
|
|
||||||
|
volumes = [
|
||||||
|
"${dataDir}/nextcloud/html:/var/www/html"
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
"--env-file=/run/nc-secrets.env"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."podman-nextcloud" = {
|
||||||
|
serviceConfig = {
|
||||||
|
LoadCredential = [
|
||||||
|
"nextcloud_postgres_password:${config.sops.secrets."nextcloud/postgres_password".path}"
|
||||||
|
"nextcloud_admin_password:${config.sops.secrets."nextcloud/admin_password".path}"
|
||||||
|
];
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "nc-secrets-env" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 600 /dev/null /run/nc-secrets.env
|
||||||
|
echo "POSTGRES_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_postgres_password")" >> /run/nc-secrets.env
|
||||||
|
echo "NEXTCLOUD_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/nextcloud_admin_password")" >> /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" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{ 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:
|
||||||
|
# <dataDir>/openldap/etc-ldap-slapd.d/ → /etc/ldap/slapd.d (config DB)
|
||||||
|
# <dataDir>/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;
|
||||||
|
|
||||||
|
# No ports mapping — --network=host means the container shares the host
|
||||||
|
# network stack. OpenLDAP binds to 0.0.0.0:389, but the firewall
|
||||||
|
# (common.nix) only opens 22/80/443, so port 389 is unreachable from
|
||||||
|
# the LAN or internet.
|
||||||
|
|
||||||
|
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"
|
||||||
|
"--env-file=/run/openldap-secrets.env"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Systemd override to inject sops secrets as env vars
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# podman containers are managed by systemd units named
|
||||||
|
# podman-<container-name>.service
|
||||||
|
systemd.services."podman-openldap" = {
|
||||||
|
serviceConfig = {
|
||||||
|
# LoadCredential stages the sops secrets into a per-invocation
|
||||||
|
# credential directory before any Exec* step, so they are available
|
||||||
|
# when ExecStartPre runs. ExecStartPre writes the env file that
|
||||||
|
# podman --env-file reads; this avoids the EnvironmentFile ordering
|
||||||
|
# race (EnvironmentFile is evaluated before ExecStartPre).
|
||||||
|
LoadCredential = [
|
||||||
|
"openldap_admin_password:${config.sops.secrets."openldap/admin_password".path}"
|
||||||
|
"openldap_config_password:${config.sops.secrets."openldap/config_password".path}"
|
||||||
|
"openldap_ro_password:${config.sops.secrets."openldap/ro_password".path}"
|
||||||
|
];
|
||||||
|
ExecStartPre = [
|
||||||
|
(pkgs.writeShellScript "openldap-secrets-env" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -m 600 /dev/null /run/openldap-secrets.env
|
||||||
|
echo "LDAP_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_admin_password")" >> /run/openldap-secrets.env
|
||||||
|
echo "LDAP_CONFIG_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_config_password")" >> /run/openldap-secrets.env
|
||||||
|
echo "LDAP_READONLY_USER_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/openldap_ro_password")" >> /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
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# No firewall rule needed; common.nix only opens 22/80/443.
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{ 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.
|
||||||
|
#
|
||||||
|
# Networking: uses default bridge (podman) network with a port mapping
|
||||||
|
# 127.0.0.1:8081->80 so Caddy can reach it. OpenLDAP runs on the host
|
||||||
|
# network at 127.0.0.1:389; the container reaches it via the special
|
||||||
|
# host.containers.internal DNS name that podman injects automatically.
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
PHPLDAPADMIN_HTTPS = "false";
|
||||||
|
# host.containers.internal resolves to the host from inside a podman
|
||||||
|
# bridge container — reaches openldap which is on --network=host at :389
|
||||||
|
PHPLDAPADMIN_LDAP_HOSTS = "host.containers.internal";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Bridge network (default) + port mapping: Apache binds inside the
|
||||||
|
# container on :80, podman maps it to 127.0.0.1:8081 on the host.
|
||||||
|
ports = [ "127.0.0.1:${toString cfg.port}:80" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."podman-phpldapadmin" = {
|
||||||
|
after = lib.mkAfter [ "podman-openldap.service" ];
|
||||||
|
wants = lib.mkAfter [ "podman-openldap.service" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{ 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:
|
||||||
|
# <dataDir>/transmission/config/ → /config
|
||||||
|
# <dataDir>/media/movies/ → /downloads/movies
|
||||||
|
# <dataDir>/media/tvshows/ → /downloads/tvshows
|
||||||
|
# <dataDir>/media/general/ → /downloads/general
|
||||||
|
# <dataDir>/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;
|
||||||
|
# No ports mapping — --network=host shares the host network stack directly.
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
PUID = "1000";
|
||||||
|
PGID = "1000";
|
||||||
|
# With --network=host, port mappings are ignored; transmission binds
|
||||||
|
# directly on the host. Force it to cfg.port (9092) to avoid
|
||||||
|
# conflicting with Authelia on 9091.
|
||||||
|
TRANSMISSION_WEB_HOME = "/usr/share/transmission/web";
|
||||||
|
WEBUI_PORT = toString cfg.port;
|
||||||
|
};
|
||||||
|
|
||||||
|
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" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 <mountPoint>/<service-name>/, 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 1000 1000 -"
|
||||||
|
"d ${cfg.mountPoint}/gitea/data 0750 1000 1000 -"
|
||||||
|
"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 -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 <pvc-name-or-friendly-name>"
|
|
||||||
echo ""
|
|
||||||
echo "To restore a volume, run:"
|
|
||||||
echo " ./scripts/restore-longhorn-volume.sh <pvc-name-or-friendly-name>"
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <name>"
|
|
||||||
echo " Restore: ./scripts/restore-longhorn-volume.sh <name>"
|
|
||||||
@@ -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]} <backup_dir> <mount_point>")
|
|
||||||
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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 <pvc-name-or-friendly-name> [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/<name>)"
|
|
||||||
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 <mount-point>"
|
|
||||||
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
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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"
|
|
||||||
@@ -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 <pvc-name-or-friendly-name> [--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"
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
openldap:
|
||||||
|
admin_password: ENC[AES256_GCM,data:hg+Ly1bX4ao1AT4SDvQWXiT/KMzsz0wdnRauiB+FetE=,iv:TAX+NZCVUNiwMeBrW58IeI1OJX6rzzGAhWiQ+cZXreo=,tag:MrwYKKBb1Cg2JvADtQqYrQ==,type:str]
|
||||||
|
config_password: ENC[AES256_GCM,data:qKEurb0slGnr6nES7w7fTPDCy/DARns0BorDZMwpI/w=,iv:+p6Fh9a2g0eBueOxDk1J+hnM9fMgE6/NYwz+sAovGjE=,tag:kKZVsxdxdDACD9J0NAf4gQ==,type:str]
|
||||||
|
ro_password: ENC[AES256_GCM,data:82htWXdJ07tdZ81o7o9a4hizxcn39yQidD5e9PijVpo=,iv:fDT1chX4ZPIS02IMEW02haPa2IIlLFhgFOpUwD7KL50=,tag:9pSo/3vFikKQAe8jS+3Q6A==,type:str]
|
||||||
|
authelia:
|
||||||
|
jwt_secret: ENC[AES256_GCM,data:SwXd/mMsrgXItP8QZr4z9YaN1lgSSO4Cpdwl+XxFj6I=,iv:gAkyHKP5D5RGJ3X3hoh8oEJfYaFnvYxGAoKxe+G1N0I=,tag:NlfWC2pmjCYiiRn9prgl2w==,type:str]
|
||||||
|
session_secret: ENC[AES256_GCM,data:bbbgmxYLbtteuT628O+uSVeo7Gx3hI6uWVIV5l8AhtNSvXBHKb9i2NvQqJfVG8D2YR00+OW9XtreocpBuS7YJKp58cP/EgrF9x1im/8UaaifAYqD8YXZReUsBw+/PzIvqyA1K9tFd0coc8tSHJmwTwea4sf6Tc10N6j/nhNQfpQ=,iv:wrGE8XVsTINmf5505XVI7HHqA33w+SBh9lsJXD+YnwU=,tag:lvSoU+IRy8f+6VpYqzvacg==,type:str]
|
||||||
|
storage_encryption_key: ENC[AES256_GCM,data:8KnaWBTlSStdC/uI4GUOYP9DJygjfCTu62zWTU9eeVE=,iv:DNl0L2QgT1lNUDdPNm9bXcGvrLXLDtWdJ9pPgRH20C8=,tag:hVhcCl0Vrt0ZnaVGaUokSQ==,type:str]
|
||||||
|
gitea:
|
||||||
|
admin_password: ENC[AES256_GCM,data:WncwJlqb/3X5WZYgIXAu0niI0ISP0eHfmhsKDLeAvE4=,iv:JBZNZJRSHKG+cCoFNJBI+jS+/WcLueqJ/UN/7wXfK/0=,tag:f6+Lhfcxstq6pwS9xkVwqQ==,type:str]
|
||||||
|
lfs_jwt_secret: ENC[AES256_GCM,data:i05gr2ou03w0yu6/bhlJOW1huysAAPTidFEusWkhQfpDj4Pyh8LEKb09Og==,iv:aqkblyz0oIFHwzVCzlGDdQuCbsDPrfBaJMzgRTw+pYU=,tag:6gBSerOUK8Y3la/2Bg2AZQ==,type:str]
|
||||||
|
oauth2_jwt_secret: ENC[AES256_GCM,data:BVvQJCEfHPbemd1jz7MWpIRia1wfvPMGuLqoi/xUMSoQoN5RPefQnPR4Cg==,iv:JAZQUTxHZSnMEnl+BIZ1PXlznMwKuPtiPP/17rc6lSs=,tag:mUw5RuthZmZegXCtfsFNmQ==,type:str]
|
||||||
|
internal_token: ENC[AES256_GCM,data:gnOebJbRsh2Cues9WjGQp4rWa6OuE3xSnby9jc3Hk8ywvpL7CNWmlGW7zmmOyDAfIKfm8kf1FxotWLXtGZDretzdbMRM9c6gkwSJf5MCsdm27Er+IRKS/QFBuvLSTEH0,iv:aVgRvs3T3zCg+AV/BKUXQyZDKvunHvXsdfr9sqo2cI0=,tag:U9aP+N0CWyRQ/xJ27Vo2mw==,type:str]
|
||||||
|
nextcloud:
|
||||||
|
admin_password: ENC[AES256_GCM,data:iK6VoE94vFQmn3i4XQc5r/c03u3b0knDgBNK8d1qyns=,iv:P1wax2vAjn9iwBe9T7SN+pKrtrWcOYb5OWUyHF4hlVg=,tag:ET8KU4IKzhWqIDeRihwcag==,type:str]
|
||||||
|
postgres_password: ENC[AES256_GCM,data:ga4cwhYsAgEBvr+aDVwiRZXeT+TjXzeef1r3ud6uYHs=,iv:PMHCjO4wLW6PER4oGODEG9CHqrvVpAbgTGF7p49MCL0=,tag:mTNzsDhufqLlf1LFu7Rl1A==,type:str]
|
||||||
|
cloudflare:
|
||||||
|
api_token: ENC[AES256_GCM,data:Erzom4DKiam9SHGLdT3CQRkuT5kkhcuUaLwTbt2P5pPjr1V56p733KB1kHheO/PZ+TRsZg0=,iv:eO+ryffyoSkzAgUXe0MH+FKitgHCQ3ychLWEAShAd9o=,tag:2Z8Io0ylpAI9rws5NXCvIw==,type:str]
|
||||||
|
tunnel_token: ENC[AES256_GCM,data:AFlD990L8l1Rh9i8wdyXwwyolrlw2ln1uuyCTiT7k1FVc2JjTOrgc8HiBIpxM40eqFGEnzDMs87tgzh1Pl8UThwV7WcLFWHMvtYHNez/F2+THknnW+ZinJbZnNSngicrhRIoNFhjQgjaR1LaS4kcNbkGBi655bl2uqNXoUpNThUKGAlZ4KwByEzK4B7QtCgAkqxQEFehtjdj41p8r58ViJTogXQKXmDYLjbqo8nPmUGSaiR/VCY7Gw==,iv:q755yc6wTMCuqHLvfHOZzBf3KoG4vcw431stQA78Bjs=,tag:Zt/LPG41HxSg2gSQOTC/6A==,type:str]
|
||||||
|
restic:
|
||||||
|
password: ENC[AES256_GCM,data:X/pWmwakzQzRpSaY+T/kOqdrtXyvGPa3UQc/iIQFFAzUS1jHR9IvzW7KYdm/3IKXPBlZzPkWDsoRvVAChPwfQA==,iv:4+RD9UD5daMP04ixeagxbCNkTdOPx+BqfSOheh88OUU=,tag:SmrIz7jvbM74y5RBX0nCbA==,type:str]
|
||||||
|
s3_access_key_id: ENC[AES256_GCM,data:XxElPQF28ThfYuiF4jQu7BiS8sh+c4V4ng==,iv:aLUIYnRGqFLYwlP3nFwDY5uvy8pXtX5QMKLMfRTxdNk=,tag:CscNNWnpvLuxw1DPK92GyA==,type:str]
|
||||||
|
s3_secret_access_key: ENC[AES256_GCM,data:9ZWyhGJm4t2benDrLmnyQ9ZA5Jjl6l+pza1VmymTlw==,iv:xYsG6QlxXhQNO9szmsycxP6lT0cFF7lq3iNg6j+ED0E=,tag:wOJT4Vg3DuNFWTtx3QS9IQ==,type:str]
|
||||||
|
wifi:
|
||||||
|
psk: ENC[AES256_GCM,data:znk9Wr+vsntzbJ3H0TORUrAiDw==,iv:wbl8fUuKlgTqhajwjlTgFS7ijaTwXBFPRW2AmtiTklg=,tag:IK4oe8cJcccPaQ0V0NlncQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- recipient: age120j8ty7nn04l3s3kgph5ty3v9g4e52fknn8xtnmzwakq9nv2la3skgte0p
|
||||||
|
enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZSGpPdTBIaTZ0TER2NkNO
|
||||||
|
U2ZPKzNwelJHUEpyU2VBSmd5Yjd5bEtibFZzCjlZZTRFa2FHN1JtK2JUSm51a3By
|
||||||
|
QmFyV1ZZNWI0OGJVM1NNZERjd2hWcDAKLS0tIG9VSVFTSTJBMjk5ZzBSL0ZQV2Ev
|
||||||
|
QXVkRlJHeW52NFZFYnVwaW8ycytDSzAKZt+p5QnZKcEOBghHA2xkH6d7NObtTEoE
|
||||||
|
wMwCYasnBHzy2unXRbZq/4v9NQ5HJd0Nu1iqbqKgIxMCD3dnxEdK7g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
lastmodified: "2026-04-21T12:42:15Z"
|
||||||
|
mac: ENC[AES256_GCM,data:fNip/7A7iKCVZqP0EziyBG7K8SVfRJTBpn4RcDLOaciJHx5DkLLszE8we9MmzpKXQIiMcJl2BTj/uqJrgc5EHTSOHwRzNJ4s2NJfvQW+8QUDfTGzKOkP3L837RkEPzH4HZLqGlfYK7cNJU5qXRPbusKjAft7Fz3+ONXmodb/ONY=,iv:CdSs1a+74+MfzWyML2JQ/b2IKbktVdefFFYP5LOtUos=,tag:ikr9LwPnmdiPucOoBt3/Bw==,type:str]
|
||||||
|
pgp:
|
||||||
|
- created_at: "2026-04-21T06:39:49Z"
|
||||||
|
enc: |-
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
|
||||||
|
hQIMAwdqopdXmgBkAQ//TzlOz/QYwiYAc6NGo2O8YJi5ERkS1+0qNpptD51g2dLF
|
||||||
|
V4iUx7400tc6IEEhZ0N54R7AO5mSX55XCWJxVQDTRJXmLDHcOR+9vThb4H571XBa
|
||||||
|
3mcmE8Dj3sN3a1K2RwajZJXl1o5d1oNvWJ83pVsCnrJegi92+GmvmOt4QZ1l5aCf
|
||||||
|
TGYgUXAz1RreqsGKjJsSXscZOvRnp+cslJ9xY8OXeKLbQvLg0Z3pSQG2QGgDmHPD
|
||||||
|
fRxYnlc2lKe32uoBlD2LXK+NoBnrRYEVrrwGf6P5GpTDpJbc0bR5BiRIYDhPxtqK
|
||||||
|
SiXWHaebg73+kbWdcm+2kiac6hW6xW/iJL4eFBT1v/NgZmNoQCnJOIA7v2vjv9vl
|
||||||
|
81Y1FM5MpIfwNiTwkJjVsgM2tHkANlbixBHJdbjlnKpo9pTS7RuttWtdCmFdmXr0
|
||||||
|
oiuKDDRPVGvykPqvHzvCLf/k5j1nYvqvb7Wn2Bycc5kIOjFYEDEeM0r37vOX9nDM
|
||||||
|
SW1HtaWoZuVceTJEit0WR63kmXYLZ/AHvXcmq6ucUw8Fmw79n+7brQiX2RMtCK1E
|
||||||
|
pfrNey3EEqvPs2RDd6XdF4/73CdMDN5s3xiFAIfLGeZ6h0Eq27fazSZNmdh4MGYb
|
||||||
|
Wzj81ur8dimoSP+W9eW1TjIfY4deH5FRnN19ldKPuHdazvikWWsdN05evNlSZsDS
|
||||||
|
XgHafkhKiNSNZLw/VVzf+1SDLhN1H5QoxZ2YsxCc+psd5CFxU1x3llIDg4hXScAR
|
||||||
|
OQvRR1VjQOLFCwdFErW7sd6nQlkS7LnAskgT/0ZJGsxfkh1gJO3YqDnEKF7+P9w=
|
||||||
|
=zKa+
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
|
fp: 076AA297579A0064
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.12.2
|
||||||
@@ -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 -}}
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 }}
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
# ---
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
---
|
|
||||||
-65
@@ -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
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user