Compare commits

..

63 Commits

Author SHA1 Message Date
10fb8e88ce fix: change click and load package name 2025-12-10 13:36:33 +01:00
5f300d9e7b feat: nb initial click and load 2025-12-10 12:49:30 +01:00
99ac2ea3b0 feat: nas move archive extraction to filebot script 2025-12-10 11:40:19 +01:00
2caa36c0ab fix: nas filebot and add extraction passwords 2025-12-10 11:29:05 +01:00
55c15c790d fix: fw lightning 2025-12-10 11:28:08 +01:00
450d9d6457 fix: claude.md update 2025-12-07 12:59:43 +01:00
e08bf42eaa fix: nas leds and disks 2025-12-07 12:59:27 +01:00
8e0e5c0d16 feat: add disks to monitoring 2025-12-05 21:57:58 +01:00
ada9db7942 feat: add disks to nas 2025-12-04 15:01:47 +01:00
5995612407 fix: amzebs add mysql port 2025-12-04 12:46:28 +01:00
5762916970 feat: add mcp server 2025-12-04 11:39:17 +01:00
dd456eab69 feat: update pyload 2025-12-04 11:39:08 +01:00
18a8fde66e feat: add uv for mcp 2025-12-04 11:38:22 +01:00
f97c9185c1 docs: update claude 2025-12-02 11:28:05 +01:00
8bf4b185a1 feat(nas): update to 25.11, add software, add storage plan 2025-12-02 11:27:52 +01:00
8424d771f6 feat(fw): update to 25.11 2025-12-02 08:34:57 +01:00
840f99a7e9 feat(amzebs-01): update to 25.11 2025-12-01 23:02:35 +01:00
1b27bafd41 feat(web-arm): update to 25.11
- Migrate logind.extraConfig to logind.settings.Login
- Update dovecot alert for service rename (dovecot2 → dovecot)
- Fix sa-core buildGoModule env attribute for CGO_ENABLED
2025-12-01 22:48:02 +01:00
4770d671c0 docs: add commit footer convention to CLAUDE.md 2025-12-01 22:25:08 +01:00
28a7bed3b9 feat(mail): update to 25.11 with TLS hardening
- Upgrade NixOS channel from 25.05 to 25.11
- Fix dovecot systemd service rename (dovecot2 -> dovecot)
- Convert postfix numeric settings to integers (25.11 requirement)
- Remove insecure 512-bit DH params, fix 2048-bit DH params
- Update postfix ciphers to modern ECDHE/DHE+AESGCM/CHACHA20
- Require TLS 1.2 minimum for OpenLDAP
- Remove weak ciphers (3DES, RC4, aNULL) from OpenLDAP
2025-12-01 22:24:57 +01:00
170becceb0 fix: nvim 2025-12-01 22:05:24 +01:00
6e8f530537 feat: amz add cron job 2025-12-01 16:17:45 +01:00
209bafd70f feat: test-configuration script get real errors 2025-12-01 16:17:28 +01:00
1d182437db feat: nb update to 25.11 2025-12-01 16:17:10 +01:00
6c046a549e feat: change pushover emergency on alerts 2025-12-01 13:29:37 +01:00
0a30a2ac23 fix: ai-mailer restart on secret change 2025-12-01 13:29:26 +01:00
82c15e8d26 feat: pyload config change, cyberghost change 2025-11-30 19:53:13 +01:00
f277d089bd feat: add cyberghost module 2025-11-30 19:29:33 +01:00
7ed345b8e8 feat: add pyload extraction passwords 2025-11-30 19:29:24 +01:00
bd6b15b617 changes 2025-11-29 22:42:09 +01:00
3282b7d634 fix: monitoring 2025-11-29 22:42:00 +01:00
21ed381d18 fix: pyload 2025-11-29 22:41:48 +01:00
4500f41983 feat: add ugreen nas leds 2025-11-29 13:12:18 +01:00
1d30eeb939 feat: update victoriametrics secret 2025-11-28 23:51:33 +01:00
537f144885 feat: add smart alerting and noatime to disks 2025-11-28 23:50:24 +01:00
dbada3c509 feat: change harddisk nas 2025-11-28 21:17:07 +01:00
c8be707420 fix: nas handling 2025-11-28 20:54:12 +01:00
fdba2c75c7 feat: add nas host 2025-11-28 20:53:47 +01:00
55d600c0c0 feat: add nas to fleet 2025-11-28 18:49:56 +01:00
71c5bd5e6c change claude 2025-11-28 01:57:30 +01:00
998f04713f fix(nb): additional sops.lua bug fixes
- Use nvim_buf_get_name(args.buf) instead of vim.fn.expand("%:p") in BufReadPost
- Add timeout to encrypt operation to prevent infinite hangs
- Add microsecond precision to temp file names to prevent collisions
- Strip trailing newline before vim.split to avoid extra empty lines
- Add trailing newline when writing temp file for POSIX compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 01:54:20 +01:00
6935fbea8b fix: sops implementation 2025-11-28 01:32:23 +01:00
301e090251 feat: add Claude.md 2025-11-28 01:00:55 +01:00
58d8ef050c feat: install own claude-code 2025-11-27 22:10:05 +01:00
41cb2ec791 fix: update script for claude-code 2025-11-27 22:09:48 +01:00
1faec5b2d1 feat: add updated claude code 2025-11-27 21:49:24 +01:00
111b8cec97 feat: change rustdesk for epicenter 2025-11-27 21:49:11 +01:00
3aaebdb1c4 fix: filebot 2025-11-27 12:50:00 +01:00
3e7b8c93e3 feat: split pyload 2025-11-26 22:39:07 +01:00
3e2f46377e fix: pyload and filebot 2025-11-26 21:08:58 +01:00
38bead3dc8 fix: pyload 2025-11-26 12:48:24 +01:00
351d36b217 fix: changes to pyload 2025-11-26 00:26:21 +01:00
59a37c9b46 feat: add jellyfin and hardware acceleration for transcoding 2025-11-25 19:48:52 +01:00
d7d3722ce7 fix: pyload 2025-11-25 17:03:03 +01:00
6475524d23 fix: amz postfix setup 2025-11-23 11:29:07 +01:00
1a70ca9564 feat: add pyload 2025-11-23 11:28:57 +01:00
d6f206f0bb feat: add email 2025-11-21 14:00:47 +01:00
b3c5366f31 feat: nb change building speed 2025-11-19 00:00:58 +01:00
fab06ca4d5 feat: change livingroom to hue 2025-11-19 00:00:43 +01:00
bd1d04943d fix: change deconz 2025-11-19 00:00:34 +01:00
8305d1b0c5 fix: nb chromium 2025-11-18 22:31:07 +01:00
2d812c03eb feat: ai-mailer add faq page 2025-11-18 22:30:54 +01:00
156e63fd6c feat: amz changes 2025-11-18 22:30:41 +01:00
96 changed files with 5079 additions and 633 deletions

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"nixos": {
"command": "uvx",
"args": ["mcp-nixos"]
}
}
}

View File

@@ -16,6 +16,7 @@ keys:
- &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d - &gpd-win4 age1ceg548u5ma6rgu3xgvd254y5xefqrdqfqhcjsjp3255q976fgd2qaua53d
- &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - &nb age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w - &amzebs-01 age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
- &nas age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
creation_rules: creation_rules:
- path_regex: ^[^/]+\.yaml$ - path_regex: ^[^/]+\.yaml$
@@ -80,6 +81,14 @@ creation_rules:
- *dominik2 - *dominik2
- *nb - *nb
- *amzebs-01 - *amzebs-01
- path_regex: hosts/nas/[^/]+\.yaml$
key_groups:
- age:
- *bitwarden
- *dominik
- *dominik2
- *nb
- *nas
- path_regex: hosts/mail/[^/]+\.yaml$ - path_regex: hosts/mail/[^/]+\.yaml$
key_groups: key_groups:
- age: - age:
@@ -129,6 +138,7 @@ creation_rules:
- *netboot - *netboot
- *fw - *fw
- *fw-new - *fw-new
- *nas
- *amzebs-01 - *amzebs-01
- path_regex: utils/modules/victoriametrics/[^/]+\.yaml$ - path_regex: utils/modules/victoriametrics/[^/]+\.yaml$
key_groups: key_groups:
@@ -142,4 +152,5 @@ creation_rules:
- *netboot - *netboot
- *fw - *fw
- *fw-new - *fw-new
- *nas
- *amzebs-01 - *amzebs-01

100
CLAUDE.md Normal file
View File

@@ -0,0 +1,100 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is a NixOS infrastructure repository managing multiple hosts (servers and personal machines) using a modular Nix configuration approach with SOPS for secrets management and Bento for deployment.
## Build and Test Commands
```bash
# Enter development shell (sets up MCP configs)
nix-shell
# Test configuration before deployment (required before PRs)
./scripts/test-configuration <hostname>
./scripts/test-configuration -v <hostname> # with --show-trace
# Edit encrypted secrets
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
# Update secrets keys after adding new age keys
./scripts/update-secrets-keys
# Format Nix files
nix run nixpkgs#nixpkgs-fmt .
# Compute hash for new packages
nix hash to-sri --type sha256 $(nix-prefetch-url https://example.com/file.tar.gz)
```
## Architecture
### Host Structure
Each host in `hosts/<hostname>/` contains:
- `configuration.nix` - Main entry point importing modules
- `hardware-configuration.nix` - Machine-specific hardware config
- `secrets.yaml` - SOPS-encrypted secrets
- `modules/` - Host-specific service configurations
- `fleet.nix` → symlink to root `fleet.nix` (SFTP user provisioning)
- `utils/` → symlink to root `utils/` (shared modules)
Current hosts: `fw` (firewall/router), `nb` (notebook), `web-arm`, `mail`, `amzebs-01`, `nas`
### Shared Components (`utils/`)
- `modules/` - Reusable NixOS modules (nginx, sops, borgbackup, lego, promtail, etc.)
- `overlays/` - Nixpkgs overlays
- `pkgs/` - Custom package derivations
- `bento.nix` - Deployment helper module
### Secrets Management
- SOPS with age encryption; keys defined in `.sops.yaml`
- Each host has its own age key derived from SSH host key
- Host secrets in `hosts/<hostname>/secrets.yaml`
- Shared module secrets in `utils/modules/<module>/secrets.yaml`
**IMPORTANT: Never modify secrets files directly.** Instead, tell the user which secrets need to be added and where, so they can edit the encrypted files themselves using:
```bash
nix-shell -p sops --run 'sops hosts/<hostname>/secrets.yaml'
```
### Deployment
The Git runner handles deployment automatically when changes merge to main. A successful `./scripts/test-configuration <host>` dry-build is the gate before pushing.
## Custom Packages
When creating a new package in `utils/pkgs/`, always include an `update.sh` script to automate version updates. See `utils/pkgs/claude-code/update.sh` for the pattern:
1. Fetch latest version from upstream (npm, GitHub, etc.)
2. Update version string in `default.nix`
3. Update source hash using `nix-prefetch-url`
4. Update dependency hashes (e.g., `npmDepsHash`) by triggering a build with a fake hash
5. Verify the final build succeeds
Example structure:
```
utils/pkgs/<package-name>/
├── default.nix
├── update.sh # Always include this
└── (other files like patches, lock files)
```
**IMPORTANT: When modifying a custom package** (patches, version updates, etc.), always test by building the package directly, not just running `test-configuration`. The configuration test only checks that the Nix expression evaluates, but doesn't verify the package actually builds:
```bash
# Build a custom package directly to verify it works
nix-build -E 'with import <nixpkgs> { overlays = [ (import ./utils/overlays/packages.nix) ]; config.allowUnfree = true; }; <package-name>'
```
## Workflow
**IMPORTANT: Always run `./scripts/test-configuration <hostname>` after making any changes** to verify the NixOS configuration builds successfully. This is required before committing.
## Conventions
- Nix files: two-space indentation, lower kebab-case naming
- Commits: Conventional Commits format (`fix:`, `feat:`, `chore:`), scope by host when relevant (`fix(mail):`). Do not add "Generated with Claude Code" or "Co-Authored-By: Claude" footers.
- Modules import via explicit paths, not wildcards
- Comments explain non-obvious decisions (open ports, unusual service options)
- **Never update `system.stateVersion`** - it should remain at the original installation version. To upgrade NixOS, update the `channel` file instead.

View File

@@ -39,7 +39,7 @@ cat ~/.ssh/id_rsa.pub | ssh -p23 u149513-subx@u149513-subx.your-backup.de instal
# 4. Add new Host # 4. Add new Host
```console ```console
sftp host.cloonar.com@git.cloonar.com:/config/bootstrap.sh ./ sftp host@git.cloonar.com:/config/bootstrap.sh ./
``` ```
# 5. Yubikey # 5. Yubikey

View File

@@ -47,6 +47,11 @@
username = "gpd-win4"; username = "gpd-win4";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO"; key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjfS2DtS8PQgkf86dU+EVu5t+r/QlCWmY7+RPYprQrO";
} }
{
username = "nas";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS6b97LPUpr7/kWvOcI40s5e+gfbfz0I2/hAPL6zTmU";
}
{ {
username = "amzebs-01"; username = "amzebs-01";
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q"; key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMkFZ60SPl8pzEtGrFq1+n6ZkDuNe3xJaccJMjr3y/q";

View File

@@ -0,0 +1,471 @@
# Email Setup for amzebs-01 (amz.at)
This host is configured to send emails via Laravel with DKIM signing.
## Configuration Overview
- **Postfix**: Localhost-only SMTP server (no external access)
- **Rspamd**: DKIM signing with host-specific key
- **Domain**: amz.at
- **DKIM Selector**: amzebs-01
- **Secret Management**: DKIM private key stored in sops
## Initial Setup (Before First Deployment)
### 1. Generate DKIM Key Pair
You need to generate a DKIM key pair locally first. You'll need `rspamd` package installed.
#### Option A: Using rspamd (if installed locally)
```bash
# Create a temporary directory
mkdir -p /tmp/dkim-gen
# Generate the key pair
rspamadm dkim_keygen -s amzebs-01 -d amz.at -k /tmp/dkim-gen/amz.at.amzebs-01.key
```
This will output:
- **Private key** saved to `/tmp/dkim-gen/amz.at.amzebs-01.key`
- **Public key** printed to stdout (starts with `v=DKIM1; k=rsa; p=...`)
#### Option B: Using OpenSSL (alternative)
```bash
# Create temporary directory
mkdir -p /tmp/dkim-gen
# Generate private key (2048-bit RSA)
openssl genrsa -out /tmp/dkim-gen/amz.at.amzebs-01.key 2048
# Extract public key in the correct format for DNS
openssl rsa -in /tmp/dkim-gen/amz.at.amzebs-01.key -pubout -outform PEM | \
grep -v '^-----' | tr -d '\n' > /tmp/dkim-gen/public.txt
# Display the DNS record value
echo "v=DKIM1; k=rsa; p=$(cat /tmp/dkim-gen/public.txt)"
```
**Save the public key output!** You'll need it for DNS configuration later.
### 2. Add DKIM Private Key to Sops Secrets
Now you need to encrypt and add the private key to your secrets file.
#### Step 1: View the private key
```bash
cat /tmp/dkim-gen/amz.at.amzebs-01.key
```
#### Step 2: Edit the secrets file
```bash
cd /home/dominik/projects/cloonar/cloonar-nixos/hosts/amzebs-01
sops secrets.yaml
```
#### Step 3: Add the key to secrets.yaml
In the sops editor, add a new key called `rspamd-dkim-key` with the **entire private key content** including the BEGIN/END markers:
```yaml
rspamd-dkim-key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
(paste the entire key content here)
...
-----END PRIVATE KEY-----
```
**Important:**
- Make sure to use the pipe `|` character for multiline content
- Keep the proper indentation (2 spaces before each line of the key)
- Include the full BEGIN/END markers
#### Step 4: Save and exit
Save the file in sops (it will be encrypted automatically).
#### Step 5: Clean up temporary files
```bash
rm -rf /tmp/dkim-gen
```
### 3. Verify Secret is Encrypted
Check that the secret is properly encrypted:
```bash
cat hosts/amzebs-01/secrets.yaml
```
You should see encrypted content, not the plain private key.
### 4. Extract Public Key for DNS (if needed later)
If you didn't save the public key earlier, you can extract it after deployment:
```bash
# On the server after deployment
sudo cat /var/lib/rspamd/dkim/amz.at.amzebs-01.key | \
openssl rsa -pubout -outform PEM 2>/dev/null | \
grep -v '^-----' | tr -d '\n'
```
Then format it as:
```
v=DKIM1; k=rsa; p=<output_from_above>
```
## Deployment
### 1. Deploy Configuration
After adding the DKIM private key to sops, deploy the configuration:
```bash
# Build and switch on the remote host
nixos-rebuild switch --flake .#amzebs-01 --target-host amzebs-01 --use-remote-sudo
```
Or if deploying locally on the server:
```bash
sudo nixos-rebuild switch
```
### 2. Verify Deployment
Check that the services are running:
```bash
# Check rspamd-dkim-setup service
systemctl status rspamd-dkim-setup
# Check that rspamd is running
systemctl status rspamd
# Check that postfix is running
systemctl status postfix
# Verify DKIM key was deployed
ls -la /var/lib/rspamd/dkim/amz.at.amzebs-01.key
```
## DNS Configuration
Add the following DNS records to ensure proper email delivery and avoid spam classification.
### Critical: PTR Record (Reverse DNS)
**This is CRITICAL for email deliverability!** Without a proper PTR record, most mail servers will reject or spam your emails.
#### What is a PTR Record?
A PTR (pointer) record is a reverse DNS entry that maps your IP address back to your hostname. Mail servers use this to verify you're a legitimate mail server.
#### Required PTR Record
```
IP Address: 23.88.38.1
Points to: amzebs-01.amz.at
```
#### How to Configure PTR Record
**Step 1: Contact Your Hosting Provider**
PTR records MUST be configured through your hosting provider (e.g., Hetzner, OVH, AWS, etc.). You cannot set PTR records through your domain registrar.
1. Log into your hosting provider's control panel
2. Find the "Reverse DNS" or "PTR Record" section
3. Set the PTR record for IP `23.88.38.1` to point to `amzebs-01.amz.at`
**Common Provider Links:**
- **Hetzner**: Robot panel → IPs → Edit reverse DNS
- **OVH**: Network → IP → ... → Modify reverse
- **AWS EC2**: Select instance → Networking → Request reverse DNS
**Step 2: Verify Forward DNS First**
Before setting the PTR record, ensure your forward DNS is correct:
```bash
# This should return 23.88.38.1
dig +short amzebs-01.amz.at A
host amzebs-01.amz.at
```
**Step 3: Verify PTR Record**
After configuring, verify the PTR record is working:
```bash
# Method 1: Using dig
dig +short -x 23.88.38.1
# Method 2: Using host
host 23.88.38.1
# Method 3: Using nslookup
nslookup 23.88.38.1
```
All commands should return: `amzebs-01.amz.at`
**Step 4: Verify FCrDNS (Forward-Confirmed Reverse DNS)**
This ensures forward and reverse DNS match properly:
```bash
# Forward lookup
dig +short amzebs-01.amz.at
# Should output: 23.88.38.1
# Reverse lookup
dig +short -x 23.88.38.1
# Should output: amzebs-01.amz.at.
```
If both work correctly, FCrDNS passes! ✓
**Why PTR Records Matter:**
- Gmail, Microsoft, Yahoo require valid PTR records
- Missing PTR = automatic spam classification or rejection
- Can add 5-10 points to spam score alone
- Required for professional email delivery
### Domain DNS Records (amz.at)
Add these records through your domain registrar's DNS management:
#### SPF Record
```
Type: TXT
Name: @
Value: v=spf1 mx a:amzebs-01.amz.at ~all
```
#### DKIM Record
```
Type: TXT
Name: amzebs-01._domainkey
Value: [Your public key from step 1 above]
```
The DKIM record will look something like:
```
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
```
#### DMARC Record
```
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=quarantine; rua=mailto:postmaster@amz.at; ruf=mailto:postmaster@amz.at; fo=1
```
**Explanation:**
- `p=quarantine`: Failed messages should be quarantined (you can change to `p=reject` after testing)
- `rua=mailto:...`: Aggregate reports sent to this address
- `ruf=mailto:...`: Forensic reports sent to this address
- `fo=1`: Generate forensic reports for any failure
## Laravel Configuration
Update your Laravel application's `.env` file:
#### Option A: Using sendmail (Recommended)
```env
MAIL_MAILER=sendmail
MAIL_FROM_ADDRESS=noreply@amz.at
MAIL_FROM_NAME="${APP_NAME}"
```
#### Option B: Using SMTP
```env
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=25
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=noreply@amz.at
MAIL_FROM_NAME="${APP_NAME}"
```
**Note**: Laravel can use ANY email address with @amz.at domain. All will be DKIM signed automatically.
## Testing Email
### Test from Command Line
```bash
# Send a test email
echo "Test email body" | mail -s "Test Subject" test@example.com -aFrom:test@amz.at
```
### Check Postfix Queue
```bash
# View mail queue
mailq
# View logs
journalctl -u postfix -f
```
### Check Rspamd Logs
```bash
# View rspamd logs
journalctl -u rspamd -f
```
### Test DKIM Signature and Deliverability
Send an email to test your complete email configuration:
#### Email Testing Services
1. **Mail Tester** (https://www.mail-tester.com/)
- Provides a temporary email address
- Shows comprehensive spam score (0-10, higher is better)
- Checks DKIM, SPF, DMARC, PTR, blacklists, content
- **Target: 9/10 or higher**
2. **MXToolbox Email Health** (https://mxtoolbox.com/emailhealth/)
- Comprehensive deliverability check
- Checks DNS records, blacklists, configuration
3. **Google Admin Toolbox** (https://toolbox.googleapps.com/apps/messageheader/)
- Paste email headers to see how Gmail scored your email
- Shows SPF, DKIM, DMARC results
#### What to Check
- ✓ DKIM signature is valid
- ✓ SPF passes
- ✓ DMARC passes
- ✓ PTR record (reverse DNS) matches
- ✓ Not on any blacklists
- ✓ Spam score < 2.0 (lower is better)
#### Common Issues & Fixes
**High Spam Score (> 5.0)**
- Check: PTR record configured correctly? (Critical!)
- Check: HELO name matches hostname?
- Check: All headers present (To:, From:, Subject:)?
- Check: IP not blacklisted?
**Missing "To:" Header**
Your Laravel app must set a recipient. In your code:
```php
Mail::to('recipient@example.com')
->send(new YourMailable());
```
**HELO/EHLO Mismatch**
After applying this configuration, HELO should be `amzebs-01.amz.at`, not `localhost`
**Check Current HELO Name**
```bash
# On the server
echo "HELO test" | nc localhost 25
# Should see: 250 amzebs-01.amz.at
```
## Verification Commands
```bash
# Check if Postfix is running
systemctl status postfix
# Check if Rspamd is running
systemctl status rspamd
# Check if Postfix is listening on localhost only
ss -tlnp | grep master
# View DKIM public key again
systemctl start rspamd-show-dkim
journalctl -u rspamd-show-dkim
# Check if DKIM key exists
ls -la /var/lib/rspamd/dkim/
```
## Security Notes
1. **Localhost-only**: Postfix is configured to listen ONLY on 127.0.0.1
2. **No authentication**: Not needed since only local processes can connect
3. **No firewall changes**: No external ports opened for email
4. **DKIM signing**: All outgoing emails are automatically signed with DKIM
5. **Host-specific key**: Using selector "amzebs-01" allows multiple hosts to send for amz.at
## Troubleshooting
### Email not being sent
1. Check Postfix status: `systemctl status postfix`
2. Check queue: `mailq`
3. Check logs: `journalctl -u postfix -n 100`
### DKIM not signing
1. Check Rspamd status: `systemctl status rspamd`
2. Check if key exists: `ls -la /var/lib/rspamd/dkim/amz.at.amzebs-01.key`
3. Check Rspamd logs: `journalctl -u rspamd -n 100`
### Permission errors
```bash
# Ensure proper ownership
chown -R rspamd:rspamd /var/lib/rspamd/dkim/
chmod 600 /var/lib/rspamd/dkim/*.key
```
### Rotate DKIM key
```bash
# 1. Generate new key pair locally (follow "Initial Setup" steps)
# 2. Update the rspamd-dkim-key in secrets.yaml with new key
# 3. Deploy the configuration
nixos-rebuild switch
# 4. Restart the setup service to copy new key
systemctl restart rspamd-dkim-setup
# 5. Restart rspamd to use new key
systemctl restart rspamd
# 6. Update DNS with new public key
# 7. Wait for DNS propagation before removing old DNS record
```
## Related Files
- Postfix config: `hosts/amzebs-01/modules/postfix.nix`
- Rspamd config: `hosts/amzebs-01/modules/rspamd.nix`
- Main config: `hosts/amzebs-01/configuration.nix`
- Secrets file: `hosts/amzebs-01/secrets.yaml` (encrypted)
## Sops Secret Configuration
The DKIM private key is stored as a sops secret with the following configuration:
```nix
sops.secrets.rspamd-dkim-key = {
owner = "rspamd";
group = "rspamd";
mode = "0400";
};
```
This ensures:
- Only the rspamd user can read the key
- The key is decrypted at boot time by sops-nix
- The key is encrypted in version control
- The key persists across rebuilds
The key is automatically copied from the sops secret path to `/var/lib/rspamd/dkim/amz.at.amzebs-01.key` by the `rspamd-dkim-setup.service` on every boot.

1
hosts/amzebs-01/channel Normal file
View File

@@ -0,0 +1 @@
https://channels.nixos.org/nixos-25.11

View File

@@ -3,11 +3,15 @@
./utils/bento.nix ./utils/bento.nix
./utils/modules/sops.nix ./utils/modules/sops.nix
./utils/modules/nginx.nix ./utils/modules/nginx.nix
./utils/modules/set-nix-channel.nix
./modules/mysql.nix ./modules/mysql.nix
./modules/web/stack.nix ./modules/web/stack.nix
./modules/laravel-storage.nix ./modules/laravel-storage.nix
./modules/laravel-scheduler.nix
./modules/blackbox-exporter.nix ./modules/blackbox-exporter.nix
./modules/postfix.nix
./modules/rspamd.nix
./utils/modules/autoupgrade.nix ./utils/modules/autoupgrade.nix
./utils/modules/promtail ./utils/modules/promtail
@@ -65,7 +69,7 @@
networking.firewall = { networking.firewall = {
enable = true; enable = true;
allowedTCPPorts = [ 22 80 443 ]; allowedTCPPorts = [ 22 80 443 3306 ];
# Allow MariaDB access only from specific IP # Allow MariaDB access only from specific IP
extraCommands = '' extraCommands = ''
@@ -73,5 +77,5 @@
''; '';
}; };
system.stateVersion = "23.11"; system.stateVersion = "25.11";
} }

View File

@@ -0,0 +1,51 @@
{ config, lib, pkgs, ... }:
# Daily scheduled Laravel artisan jobs
# Runs artisan finish:reports at 01:00 for production and staging APIs
let
php = pkgs.php82;
sites = [
{
domain = "api.ebs.amz.at";
user = "api_ebs_amz_at";
}
{
domain = "api.stage.ebs.amz.at";
user = "api_stage_ebs_amz_at";
}
];
mkArtisanService = site: {
name = "artisan-finish-reports-${site.domain}";
value = {
description = "Laravel artisan finish:reports for ${site.domain}";
after = [ "network.target" "mysql.service" "phpfpm-${site.domain}.service" ];
serviceConfig = {
Type = "oneshot";
User = site.user;
Group = "nginx";
WorkingDirectory = "/var/www/${site.domain}";
ExecStart = "${php}/bin/php artisan finish:reports";
};
};
};
mkArtisanTimer = site: {
name = "artisan-finish-reports-${site.domain}";
value = {
description = "Daily timer for artisan finish:reports on ${site.domain}";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 01:00:00";
Persistent = true;
};
};
};
in
{
systemd.services = builtins.listToAttrs (map mkArtisanService sites);
systemd.timers = builtins.listToAttrs (map mkArtisanTimer sites);
}

View File

@@ -5,6 +5,7 @@
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
# api.ebs.cloonar.dev # api.ebs.cloonar.dev
"d /var/www/api.ebs.cloonar.dev/storage/framework/cache 0775 api_ebs_cloonar_dev nginx -" "d /var/www/api.ebs.cloonar.dev/storage/framework/cache 0775 api_ebs_cloonar_dev nginx -"
"d /var/www/api.ebs.cloonar.dev/storage/framework/testing 0775 api_ebs_cloonar_dev nginx -"
"d /var/www/api.ebs.cloonar.dev/storage/framework/sessions 0775 api_ebs_cloonar_dev nginx -" "d /var/www/api.ebs.cloonar.dev/storage/framework/sessions 0775 api_ebs_cloonar_dev nginx -"
"d /var/www/api.ebs.cloonar.dev/storage/framework/views 0775 api_ebs_cloonar_dev nginx -" "d /var/www/api.ebs.cloonar.dev/storage/framework/views 0775 api_ebs_cloonar_dev nginx -"
"d /var/www/api.ebs.cloonar.dev/storage/logs 0775 api_ebs_cloonar_dev nginx -" "d /var/www/api.ebs.cloonar.dev/storage/logs 0775 api_ebs_cloonar_dev nginx -"
@@ -12,6 +13,7 @@
# api.ebs.amz.at # api.ebs.amz.at
"d /var/www/api.ebs.amz.at/storage/framework/cache 0775 api_ebs_amz_at nginx -" "d /var/www/api.ebs.amz.at/storage/framework/cache 0775 api_ebs_amz_at nginx -"
"d /var/www/api.ebs.amz.at/storage/framework/testing 0775 api_ebs_amz_at nginx -"
"d /var/www/api.ebs.amz.at/storage/framework/sessions 0775 api_ebs_amz_at nginx -" "d /var/www/api.ebs.amz.at/storage/framework/sessions 0775 api_ebs_amz_at nginx -"
"d /var/www/api.ebs.amz.at/storage/framework/views 0775 api_ebs_amz_at nginx -" "d /var/www/api.ebs.amz.at/storage/framework/views 0775 api_ebs_amz_at nginx -"
"d /var/www/api.ebs.amz.at/storage/logs 0775 api_ebs_amz_at nginx -" "d /var/www/api.ebs.amz.at/storage/logs 0775 api_ebs_amz_at nginx -"
@@ -19,6 +21,7 @@
# api.stage.ebs.amz.at # api.stage.ebs.amz.at
"d /var/www/api.stage.ebs.amz.at/storage/framework/cache 0775 api_stage_ebs_amz_at nginx -" "d /var/www/api.stage.ebs.amz.at/storage/framework/cache 0775 api_stage_ebs_amz_at nginx -"
"d /var/www/api.stage.ebs.amz.at/storage/framework/testing 0775 api_stage_ebs_amz_at nginx -"
"d /var/www/api.stage.ebs.amz.at/storage/framework/sessions 0775 api_stage_ebs_amz_at nginx -" "d /var/www/api.stage.ebs.amz.at/storage/framework/sessions 0775 api_stage_ebs_amz_at nginx -"
"d /var/www/api.stage.ebs.amz.at/storage/framework/views 0775 api_stage_ebs_amz_at nginx -" "d /var/www/api.stage.ebs.amz.at/storage/framework/views 0775 api_stage_ebs_amz_at nginx -"
"d /var/www/api.stage.ebs.amz.at/storage/logs 0775 api_stage_ebs_amz_at nginx -" "d /var/www/api.stage.ebs.amz.at/storage/logs 0775 api_stage_ebs_amz_at nginx -"

View File

@@ -0,0 +1,56 @@
{ pkgs
, lib
, config
, ...
}:
let
headerChecksFile = pkgs.writeText "header_checks" ''
# Warn about missing critical headers (but don't reject from localhost)
# These help identify misconfigured applications
/^$/ WARN Missing headers detected
'';
in
{
services.postfix = {
mapFiles."header_checks" = headerChecksFile;
enable = true;
hostname = "amzebs-01.amz.at";
domain = "amz.at";
config = {
# Explicitly set hostname to prevent "localhost" HELO issues
myhostname = "amzebs-01.amz.at";
# Set proper HELO name for outgoing SMTP connections
smtp_helo_name = "amzebs-01.amz.at";
# Professional SMTP banner (prevents appearing as default/misconfigured)
smtpd_banner = "$myhostname ESMTP";
# Listen only on localhost for security
# Laravel will send via localhost, no external access needed
inet_interfaces = "loopback-only";
# Compatibility
compatibility_level = "2";
# Only accept mail from localhost
mynetworks = [ "127.0.0.0/8" "[::1]/128" ];
# Larger message size limits for attachments
mailbox_size_limit = 202400000; # ~200MB
message_size_limit = 51200000; # ~50MB
# Ensure proper header handling
# Reject mail that's missing critical headers
header_checks = "regexp:/var/lib/postfix/conf/header_checks";
# Rate limiting to prevent spam-like behavior
# Allow reasonable sending rates for applications
smtpd_client_message_rate_limit = 100;
smtpd_client_recipient_rate_limit = 200;
# Milter configuration is handled automatically by rspamd.postfix.enable
};
};
}

View File

@@ -0,0 +1,84 @@
{ pkgs
, config
, ...
}:
let
domain = "amz.at";
selector = "amzebs-01";
localConfig = pkgs.writeText "local.conf" ''
logging {
level = "notice";
}
# DKIM signing configuration with host-specific selector
dkim_signing {
path = "/var/lib/rspamd/dkim/${domain}.${selector}.key";
selector = "${selector}";
allow_username_mismatch = true;
}
# ARC signing (Authenticated Received Chain)
arc {
path = "/var/lib/rspamd/dkim/${domain}.${selector}.key";
selector = "${selector}";
allow_username_mismatch = true;
}
# Add authentication results to headers
milter_headers {
use = ["authentication-results"];
authenticated_headers = ["authentication-results"];
}
'';
in
{
services.rspamd = {
enable = true;
extraConfig = ''
.include(priority=1,duplicate=merge) "${localConfig}"
'';
# Enable Postfix milter integration
postfix.enable = true;
};
# Copy DKIM key from sops secret to rspamd directory
systemd.services.rspamd-dkim-setup = {
description = "Setup DKIM key from sops secret for ${domain}";
wantedBy = [ "multi-user.target" ];
before = [ "rspamd.service" ];
after = [ "sops-nix.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
DKIM_DIR="/var/lib/rspamd/dkim"
DKIM_KEY="$DKIM_DIR/${domain}.${selector}.key"
# Create directory if it doesn't exist
mkdir -p "$DKIM_DIR"
# Copy key from sops secret
if [ -f "${config.sops.secrets.rspamd-dkim-key.path}" ]; then
cp "${config.sops.secrets.rspamd-dkim-key.path}" "$DKIM_KEY"
chown rspamd:rspamd "$DKIM_KEY"
chmod 600 "$DKIM_KEY"
echo "DKIM key deployed successfully from sops secret"
else
echo "ERROR: DKIM key not found in sops secrets!"
echo "Please ensure rspamd-dkim-key is defined in secrets.yaml"
exit 1
fi
'';
};
sops.secrets.rspamd-dkim-key = {
owner = "rspamd";
group = "rspamd";
mode = "0400";
};
}

View File

@@ -1,45 +1,46 @@
borg-passphrase: ENC[AES256_GCM,data:6T00Em+a5TcrmQNvtoCoij5aks6KIZkCAAaPXLirkQlZ6x1p1bX9KXU2ZvBAtVPrUuTeZLPTKqT/iL5Io+WKGw==,iv:gB9cktzKa8khmZZ8xwLS6oEX+Ag3APmf2jIQNLa1g/Y=,tag:sWfVbKQHgaaSRWuqYdpKTQ==,type:str] borg-passphrase: ENC[AES256_GCM,data:Q2GvEat5EHmshFiya3yNqFTVS+oJv0al+bYMRwysb0yu7F2gCJd000Y3ibA+tUPSL9iSlMSy0cTkesGVEGBt9w==,iv:/kUJXgibF1cyaCPB55/0nKYq9sSva6psxu2P/l7iRN4=,tag:velr9LTfoj7gEWhUmvPtQg==,type:str]
borg-ssh-key: ENC[AES256_GCM,data:TrfaVOWlk8NXMEm6xr5+9pv2j8qPQ2dd6jAhHw8uw25ijhiA+eNtQh9YuQj40zw7hj7cQKctZ6pptdGS1OvS0Zoq1r6IJWLJ2UZcYLXDhOX2/TJQbRcooawG5+JiYCMBe6+T1bzgQaDGWKM7l09lq/saycci/ICe6KKQE8d8i0RaKCsCTf6auNiSMgjYBUMezYP0tTMTIZT6lqeHCUqiEkQ32aE7fdFCAF4hU9JkzhUad3BYasJfDKWksPGc2/GrCpx5JHAD4Tp6GbUpTFVCFf0JVFWaAIyYfKsIShO+yks2TIEOkcQbg8VZADbTtAhhEBcvOCS6mVcgpqQRiEfJI3OvG07KhujJa66EKHlaOM2K71RDDVG+KrlPTW2/1Zaz03FDo7QxiOae9u6KkF+GCIMXAiOL6XYAyDMkxssUA6JodFXImWWwqDJe5Af5jIUAIAAGJTDBSl6S+TWP3pJSM+Pfq6OxRJYFeCVrIE2P4aC37x2vi8V7iTPk0EJSm9TmrLbw+Ia5OvrarwzwtZ2u,iv:IVyeqEGhWUamXw8HPwqyvrHcmTcyEOZmm2NRaTdK+qw=,tag:hQb7wk0YeeQxrPFWuMlfGg==,type:str] borg-ssh-key: ENC[AES256_GCM,data:0YEvv7QDmGsur0PFMmz5HqDgDCEk0kRaOu1n7GGWAwmmr0K0bVbpKzHw5wMiMnXEBrvj0izo4P4LYGAlAAYn22Bhgi2eN/vdvSO5V5uDV1ep2dV/TN2m3oYgTIgot47YgwBhcNunIUtEsbZuhAsTGL6LFBPJ3OCLKvhXTNTDaajgH/e4CvyxHHl63MBzr0i1ajigl1IKCk2hhZF4Kd1YGBCVZRoNyyNXywihlcFeskNfldW/sd5Qn2nowVf1MEV9n6Il6Zc1FX69WUVy1k+kOT7HJZGq3uDmgwXQgQhqKm1wh5uOlLkGUX6fz/nz+YFzLFMuUVvs34CzbbEFuWmGU+aNQrfCfI1hqwB5s6wVNdpUmigX9AQMQklu85tHFJg1AaRvhA24Cp/GrptggrTThcjwVFoe9NSQouNYn+ImTvlsE4HuDRRFE6YUounGd2lpRd40LsEjwKiLtwBwqG94u4ZOI91+LG6ZqHftRehE9r/CtedLyqtluNyyQyUNKPraUOm9Rrapewsj0ZCZgGQU,iv:xdRUBQlZlwVIog5KgZRmGNxdmhFE9HgnK3Ahfo+zT9k=,tag:McsJKUEGnKXxiv8Tg5zA4A==,type:str]
mysql-readonly-password: ENC[AES256_GCM,data:KQiL0ZJGkJEqX7wADmY2YucT79Grt+tCQA/aER7llHgqUIvjJHO8C2yw+VI=,iv:M3QchAeKXp7BjP2FfaWgUNiGPs0qQHe9P5lttxO5+Fg=,tag:TPGUWXYQsf40hlVu7PGEEg==,type:str] mysql-readonly-password: ENC[AES256_GCM,data:k2RplkUZPGZlh29KXXdtwe+MCqKzTI/bLdyuEeicdkbGlBk1SGyLF8vW4t8=,iv:a14IrXYVCDqPKGfJSEPP8g19sPvRTx5NT8IVJJeL48s=,tag:GWgU3oa/+u21/L2y3+vOsw==,type:str]
rspamd-dkim-key: ENC[AES256_GCM,data:maOnsx8AQUIjXqHEzHLxtSvAkr9+YCZid9xWaflkffS0gHd/hoHrozHy+rHSjU7Mz7QHYhjUjFY7Hp7wdKQnHpQLJRV96iNPXTXXYtBr7oDL51cq8ozd094FuMeLNSPitV89OHDcM+9h1F4dsdDWPUiw7eoijQeZ8vx1/VCVAp4FVxTFX3qhoMhXlFabiyM85eKMwJG4BdSwqS624f2Z4tvECRp0pBGtd/3r4/EVRDV1qNsiFvH8mi8eyg9xiWDLDrePq4TuWSu1Xc7z0qpDy0o8iAwGhPu9egIyzHEPk07j9U7PpK56C2UCSY0JBm0hkBGbqLXyRklSMytxoKgw4GJykMwNPNXmA9yuLPanxagJB/z8b7X4HTuYhExzQcC6ke/y8xKcxU4qGt8Ayy5v+QoNpdqXIPsZkIuw9uWm6RIgDt2dCaOdI06lesZKjqU/T6EhDfGoGZX7DwQ7uV9xNDM4NW2jsKpUdFKnzPCCe7/jO/ck4P4i8V+6NWDjj4+/BXDNnKJbMcHIHoSvckCGZiginJsbGvSWd0HfbpR7GQAnL3uKB5/HuFAaUkx+dPHmmP2tOBv6vNt+tq+V9i4kQmwAdl8a9KI456tw9vLwXcBDZOO7n4X5H0jc4afoYCnvLxahvbIXm2QNcBYVKxkqBCvoYEMrBjrnujwbQdEfDKQf3g5p8LQwAfCQ3ng+XH/BDF3qMBdsN1u5Di0FQpCDaGKX8pJ1gg+il76fJgSU8ftoaT32hJnLAjal4cgNIxbta2UYQLixUqaWZ8xqvxrSopkWYrlBBUyQh9jMEoTzpxwCsEPQ72qgVcQfJYlMl4WUBwcasfJnySR+qZ22g3fhStpAQ2HuTGhLjTG1QOewYdwDXDhNmcbqZ478Sp1t7qbBx0R7vWFSyYCMlbmLmvzPm6Z3ET7lkfCrMjMNXaQ8cWSF19QaHAfqRwQooLL93yx7U0KHCilEg4bUjsw8MLQNa4A0ohpq6CG4s5O1+di7W4/h71/moggIebFb2eGLJ8BvbkwiVozXI9L77IGd9RswlEjZed18u7fqetS7dyDthhVG2pvya4zZI/cxIq6oJkNr2RIt3NgYChOh0I/17DuSJJ1jAmPB0Evj8QtCCo49ENnyO5cGWn12DZWybwYkg2jQC4aDFA/u5ajTo3wOKdwHj5hgMz/z05Bn2vAdhCGl6uWWNzcNnDiu3/rjqsjOkfkp0hCP7Q==,iv:FORxJ8htcoLIEJihUN7im3dN4jhnigB70InTohtpWwU=,tag:e2DHBd2dn3piCkEdkbHdoA==,type:str]
sops: sops:
age: age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvWjd4K3BueGVxNGs2OC9t YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhQUpWNUgxVnhuTXd2TkF0
YURtcTIzNytCMDdYd29KenZOdm4vMk9mWEZFCkJrZlVZdkVJeW4rNzc0N3NBY3hM SVVHemFKRWlYczZ0TnBESVNRczhuRUNnUG1BCmJKQ2JZbHhFcXJidHJzci9OaFBm
dE1ORzRHRHlONEQ2dW83R051aE45QVEKLS0tIE0zOVpVbWphNitPaUg0cmxUbW5m ZTd0MGhsaVBic3dMb3psUHRCRnR3ODQKLS0tIERrSG1GVTRHdkJpVWpqdTZ4Yytq
ZHViVHJrOWREb0pYR3hOTm0zSHZ3N0EKeNcZOM+H0XZN3Ji1ubBoHMgycuJFX3+C OHhlZjV6MjRVbXFsWjlQSU03ZDNwYm8KAswHRSdV0BW/oJyZx63iZRHsF7SZ6PO+
YvJ795wSwtXMU+mCDB04tcYPSAI0RC82wGT9r3XLNZgbF/xP0Er3nw== hajQqmEyfcVfEu39zZzxQ2mtWlOr69I++irOhE3NeiFeJ1yIRQDJEQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTnpIdHBmMGdBQzk2N0xW YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUaUNnY0hpdDAzMTNIUS9D
SGlNMUxjYU5SZHJLK0wvWnZyYkEzK04vdWd3CmVGWW50RUQ2K0lLRC90WW9KY1hj RmdKbmplUk9DRXlLRXEvSnVjT05sQjcvTnpJCkd6bGRINm5yYUZOUTVzWEdjRmtG
NGVpMEdzaHUyTUVBaDcxb2w5MC9BUjAKLS0tIFdSSjlFTHl1Z3NYNVhxaSs0WkJE Mmx0ci93N2wvTWV5MzlRVnlYdUxoUWsKLS0tIEVHUlNWYStWTG01RzRrVnNXc3BW
eGVWZHdnMkhaNzlDNlhCa24wZzlvNmsK7pLzsxtlMevP2o9nJOjVgDAjrYdEgRUu VkRkUXROU3plNmwvTUVhYmhCS2syQkEKKgC0EmUu1u2vZ/SZTnam+h846gZSyY4V
NlJHfO0m9U7fJfeu6XSWQgGYRJm7tSmTZKvsJgTS+pKcynHz8B9rkQ== JyMzkws8O5TY9juWdDzXJIU67mIgc4qrWWN3uh8k28JBZGc078b5bg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNMk9VL2s0ZmoxSzY3NmFQ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoK2JUNVdYTzkvM1BBWXRm
MTZEaml5Q0kxRDFwSEN5dG90SzZmcXZLUjJBClpVNUJEZEdaa09hM1BQUU1jVVQz citCNlE4Z1NLdEZ2R0tNZTVSMlFSeGxGOURnClJnYURYa0JZaVprQWdBcmVnOWVj
M2w4QXdmZnJya2pCTEV1QW9keXgyTWMKLS0tIFA1VWl3RzF5V2FMUE1mZ2NYRnBU TGVCK1JWMVlueHJUaTZZYmROM0E5aDAKLS0tIEJxYkdadGtZM250d2d6Ujl2UU9C
OURWSFZnM0lEMXJEcjVPL3hnZ0pIQ1kKVvoCVQuayH/XRfddMKq2d8TssXOS5e1o YUpkVll2S2RpT0I1UVZiZFRKS1prMEEKp/bGImanJ/58vTQG/gUun/Y2QdmOEi3h
bIL6F+tRBle2UgVuXSMkyggCnvLePA8OxfAdMMg5npSFkPgTZrAYYQ== hVS0V2QcfuGgi0/YofLOM3+M6k6ViXw07XfXmR+puvLIHKr2y11x1Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w - recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3QmR3T3FYN2RpR1JrV0w0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDSGdEZnZEaDRpWUJVcnds
aVMycUJUN1NKME1UKzVsZmpNNTAzTmxUUUZNClhmdUF5N0Q4K09IeVhNOWhNNEc2 VGFSQklvczBZdEdEbXhodW8vME9wMUpVRENjClFZcnVqYkJxdlBiZFhma0tmZjgz
UTNzeGJ4NlpxMUtEaHZDWDZOeHdvSU0KLS0tIFNTaThWbklXeE85c3hSMWZwNTNN YXlIdlRDTDU4MHg1dzhGVDRJb2FGYVUKLS0tIDBXSWZ2NkxzdEk0ZlFRM00ybFNy
RWFUVXVXWjdsSHM5ZGljd3YyQW5ja2MKvAhwHL5PcLFxuU7MfV/cWtNfzTb9yoqR M0doaWl5R2cwU2RxQm5DbWxXeTZ5S2MKwrB3SysmgzCThQOhEVx18dxIfko0+oZY
3iD4UJsDDagCIkpvjKods4ydlzh3agOyLHswDSX/WmUur9J5pd4PAg== 9BSZOoFbfuwiLbtpL4J8bzxDvxn6sXxB8EBJH1hbpID53AquWDsxSw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-11-14T11:33:59Z" lastmodified: "2025-11-19T11:16:25Z"
mac: ENC[AES256_GCM,data:AnEs3yzpOJ5/wyCL/sHV6U5V7FBhZZlBQeA+mCGfZ25JZAL3Yb6yD6xJhmGC8AqIFS6PIFSWa2r0suDRQAoVO2AwFVwd9Y/TEwjPGnXvWfwB82+mnyLIakyzM/pcLjiMePUqr5nnJ8tWoKzuqs/jQHuMOGkItqwjkVDr9/hx3lc=,iv:kc08z63phDfs7gzruHjnQA9bXAvWMGkE14/0Kyfhuds=,tag:9wedcRCTAv0HMtSap55JOw==,type:str] mac: ENC[AES256_GCM,data:x4yor9G+QirceSYSX1K9GdfyGellT4JCkE09Tl9/mOX8HMOKFAQGknuwwU6SNGg+ciBFk4TdjQnDmVai4T8JQo9W/DLiZ+GKnWO3s+ZLDX30sEF0aMjKa43R5CCPO/Fl2XH96TaPC+8itTJQ6TpBSg51QLPcpqrMljiBNWvEoTU=,iv:Zi9rglAwgsejUmIpLN/1QlL80BSp3HP32k1xkWt2b+o=,tag:2ADk8d2G4OezkQjcV3CZuA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.11.0

View File

@@ -18,7 +18,7 @@ in {
# React client-side routing support # React client-side routing support
locations."/".extraConfig = '' locations."/".extraConfig = ''
index index.html; index index.html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html$is_args$args;
''; '';
# Cache static assets # Cache static assets

View File

@@ -18,7 +18,7 @@ in {
# React client-side routing support # React client-side routing support
locations."/".extraConfig = '' locations."/".extraConfig = ''
index index.html; index index.html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html$is_args$args;
''; '';
# Cache static assets # Cache static assets

View File

@@ -18,7 +18,7 @@ in {
# React client-side routing support # React client-side routing support
locations."/".extraConfig = '' locations."/".extraConfig = ''
index index.html; index index.html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html$is_args$args;
''; '';
# Cache static assets # Cache static assets

View File

@@ -18,7 +18,7 @@ in {
# React client-side routing support # React client-side routing support
locations."/".extraConfig = '' locations."/".extraConfig = ''
index index.html; index index.html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html$is_args$args;
''; '';
# Cache static assets # Cache static assets

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05 https://channels.nixos.org/nixos-25.11

View File

@@ -7,8 +7,10 @@
./utils/modules/nginx.nix ./utils/modules/nginx.nix
./utils/modules/autoupgrade.nix ./utils/modules/autoupgrade.nix
./utils/modules/victoriametrics
./utils/modules/promtail ./utils/modules/promtail
./utils/modules/borgbackup.nix ./utils/modules/borgbackup.nix
./utils/modules/set-nix-channel.nix
# fw # fw
./modules/network-prefix.nix ./modules/network-prefix.nix
@@ -25,7 +27,6 @@
./modules/podman.nix ./modules/podman.nix
./modules/omada.nix ./modules/omada.nix
./modules/ddclient.nix ./modules/ddclient.nix
./utils/modules/victoriametrics
# ./modules/wol.nix # ./modules/wol.nix
@@ -49,6 +50,7 @@
./modules/firefox-sync.nix ./modules/firefox-sync.nix
./modules/fivefilters.nix ./modules/fivefilters.nix
# ./modules/pyload
# home assistant # home assistant
./modules/home-assistant ./modules/home-assistant
@@ -85,11 +87,24 @@
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"mongodb" "mongodb"
"ai-mailer" "ai-mailer"
"filebot"
]; ];
# Intel N100 Graphics Support for hardware transcoding
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver # VAAPI driver (iHD) for modern Intel GPUs
vpl-gpu-rt # Intel VPL/QSV runtime for Gen 12+ (N100)
intel-compute-runtime # OpenCL support for tone-mapping
];
};
hardware.enableRedistributableFirmware = true;
time.timeZone = "Europe/Vienna"; time.timeZone = "Europe/Vienna";
services.logind.extraConfig = "RuntimeDirectorySize=2G"; services.logind.settings.Login.RuntimeDirectorySize = "2G";
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml; sops.defaultSopsFile = ./secrets.yaml;

View File

@@ -33,6 +33,7 @@
context: context:
urls: urls:
- "https://paraclub.cloonar.dev/de/tandemfallschirmspringen/faq/"
- "https://paraclub.at/de/" - "https://paraclub.at/de/"
- "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/" - "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/"
- "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/" - "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/"
@@ -103,6 +104,8 @@
restartTriggers = [ restartTriggers = [
"/etc/ai-mailer/config.yaml" "/etc/ai-mailer/config.yaml"
config.sops.secrets.ai-mailer-imap-password.path
config.sops.secrets.ai-mailer-openrouter-key.path
]; ];
}; };
} }

View File

@@ -2,21 +2,33 @@
virtualisation = { virtualisation = {
oci-containers.containers = { oci-containers.containers = {
deconz = { deconz = {
autoStart = false; autoStart = true;
image = "marthoc/deconz"; image = "marthoc/deconz";
volumes = [ volumes = [
"/etc/localtime:/etc/localtime:ro" "/etc/localtime:/etc/localtime:ro"
"/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ" "/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ"
"/dev/bus/usb:/dev/bus/usb:ro"
"/run/udev:/run/udev:ro"
]; ];
environment = { environment = {
DECONZ_DEVICE = "/dev/ttyACM0"; DECONZ_DEVICE = "/dev/ttyACM0";
TZ = "Europe/Vienna"; TZ = "Europe/Vienna";
DECONZ_UID = "0";
DECONZ_GID = "0";
DECONZ_START_VERBOSE = "1";
}; };
extraOptions = [ extraOptions = [
"--network=server" "--network=server"
"--ip=${config.networkPrefix}.97.22" "--ip=${config.networkPrefix}.97.22"
"--device=/dev/ttyACM0" "--device=/dev/ttyACM0"
"--hostname=deconz" "--hostname=deconz"
"--mac-address=1a:c4:04:6e:29:bd"
"--cap-add=CAP_MKNOD"
"--cap-add=CAP_NET_RAW"
"--cap-add=CAP_NET_ADMIN"
"--device-cgroup-rule=c 166:* rmw"
"--device-cgroup-rule=c 188:* rmw"
"--security-opt=label=disable"
]; ];
}; };
}; };

View File

@@ -73,6 +73,7 @@
"02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix" "02:00:00:00:00:04,${config.networkPrefix}.97.6,matrix"
"ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git" "ea:db:d4:c1:18:ba,${config.networkPrefix}.97.50,git"
"c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant" "c2:4f:64:dd:13:0c,${config.networkPrefix}.97.20,home-assistant"
"6c:1f:f7:8e:a9:86,${config.networkPrefix}.97.11,nas"
"1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz" "1a:c4:04:6e:29:02,${config.networkPrefix}.101.25,deconz"
"c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz" "c4:a7:2b:c7:ea:30,${config.networkPrefix}.99.10,metz"
@@ -133,6 +134,10 @@
"/foundry-vtt.cloonar.com/${config.networkPrefix}.97.5" "/foundry-vtt.cloonar.com/${config.networkPrefix}.97.5"
"/sync.cloonar.com/${config.networkPrefix}.97.5" "/sync.cloonar.com/${config.networkPrefix}.97.5"
# multimedia
"/dl.cloonar.com/${config.networkPrefix}.97.5"
"/jellyfin.cloonar.com/${config.networkPrefix}.97.5"
"/deconz.cloonar.multimedia/${config.networkPrefix}.97.22" "/deconz.cloonar.multimedia/${config.networkPrefix}.97.22"
"/ddl-warez.to/172.67.184.30" "/ddl-warez.to/172.67.184.30"

View File

@@ -396,12 +396,13 @@
all = true; all = true;
entities = [ entities = [
"light.livingroom_switch" "light.livingroom_switch"
"light.livingroom_bulb_1_rgbcw_bulb" "light.living_bulb_1"
"light.livingroom_bulb_2_rgbcw_bulb" "light.living_bulb_2"
"light.livingroom_bulb_3_rgbcw_bulb" "light.living_bulb_3"
"light.livingroom_bulb_4_rgbcw_bulb" "light.living_bulb_4"
"light.livingroom_bulb_5_rgbcw_bulb" "light.living_bulb_5"
"light.livingroom_bulb_6_rgbcw_bulb" "light.living_bulb_6"
# "light.living_room"
]; ];
} }
{ {
@@ -415,6 +416,7 @@
all = true; all = true;
entities = [ entities = [
"light.kitchen_switch" "light.kitchen_switch"
"light.kitchen_bulb_1"
"light.kitchen" "light.kitchen"
]; ];
} }

View File

@@ -0,0 +1,153 @@
{ config, pkgs, ... }:
let
cids = import ../staticids.nix;
networkPrefix = config.networkPrefix;
filebotScript = pkgs.callPackage ./filebot-process.nix {};
pyloadUser = {
isSystemUser = true;
uid = cids.uids.pyload;
group = "pyload";
home = "/var/lib/pyload";
createHome = true;
extraGroups = [ "jellyfin" ]; # Access to multimedia directories
};
pyloadGroup = {
gid = cids.gids.pyload;
};
jellyfinUser = {
isSystemUser = true;
uid = cids.uids.jellyfin;
group = "jellyfin";
home = "/var/lib/jellyfin";
createHome = true;
extraGroups = [ "render" "video" ];
};
jellyfinGroup = {
gid = cids.gids.jellyfin;
};
in
{
users.users.pyload = pyloadUser;
users.groups.pyload = pyloadGroup;
users.users.jellyfin = jellyfinUser;
users.groups.jellyfin = jellyfinGroup;
# Create the directory structure on the host
systemd.tmpfiles.rules = [
"d /var/lib/downloads 0755 pyload pyload - -"
"d /var/lib/multimedia 0775 root jellyfin - -"
"d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -"
"d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -"
"d /var/lib/multimedia/music 0755 jellyfin jellyfin - -"
"d /var/lib/jellyfin 0755 jellyfin jellyfin - -"
# PyLoad hook scripts directory
"d /var/lib/pyload/config 0755 pyload pyload - -"
"d /var/lib/pyload/config/scripts 0755 pyload pyload - -"
"d /var/lib/pyload/config/scripts/package_extracted 0755 pyload pyload - -"
"L+ /var/lib/pyload/config/scripts/package_extracted/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process"
];
# FileBot license secret
sops.secrets.filebot-license = {
mode = "0440";
owner = "pyload";
group = "pyload";
};
containers.pyload = {
autoStart = true;
ephemeral = false;
privateNetwork = true;
hostBridge = "server";
hostAddress = "${networkPrefix}.97.1";
localAddress = "${networkPrefix}.97.11/24";
# GPU device passthrough for hardware transcoding
allowedDevices = [
{
modifier = "rwm";
node = "/dev/dri/card0";
}
{
modifier = "rwm";
node = "/dev/dri/renderD128";
}
];
bindMounts = {
"/dev/dri" = {
hostPath = "/dev/dri";
isReadOnly = false;
};
"/run/opengl-driver" = {
hostPath = "/run/opengl-driver";
isReadOnly = true;
};
"/nix/store" = {
hostPath = "/nix/store";
isReadOnly = true;
};
"/var/lib/pyload" = {
hostPath = "/var/lib/pyload";
isReadOnly = false;
};
"/var/lib/jellyfin" = {
hostPath = "/var/lib/jellyfin";
isReadOnly = false;
};
"/downloads" = {
hostPath = "/var/lib/downloads";
isReadOnly = false;
};
"/multimedia" = {
hostPath = "/var/lib/multimedia";
isReadOnly = false;
};
"/var/lib/pyload/filebot-license.psm" = {
hostPath = config.sops.secrets.filebot-license.path;
isReadOnly = true;
};
};
config = { lib, config, pkgs, ... }: {
nixpkgs.overlays = [
(import ../../utils/overlays/packages.nix)
];
imports = [
./pyload.nix
./jellyfin.nix
];
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"unrar"
"filebot"
];
networking = {
hostName = "pyload";
useHostResolvConf = false;
defaultGateway = {
address = "${networkPrefix}.97.1";
interface = "eth0";
};
nameservers = [ "${networkPrefix}.97.1" ];
firewall.enable = false;
};
# Ensure render/video groups exist with consistent GIDs for GPU access
users.groups.render = { gid = 303; };
users.groups.video = { gid = 26; };
users.users.pyload = pyloadUser;
users.groups.pyload = pyloadGroup;
users.users.jellyfin = jellyfinUser;
users.groups.jellyfin = jellyfinGroup;
system.stateVersion = "24.05";
};
};
}

View File

@@ -0,0 +1,89 @@
{ pkgs }:
pkgs.writeShellScriptBin "filebot-process" ''
#!/usr/bin/env bash
set -euo pipefail
# FileBot AMC script for automated media organization
# Called by PyLoad's package_extracted hook with parameters:
# $1 = package_id
# $2 = package_name
# $3 = download_folder (actual path to extracted files)
# $4 = password (optional)
PACKAGE_ID="''${1:-}"
PACKAGE_NAME="''${2:-unknown}"
DOWNLOAD_DIR="''${3:-/downloads}"
PASSWORD="''${4:-}"
OUTPUT_DIR="/multimedia"
LOG_FILE="/var/lib/pyload/filebot-amc.log"
EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt"
# Ensure FileBot data directory exists
mkdir -p /var/lib/pyload/.local/share/filebot/data
mkdir -p "$(dirname "$LOG_FILE")"
touch "$EXCLUDE_LIST"
# Install FileBot license if not already installed
if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then
echo "$(date): Installing FileBot license..." >> "$LOG_FILE"
${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true
fi
echo "===========================================" >> "$LOG_FILE"
echo "$(date): PyLoad package extracted hook triggered" >> "$LOG_FILE"
echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE"
echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE"
echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE"
echo "===========================================" >> "$LOG_FILE"
# Check if download directory exists and has media files
if [ ! -d "$DOWNLOAD_DIR" ]; then
echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE"
exit 0
fi
# Check if there are any video/media files to process
if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then
echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE"
echo "$(date): Skipping FileBot processing" >> "$LOG_FILE"
exit 0
fi
echo "$(date): Starting FileBot processing" >> "$LOG_FILE"
# Run FileBot AMC script
set +e # Temporarily disable exit on error to capture exit code
${pkgs.filebot}/bin/filebot \
-script fn:amc \
--output "$OUTPUT_DIR" \
--action move \
--conflict auto \
-non-strict \
--log-file "$LOG_FILE" \
--def \
excludeList="$EXCLUDE_LIST" \
movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \
seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \
ut_dir="$DOWNLOAD_DIR" \
ut_kind=multi \
clean=y \
skipExtract=y
FILEBOT_EXIT_CODE=$?
set -e # Re-enable exit on error
if [ $FILEBOT_EXIT_CODE -ne 0 ]; then
echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE"
exit 0 # Don't fail the hook even if FileBot fails
fi
echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE"
# Clean up any remaining empty directories
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
echo "$(date): All processing completed" >> "$LOG_FILE"
exit 0
''

View File

@@ -0,0 +1,36 @@
{ lib, pkgs, ... }: {
# Intel graphics support for hardware transcoding
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver
vpl-gpu-rt
intel-compute-runtime
];
};
# Set VA-API driver to iHD (modern Intel driver for N100)
environment.sessionVariables = {
LIBVA_DRIVER_NAME = "iHD";
};
services.jellyfin = {
enable = true;
openFirewall = true;
};
# Override systemd hardening for GPU access
systemd.services.jellyfin = {
serviceConfig = {
PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access
DeviceAllow = [
"/dev/dri/card0 rw"
"/dev/dri/renderD128 rw"
];
SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access
};
environment = {
LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable
};
};
}

View File

@@ -0,0 +1,69 @@
{ pkgs, lib, ... }:
{
environment.systemPackages = with pkgs; [
unrar # Required for RAR archive extraction
p7zip # Required for 7z and other archive formats
];
services.pyload = {
enable = true;
downloadDirectory = "/downloads";
listenAddress = "0.0.0.0";
port = 8000;
};
# Configure pyload service
systemd.services.pyload = {
# Add extraction tools to service PATH
path = with pkgs; [
unrar # For RAR extraction
p7zip # For 7z extraction
];
environment = {
# Disable SSL certificate verification
PYLOAD__GENERAL__SSL_VERIFY = "0";
# Download speed limiting (150 Mbit/s = 19200 KiB/s)
PYLOAD__DOWNLOAD__LIMIT_SPEED = "1";
PYLOAD__DOWNLOAD__MAX_SPEED = "19200";
# Enable ExtractArchive plugin
PYLOAD__EXTRACTARCHIVE__ENABLED = "1";
PYLOAD__EXTRACTARCHIVE__DELETE = "1";
PYLOAD__EXTRACTARCHIVE__DELTOTRASH = "0";
PYLOAD__EXTRACTARCHIVE__REPAIR = "1";
PYLOAD__EXTRACTARCHIVE__RECURSIVE = "1";
PYLOAD__EXTRACTARCHIVE__FULLPATH = "1";
# Enable ExternalScripts plugin for hooks
PYLOAD__EXTERNALSCRIPTS__ENABLED = "1";
PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously
};
# Bind-mount DNS configuration files into the chroot
serviceConfig = {
BindReadOnlyPaths = [
"/etc/resolv.conf"
"/etc/nsswitch.conf"
"/etc/hosts"
"/etc/ssl"
"/etc/static/ssl"
];
# Bind mount multimedia directory as writable for FileBot hook scripts
BindPaths = [ "/multimedia" ];
# Override SystemCallFilter to allow @resources syscalls
# FileBot (Java) needs resource management syscalls like setpriority
# during cleanup operations. Still block privileged syscalls for security.
# Use mkForce to completely replace the NixOS module's default filter.
SystemCallFilter = lib.mkForce [
"@system-service"
"@resources" # Explicitly allow resource management syscalls
"~@privileged" # Still block privileged operations
"fchown" # Re-allow fchown for FileBot file operations
"fchown32" # 32-bit compatibility
];
};
};
}

View File

@@ -5,6 +5,9 @@
gitea-runner = 10003; gitea-runner = 10003;
podman = 10004; podman = 10004;
foundry-vtt = 10005; foundry-vtt = 10005;
pyload = 10006;
jellyfin = 10007;
filebot = 10008;
}; };
gids = { gids = {
unbound = 10001; unbound = 10001;
@@ -12,5 +15,8 @@
gitea-runner = 10003; gitea-runner = 10003;
podman = 10004; podman = 10004;
foundry-vtt = 10005; foundry-vtt = 10005;
pyload = 10006;
jellyfin = 10007;
filebot = 10008;
}; };
} }

View File

@@ -19,21 +19,19 @@
}; };
# n8n service configuration # n8n service configuration
services.n8n = { services.n8n.enable = true;
enable = true;
settings = {
database.type = "postgresdb";
database.postgresdb.host = "/run/postgresql";
database.postgresdb.database = "n8n";
database.postgresdb.user = "n8n";
executions.pruneData = true;
executions.pruneDataMaxAge = 168; # 7 days
};
};
# Configure git integration via environment variables # Configure n8n via environment variables
systemd.services.n8n = { systemd.services.n8n = {
environment = lib.mkForce { environment = lib.mkForce {
# Database configuration (migrated from services.n8n.settings)
DB_TYPE = "postgresdb";
DB_POSTGRESDB_HOST = "/run/postgresql";
DB_POSTGRESDB_DATABASE = "n8n";
DB_POSTGRESDB_USER = "n8n";
EXECUTIONS_DATA_PRUNE = "true";
EXECUTIONS_DATA_MAX_AGE = "168"; # 7 days
# Other settings
N8N_ENCRYPTION_KEY = ""; # Will be set via environmentFile N8N_ENCRYPTION_KEY = ""; # Will be set via environmentFile
N8N_VERSION_NOTIFICATIONS_ENABLED = "false"; N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
N8N_DIAGNOSTICS_ENABLED = "false"; N8N_DIAGNOSTICS_ENABLED = "false";

View File

@@ -33,4 +33,53 @@
proxyPass = "http://${config.networkPrefix}.97.10"; proxyPass = "http://${config.networkPrefix}.97.10";
}; };
}; };
services.nginx.virtualHosts."dl.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
# Restrict to internal LAN only
extraConfig = ''
allow ${config.networkPrefix}.96.0/24;
allow ${config.networkPrefix}.97.0/24;
allow ${config.networkPrefix}.98.0/24;
deny all;
'';
locations."/" = {
proxyPass = "http://${config.networkPrefix}.97.11:8000";
proxyWebsockets = true;
};
};
services.nginx.virtualHosts."jellyfin.cloonar.com" = {
forceSSL = true;
enableACME = true;
acmeRoot = null;
# Restrict to internal LAN only
extraConfig = ''
allow ${config.networkPrefix}.96.0/24;
allow ${config.networkPrefix}.97.0/24;
allow ${config.networkPrefix}.98.0/24;
allow ${config.networkPrefix}.99.0/24;
deny all;
'';
locations."/" = {
proxyPass = "http://${config.networkPrefix}.97.11:8096";
proxyWebsockets = true;
extraConfig = ''
# Jellyfin-specific headers for proper streaming
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering for better streaming performance
proxy_buffering off;
'';
};
};
} }

View File

@@ -1,68 +1,69 @@
ai-mailer-imap-password: ENC[AES256_GCM,data:gDjqNlI7AN+z7MsQONxn7/ZSxw==,iv:SHOAtBe3iEYzg3+3wzMgej8mWFDD9EzdT5ie0wDDx+o=,tag:aoHbnJoGABFx00+eD7qYsQ==,type:str] ai-mailer-imap-password: ENC[AES256_GCM,data:q9eJ9Tom+X6KxQJhWQTUB61k5A==,iv:FH+IUWi2yZBBgMiL/kNW470GEVHEG3fImf0bel9og/c=,tag:RSlcpXwmNyLB8Oc/K2Epvw==,type:str]
ai-mailer-openrouter-key: ENC[AES256_GCM,data:USt+zJkS+8fF5PTzwT9R1z9oeBYqOrP5pGtmtrnZbJV2gXsCzyTjYXKqI18GrFAAokJdwnOHZ9tNO6w8hGTIVt8gNX4kp7BDBg==,iv:FMZPORB9UzsWHFQqP120z6byVNbjKMSGBRDaL+AvyCE=,tag:x2sNElJK7tgM7FXOVRykiA==,type:str] ai-mailer-openrouter-key: ENC[AES256_GCM,data:EvI0BuCBA1uYOderjAVcB8RSk7un7tiKmgsSe70KQcmfu3CxmQerP/2kQsRTJ0/6pWf4QqNpaes691O3nf+UG1qgG2CUcIaYRQ==,iv:OYEy0xMs+vkGa0qMtY4UP/iol5JPQ0eFVyPpPXLAmUE=,tag:5PeXZcI8TRSUOyuKs0STWg==,type:str]
borg-passphrase: ENC[AES256_GCM,data:Dn5+JCnIHIr7jYA2nVqemWRXZqUpPgk9h4pAP4frdaVAZL1sod4GatXI1VHnH/idOR/FgBEPdiCgqbQ650EsGxLfCuU=,iv:dZUlH0FnNh5GBUXEdFGA0K2xwbR6mgajumtzm2jWxpg=,tag:oJ1WVnC6BZzmx5coLBVp+w==,type:str] borg-passphrase: ENC[AES256_GCM,data:GGmf09zX5wQ8Fih1EyP1p3up9ckFjVKsktU6ZFwvuZnG/O2OyOod66qXc/IXx8GQordubZ3TgisOeMLNnSowp2qylh8=,iv:fFgw/x8Ww9cInkNlPIoE3stUfISbfk46PBj7aimuXNA=,tag:hnNYrkLgt1qJc+gN5s9L2Q==,type:str]
borg-ssh-key: ENC[AES256_GCM,data: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,iv:2aDMP3t558cKzNSLb7j0slhABQRjzPtrq8QwvKRwKSY=,tag:mF3LVq7/RgabYyXfhe0nog==,type:str] borg-ssh-key: ENC[AES256_GCM,data:I7hErgUwWDIxKLdw2FPKuTx9aOcqF/EhOIyU3pfd5QA9sQZkZT/c+IU3b0rNdi6OL6KYDQ8+pMhFuDB7Tcna17etB5HQYnWR6L/QDv5rZTHAHbhTNUEk8w+4zIsvffKJlfObM6qnKZhZbCZT4CvbbvFqmg1KxkFqhpMZSOfR4hpvsNVqEmagygYxuPUSamWgb0y8+CFBxgR8FVEz36tJW8bH7110ZupfkF3P/DNgw9Mci5tYHdrzu6CU5YhbpW+hx88U62rvenWc4T0r7gVcEZYjkVExPJRKs994UVCZO22e8Kx8cy9OZo0S4FBgFfT8Kx8HkYX5viBnwxZrWRsw1ober2LIa48Yc9Sh1cuf1HqD3HSwEc90LuXP0EFKwJLm8MJ3Y26fLLz+NMQhVtug6lAt2cUZj1qapgMiSznw1RJv7nEJNKRx4rKGDdzf4gJ1Nn1DcuS1gPe+WMd2K1WoGB+QiBRIvCvv8f6YrnanNS5YDt5W11vrh2YK6u4JVjehbScoG32ikv2YAYD6HxkNmIyPAwKXM1hIdk91d55bk/feXDLSGsnlBcSdQ7AgBDKEy4UDLEvUPKwNgxur2t9PgEa+AHXZSjhQOpYVEHQI7qzALgSR5v9lEpPLvz4Oa26VwglMwyKcCxB6vYhz5CXnRXXuypBi0TAEtnWsQaUQrcYNduEkNTi5BwtGYJSwD0nVxOFQhqxwbCvQSbW4iT1QBHbvfEGhKtsDIetZrx2DL96aQ0maj9B08O5ODsiQsZzyc68vyS80Ctn6dd/7BIeJka1ciCbXrZiWXHa4Ibrfp4pdjTfLVXhvqocNolLQupwqxXdCMI7pmZYrcQEY6VmyYmuePWOQr76/ed4QG3mhmAJN16SZw3SlDo02LxF7qiQV1oCojpeqxdWnr0jh+tG8z2hzK4cODvwnNx1XCq8O1P8A2vcuk7mzCnB3tTMtLdIGh6kF0V8I6l4zidJkDPRhqB/LonbK6Gj1OixqgL9aJ3Od0ahF3rOq/SnMQWg+MJkJUv0CV9KJCJZ5FoHv9JfChzTG76PsrXtAFGoXwHMsrjJi/1E1UxSFV7TuD48FB1fRleWrt1Zhbefaq6aDfYPCiRLbpNbQlepnX8mRzMfXleq/ESZqXHKaG2dza9R7hiaJFOd5OTXrqoaf3spIRCdhSAhvd6ChpD+NtFssqUWaAXmZuVQhg/udVOGGW+mOgd6pRKoFjOXkD5bnVf27KY+w1EHTOUNoKWtZlLWmmsbJBF8MGc3E5XIGM+Mlq0E5mNSWB/XwnionuUTGg1+gUUCcCbcmOm7P6J3k4l3ZLhuu4ilw+wV31pjednAnw/IIw9r4lMGL0RN+zJL7leE6FT+fFqdjRx3ZCdCFGxjrmJgugtfZECjgR6nHQWk2zLv81fi6OD38wBFeeMe7Bm7fV0A7+oHYrTXyvxuo5aXfv3pOacAGnUulbhWUIcsN/GTFzcGio5iBlIOI7yI6EeW6VcLbXm+ISw2YHv/zhMFCDrOeXn5jYCI5zwi31+T+XTIuku0DOr+k7WnS8WKWQhdlhuLUp8XpNdUVwegfrZoU3xWo46WbUWrtOcpBoQguvjGvVYelb/FHWx6k8mQy/9Ke7juaPXgrhKclAA1pyKuOvFjO8UuUI/KBMZGTWtnAxrS+cOTdf8CBhw5smJ782wyvYkxQJfddfcUU42TWGLSpLUJUYc0kAjbbAEfKkWQw73tCPH68VPSq4/3nWH6lS95bMayn5yj9B8pnl5Ml1Fv7q1SAj/F5FrC5uLB7QBUP8alKQoLvpCCkXwRQn6FHPzTPoKMmkmRCuqrVmG0blyyY1PtZq+hzfYw2oM7kPwElXKUMo+Pd7bJKerRcTRir0odIMyKXNXCu+OC2/WhcLLfwuiW9PH5clbCt3gjjM4XSMsz/vxxwasuN+WnzFHuTlL2DN21rSFNKjPR7b9FfifQObQQCfEE+GBAeid8OsLU+7Uk+0PvjWq072HOMU+fkbgdISLp1wmafOjjt68hsnxx/dtsqTxaE+mBCTnLlBdSJOHi9JvcAC8QZD/fHUVGd0EpItUQJCePaWSVjkg3Sd5wYR5zko7WHAW0iuGXqZUQkNJpMm93w8cygkAiNJMffM4FkCM3yXpSwBqpjN6+OHNtcHzDIjwwUCMigyqHKvCkalcsgSLus0451Z7m6FsUnOvlkIZxWovQEQVqVDwa42g7EqPyaVghaUmIsUo23Dlz/eNPM/HmgqtdQN6+vJTgK4kdkX2HBqEpPvNn7Faa1hW/gfLIVl2cknNUNDNnkmqkvJLPSUS8Fi9lNhM5HDt0hCr4JmmGJrvgo2TlxnMpBGUPi4UkfxEuFIgxmzvpNhSpyDJNF+nFp0UD0ztcCdtAta1lpoGQ0zZEiTr6Mwwc0fozxerGtxFLPm2pTSgjXKI65JyRGJjNHAHG2XeOC/7dWnwo4GthFCZBKPB8/W5EsaOeuSsWfDAP8n3GW0AIVwIh8Pmf3tdml7dKU1J7ciVnLqoHE9NcXfmnRW6liJ05Ca1UUgcrtQasQKETbSEIBBM/hvZAMd8aaazyIBRJwNc67TIEkKBpvC6dRxpUYEJ5ZwVvWGrJIkh4WekDEV+fFZDyH6V8sngTmnT6Z3GCyRContJLg8Gx+CE6fLnZ69OUXVZ0FU6C0K7GUqueB8l60ccJa5AniiEqhRk/5WQAJBrcpYAYrYU/WTEKkyt7EtBnxeWPCyb7CpnF/11i2JOk33WAcYeXUK0IIfVaPmW5cBQFDVZ9JX5W/kfEMwufizXqjWSMXIKnON3ICD74iOJRmnMevaT9/ruVtMlXx/7M4iQVMeIl8+0He5S5+U/1kUZVvx4OmvQBdfgIQrjso6fTDeGMziM5jeQeCUUWJg/cBV9VGrMsGlELiZiCWvVI3Ysa/H2LkutKa1dIRHtvhZNBan4y1GYq7hcDpkmBJ6e0WHV2gQxhgbwgWpOrf0nww+KUivUJySz2uTdRunrujkwCMDIt7zDXHrItuI0xAfrENYWXvp+KyOMeWUbjUrOUc0IdeAD4DIBywKVgZD/2j5xtew53d0wv8LmcBe89dKSyMHWm09ZPjbwm5FL+qafFLFZZYSlfr/7f+MFg0kK1Fd51rh1pmO9vpgha15MrzgK1KQ7FBsShG5UZ/7l57CPGV7+TS8B6ujDbFvX3vvmwJ9LMhPsOkeh2f8yQGjZppaOjx7WorqCnjP7K4HP5gfbz+GC90jZ8xEabm5p95t3tnOw0NBG8peE0bObUosd5phJN3PuahcCYGNF9WBtCAsDR9WzC4vJgwpSRl1qVZydm/F5c5N29cszRViM2yG1vIqzbUo3uGkN4+Dvr82YB5rRhn25pdV7Z79xroJMnx9mDGAM5ShQPDloEZeG32i/rGB3PKruyfCYNaWUT48qrvt8S9FTrt0etZVJrxFp1v9M7Ct4ojw+OQPW+bh+bTIWd1UqY4TGhAeg==,iv:f7rBK8aNqX8dGyzjoeRX6yl20XsnLU8b4gitaw9+O+0=,tag:WvfUw1JgFBAtS3vsVIvM6Q==,type:str]
ddclient: ENC[AES256_GCM,data:YpHkMyuGBR9mANOslg+Nyy9ZJeNsS/tv8sXZ3RwhONo=,iv:L18QQIn1te6rjrel8dS+89IomuvTl8AVlfjzf8aU1UI=,tag:V/lj0focF9F/2Njlf0krBw==,type:str] ddclient: ENC[AES256_GCM,data:dS6TVVNb6R7EE1JVMDfSnRYCZyHHqEPvwaYpkTSj+VA=,iv:9uMo+9X7dFdVW4wuSgrqIAaQelXuA4cek2oif0GRHow=,tag:ncQq4UeUzWtjPNxEUOlqNA==,type:str]
gitea-mailer-password: ENC[AES256_GCM,data:kZyWqGYgSgHUuHDHwip0YoumgvrfwLrPMhKBJdkTXo7Ej71YAEfYwTA2l2dVPytypxMgO+6Yvb4vsG9P6k9tf1y3,iv:d2JGAjTfQx7r2QX4WQ6mWTdb3pg0RYGa7s57ZQc6DpM=,tag:iQp6AVPXtQYKeKAomBhGfw==,type:str] filebot-license: ENC[AES256_GCM,data:jY7E29fFJ/h9NIgIjuX++WBhnLk6Mm4iRfMh4P0pUDdqH231gXDsTZ6pJ1rpFXdEHSuNN4LfznDTKgZ2azKid4WprDUzGkN0uJD6CfSR8gTIx5Rq0M8vkRah51LC36bop4hTMzECYQd1YA47hOBV/gfyg3RIw95coWamV9FebnQjIBgWYxE+wTvO5iRvWpiCHd6VZQfkiiR0KF1DrkYkuxlX0piGEKmIgyYCiKMFZ4nrrIe58x5lEQA9uPVjE7vmq3c3ge6tJzjVVaaNocbJhxhA18GLMqTSHfnBsOLRlA8qSQ3xX/VRzKQmaYQHIM77Ylb9ZQsvFt6EDlzQMl5NqT7OJZUW/0jwNaEXHURjeTOC3Hr1HugiDGm+uLXEraaJ6Na2AbFDn28o+3J22p9xNg6vWL0FElzKuaz5TFDzdZLZsD9HOPQm95/ZM8JymDjN4qxkkd2o9rEKY6to1MVDarj+lDxIHhf4pL23YhZsn3esNlEbFswzHQiH7nMsu9Jg6a0rPu7IYylDnH/soBjxSKmf2dhH1LLsDm8It9K/7NnXwmvncFXaqNBqm/e7JzCDBCCyVUf/BXbBc3xwLwZf5MiirZ/iYiYnRtUssveh7BV7ICigRj5Ewtr+n97+IGI+FyonkvOgM0bn8nHf79ZzJCKMuntcw3FlGd1nIkmcehkC79PlKIS95oV/wypl1OmU0CVel+D8hsMuONmF9NPHgFk/ztp4GF+XXRO4ExNotX1XrUlvLOccoHDsl1TedUOISzgAK71edxfI8y110shIe9OfsCEUAbmWMmjGVWH2fKu/IrYYQTry6pYFOjG2bIEUXMaiIP0lALbq/QNgleqMNPY8wGzFbP+/jaYzTbw9KXH4bwYQCl1hSI8THfV7lLE=,iv:4ik/aQqi/hIqH8ix3ejgUiXGY7ycw0ymdVrV+CEQe1o=,tag:7ymc4QZEezJVPlYTlU4H/g==,type:str]
gitea-runner: ENC[AES256_GCM,data:TH05utTgZDpv9AjPnN9AUA0DUXWQvS6D9vg/6f9tfRiqqKIFBL+V98VOSTjiPw5x92aru0v2Y+XBYhbw8alYnOuk/I5c954qotXNxCKwLW1i74eA4VOf6t/gY7PjqaTC17aG7UIxTtZI6rJYLm7vBonQkaJxxE5umu069Nf67cyeVU/RtvIzFKLKOxOC8qej70j9vj0/QX+QMhsX8D948ia4oH2a1jVENZO119rIlvI4byh+494X5MTVPatMzR4SVlgHLBC9joXP6yxOOrZa4AI=,iv:ooNmpw84JDuPmezQBbhRx3cDI3+3ds/5i746WIwtOU4=,tag:ksnnPCj1/0Mhdv5JnjjElg==,type:str] gitea-mailer-password: ENC[AES256_GCM,data:lEv5euTCHG6pyNqrVtKK7oE8wLvk+q8ABXOzFSizQ2TVFi35lyGPzOTel/dCCC0Je5GAHE1KQQ4Y4/iHghZgb5Ft,iv:gt/mCzLbDrHFNqW+Lkd2dy9nRIBKO+rqsVuXM45zJ8k=,tag:gCxTSzY7GZ+jQP9SCsdUtw==,type:str]
gitea-runner-token: ENC[AES256_GCM,data:0UcHRYJLNEbtm15SEOsuHbZnVvNBdE4cP0LU3A+e64c+Y6KgIrrDbmz5n+YTuK0=,iv:eJjMSYld586EdNf98PGl/t2rw28mw4LIUWCZODorAGw=,tag:+O3UVRyt+1vUMZXimGjXSA==,type:str] gitea-runner: ENC[AES256_GCM,data:HLjSETmu2C2ROf6kqUuIzQl/t4Fe5EOVkMqdTeLNnb6AJ95l6M/WUk//dnPMrWVvEq7rV07awUiyvyJcYQzMgPNddCrfcn2Xr0dYK4XFenz/sdhknVex9uS/RhK8fOqdYJ6djpynikMKddZMQr9AOVfpF5mea//87+Az9rOrlzLdgNtf5HyBEAFKaOFbkZboAsP+jlxyyYurGHPr8LxxikewDVxnpB+XzMc6RAnesrZPOTDQlkMiPZ2t2o0klhD/4VomgiHEklULxCCmIAHaqDo=,iv:1FwTespqVTnKFbyf9Unbbod08D36MKsVbDhIBNGBkHg=,tag:rgVvyxUCwzYB2CqWm2fwgg==,type:str]
home-assistant-ldap: ENC[AES256_GCM,data:hdut/WfbLVFGYZqx2rmOGNndt+K99LkyIaRVPLyqUesCG7gaRWrWH1XqU8Cnf7OLHP82uHL4JEMrU8T3mlPC5g==,iv:pNmFTlMKIzoDvodYN0WC0ZZhIEgNsdx6Am4SZdFz1tc=,tag:DLwIb7T2MTTjMiyMuqX6Nw==,type:str] gitea-runner-token: ENC[AES256_GCM,data:pzJp7j1Ktz+27oU+qtESk7D32w7+BSEUkPSX4xuFml0i10z12Gzu0QHXL9s3734=,iv:U77b5515H1URfz5BCdzuY03zVkhSRsL9d+HdHUJFx9U=,tag:QvooaT4TS/X5R5KGdaVpVQ==,type:str]
home-assistant-secrets.yaml: ENC[AES256_GCM,data:+l1eSLm3nmujtaxt4h/1tbwxS0HkmcZPpxiGMxdKBFksZYc87EEXCnqHNxML2vGW6QDF/ygL73z913BczWchwIySBnXpvNAWdSZ/Ba35MwKVc9xc8Kt8CwpqK0o+rZDrC+B6pkcd0pwo5Byrho+kxwobUaCBn3R5nyDtnB5H+xg/ZPElGLdB0Pv+bceTIUjEXTgqx4MtuNVIsbYZW73optovcTHqaYUDWA+rf3lKGdB2iBRb1Wmdd9jvtB2Bl0PcRHKPlorxfW0ENw==,iv:NMk979DIMl7xNTe6CQyWCRjAe/Ru3qTBdGf4Tv6hGo4=,tag:4LMh63y0hXbDO0k+fHNYgA==,type:str] home-assistant-ldap: ENC[AES256_GCM,data:4kofJzPbiLXILxjuAZWiTb9hu2Gver/IHBCXDnrmrKuCSII6SJ9FrSi67nl7SHdoA6xe22GSMfmPrKzy5sGiow==,iv:F8mIHhWHpaI6kzRV9du6uW/Fj07PbEIU1goSDmeSD5E=,tag:6NIC6sN8OclinribZhrLLw==,type:str]
piped-db-password: ENC[AES256_GCM,data:OYxFR6WOfhm0wy9ooyXkDaNkSIdkW7HN5HOc01Ug7vmp2XhJIF+y8zJzhas=,iv:+8L82xp492lkFwHV5Cc7EAZFBMIlYoHcmuvHfSBwOXA=,tag:ZpJN4Q86dh6gx9A2tRurdQ==,type:str] home-assistant-secrets.yaml: ENC[AES256_GCM,data:rns9heAmVMxB6WWlGMXvF/ianFUnja3FObiLTEKJmodePNsJ8ah3OhuCAX5jON+/7NZ+3JN/hIJjXsORC5WYhr01DvO9meykf0aMpbmAnYI+cmPEPvcunF4NNInl96rpcI519nMiHDSh5J7pD74CxHZcXSV4c9ZR5UBymchrwmHyZMF6dVrD9Jbr9yph1r7iq6S5wlI2ZImWRjaoGDZ1x+ZU8XnsUmYcP4pa1Yt8JBxSnyUw5gxgBkVCh4eSZBsUCt0cd9P0i7qWVg==,iv:YXQsawXZsQb9ZUt1/lkpfTa4tfKIQrLkkyShFtBRaIQ=,tag:/vSnipGiMntdMqHLePSEQw==,type:str]
pushover-api-token: ENC[AES256_GCM,data:jtf5QSHPteh8WuH6kd364ft66RfOe5itZxKxX9ek,iv:B0r7d59BMt1/OZ1dpADojxCS3R5y+g9begBqY+PQE+k=,tag:RbtViJdaPyN9AYDt8F4Huw==,type:str] piped-db-password: ENC[AES256_GCM,data:5atQccdHYDEf638bpiON9VO14jqNDtzZ8nnXVW0/cqtWkZJc8RYn9N7QhAw=,iv:Gwyf1R+mpmX+TFuoYLPHjXwSDwzJhSEpnj5ZsJgmrtk=,tag:zm4zNkzbqbCyTN6o3lQQfg==,type:str]
pushover-user-key: ENC[AES256_GCM,data:AKbD7zPcus1/pTvc1oLRYqOgRWUuMRTqgS1vK8u/,iv:DF00iWC1wGSIYnyC3aw1HUQ9O0kO7rxwiOJ/XGT93WY=,tag:bRCwxx0GmbBpRZAXm35SDg==,type:str] pushover-api-token: ENC[AES256_GCM,data:cMBDdySEBQ7vS7FUC2DsCcSvEMpapWvMFmnuCsY6,iv:SVDrrDm2pcAfwUVAC5j47YwF4s/FWNARlZdIZ1Wgwgw=,tag:w7ZeNMPXWc9j+zVaSxq1cQ==,type:str]
wrwks_vpn_key: ENC[AES256_GCM,data:OjZA6MeIdYP7adTf/F8kp+5igdj2dpK/ze2n1BDzaRz5RyXhGuQgdp3CLec7JITwTORmJ2fgNjBLO6K66aFG3A==,iv:vU4jBV3uaG3+ZgJhJgJDWk/j7Y4azDT1TUyah0AszWQ=,tag:ZD11XKdLNB771+mnDBp58A==,type:str] pushover-user-key: ENC[AES256_GCM,data:fjoA2YQxmeWEbSKWWE5iyi+CUh1vtW9usVCm5EGk,iv:p4YwYIhpgn/bY9t61//CDrDmZrsj9B/naZit62lCpwo=,tag:pqEw3pDlX7i87tE0Nsy0/Q==,type:str]
wg_cloonar_key: ENC[AES256_GCM,data:c65a3mo4ACHzZHvUFEEYjR0op3i8LycJh/RTTeBhNDRRN8A6jGJgyPMfA2c=,iv:vADTTX5wTP/F3x/v4HqjZkXGlVDMnU4XRLmONdp9/IY=,tag:mRgweOD1hS1GZxXt+HTK7g==,type:str] wrwks_vpn_key: ENC[AES256_GCM,data:VEHqnr/bDtmyLzs0wnmZ0jCWS0BGJWu6Wjq0ZHJuEz8PH3j/E54S9NUe6WRIo+BJCsh1PlRqw/PD9xSqlW5uPg==,iv:OMP0s8Lc2CmFgwRuwB3UWJVuQFqvpy+BiyhnIKbVIb8=,tag:x1LvSf6i8khd8jKgv/284g==,type:str]
wg_epicenter_works_key: ENC[AES256_GCM,data:fDBZK1JK19CfCgihRP3iSlMnJc2SoGFtgnfhHgWI4Mpze9+u+Yn/PYjJl3Y=,iv:a+2HrB4rgWbTseohJgmpWIgcOfLqnOcIQcPFhawQrXo=,tag:7TYOVSEUGznnEjYyd6RbQQ==,type:str] wg_cloonar_key: ENC[AES256_GCM,data:1OfHD8yX+pgCXqqxn7cddnnCA9HBjGra4eht7uLxdcbdG9vDvxUoE1x6aWg=,iv:/NBEbmA3wP/zwrqCeBKDzaoSMqz3f4ZeMlWbu81R5Pg=,tag:Apt8x/j0qiJAKR4UEVSkrA==,type:str]
wg_epicenter_works_psk: ENC[AES256_GCM,data:kaBPIzjINEWQ+9iHJO1Gm9WqLwfR4pwzEgfXcEqZd1a8G6AMW1CxcBMsPHU=,iv:oex6RJjMmd/6RQ6I0NdpVP3b8u5jyLkwQfFsVRZoty4=,tag:dIocYsfpddTfQfyF3KjHdg==,type:str] wg_epicenter_works_key: ENC[AES256_GCM,data:CTZkVGEVRlCdt6W0BGPmX0SZbuBBH5IIlUsi44SGXi7gdmrZNwv2zDv6zjA=,iv:4ZDDKqR6pBq8cjX763tBxOvWFaS2IiGaBxJu6L2JYig=,tag:H8p63BvXSx1SKPFw5gnptw==,type:str]
wg_ghetto_at_key: ENC[AES256_GCM,data:DBoVoKRjA5Q/uA5pG+sky0WTr+MiiE1YuFD7ucY+uHydufCP8UdLsl6TvN8=,iv:mWMYw7naZ9u1FS8fNswnXucrDxkgTFLnmfAvQUg2cQQ=,tag:jQnBhBRGy2yD2yr7ZYRjvA==,type:str] wg_epicenter_works_psk: ENC[AES256_GCM,data:K0SDlDWfUk9vIGP5U1j8p6TJ9GsydJTuKPb4kMgde1CILOia0S9/+4AkMWY=,iv:ITwLoWZXR6NxRFF3eBvOogiWHLmXnf7S1e2FW0ofr/M=,tag:2OVi3OBFYT0nlCx8gf2AdA==,type:str]
matrix-shared-secret: ENC[AES256_GCM,data:nV6HLSdXi0BjBfeIReVNGk/3D2bKPC19+Rxfq68aSNAXMGfNlFj1++bQbglTC3ometwAXprkzrIpX1Yz0Q==,iv:n9cXQTpY7GDyHcoVF9qdY5COOkw0CbKErRgiGEZFUUU=,tag:laXNDFDe6yCy8KBGrcVw6g==,type:str] wg_ghetto_at_key: ENC[AES256_GCM,data:+bonpVjV1hxwaqtR7ywshmoDxCnFPD11q0OiNLzxUJIaYrDeS1srpyo6rlE=,iv:Djn16kuXTWqJZy/AT77GpH8RcNtUMZ6zcIdKIMHv+PM=,tag:LP2JCaPKpzeOKvBc2bMr4w==,type:str]
phpldapadmin: ENC[AES256_GCM,data:l6pT7JaHbPiqMzVceuuko5kxgjTWaODKG99YMVBT604epjF2k+hg97EQYZOJEJ/xoeEFq+UisTG37M79HmVVmBjaJcPW0xDgwoGjx1e4BdPV0aSnYxcuX+W/bTmKaakhU1Lsz2W4QSSCpWGIQkUdCm9li1C1MHFdC5xgfdK17ejvI6u9Xi254Id1TrOMe1Ea1ZYf/eAJCQipP2srRY09q5eXM271ZxGY7axqUiGBVkYI87daoJovAKL/Un3qQh6X6f7A3vg0isNLs2bCCWBd0j9bIQXyUkoC2Jg1Ymv+Du02U80TVpxwXlfSJv3I1KaI9LRnFmLEpd7mWZlZpFdKCf++RY7xMHk3,iv:jJ/fq6+MIyOlskGU3mLLi29BDZ90mZ2RiC3a59IA3z4=,tag:wOYWjSHYM8YfvNJDb7qNuw==,type:str] matrix-shared-secret: ENC[AES256_GCM,data:nVSHwPa8xYUaDCxL+5neFtzc11DDNzJtoDCSHYXZ+bZXVAAbp6/Pjx6UkTdAA8B2GOM09nFAsBuLnQfJ3w==,iv:WU3hnRlWVwx7Qin3ejw7V4VhAmYLf6oXzVk6xQgZPgA=,tag:O2hJ2q8XDxYF+rHPNgATgA==,type:str]
palworld: ENC[AES256_GCM,data:R8lcg+qhrSqpmopN3/GJ6aE2nej966Y0DsS3RNHP3iPJSP9UOiy1mgdHqUObSE1s76gMrmmKPp1FyJaWx5eXL2JrtBllwSQad6GHxuyqnp3LavyOZzR+dgik/2qcOrzHzL/yXcEmwCttA+U3Be+so3/y2Y21x0uEOlQgnOfcGYkk6Uptv8t19fWMMJxFMGbaWAlLFbCIvqyjhfcXwZc7CR6g4pcVNsNfdQFKlruzoHaEQCzq5bTu4oqlWFNqk25ajRJk/rNc4IAFlSiCV0Uw7KXLkUHFodKA8nvWwN5eaZ3FYabMFh4lo/JqA3bYXzQiuxaTPuQOYWRJ3sWGn5NooaNL2FaVu4/FTxrcVK/X1OzG+SILxfyIK5aD3pS28tvKxRpXpF3e4jChFToxfYTzsiwP5WwmBowKNedX+yrTeUaLOarKUEdeD4LbBekZdOpXRFMGlwhJ4vkxARHMxz5ls7QMen1kTOUhrQEO/ny25rkN7X4+UDiIaqPibTi1oHdsLrUetIhwkUw+3WGhTtSayE2sEA/EgORJKqYEiyNAFJ3rF2F4nYnMK5apSACfnYSomTmiAiuwF5IhybvI37c4C1g3OdmTznsX2mpDgZwH68fd083TQR3oLeiqRxct0XEm7JCG2zBfCvbV9bU1zhaGhYuHg1f+V5a3hetvvd4aZ92qNMjpNz36/QGvQHBE5fZOVosgwHPeBYqrSTjwxA==,iv:b+qj+gOd5tGNNTrZ/ySph7ynlcBNbG7+wNt3DKhFtLY=,tag:sVMyuB8dk6OMamy5lel4HA==,type:str] phpldapadmin: ENC[AES256_GCM,data:94jCcgGJ89Er5ENLqhFZ1qY44Qp709SuUhBUuED6v/a7mPPjrJGDmi0Gm3r1Hb4CDPGkWf+x4NStY7LSQ2bHEzjyMPMS23wvSLTmC5b2TVca1UI8vZRTD1R7OvdWo8d1oNweSpYEnAXGv3USYF0NZo8DrPLM5G8lG5Tk/rKS/mxU5ZRhPyA60rbmIiy3Mk4yNcs1tvTEckxU/zMVl7zUPAsOOlmYGuwJrHmmh9p7YIWHGIgZNiLs3U0BvSKzN7WktmlwqjfWpeLn4dusqgov4SSQ2otAkxLHIH8mGhyotd1wgXJDZc6tilMe+WPHQDz9db7FT0VdeKggQ94FD+8rP0OsIjR4AdjZ,iv:C8X10wtA9jPgS41pxasaZJTO/XFcRymOyTDZCWJlhmg=,tag:xkMJsGubny+Di+GucAqypQ==,type:str]
ark: ENC[AES256_GCM,data:RABtQqMaRTpu0rn6H3O33GwoMcQpwoNj9kN3tmSYA+TtJlW6vxgqvwCB3/nNRwZ5E7szHynDP6/uRZGQkhw/NZDnA1WU9CAhncr8ad4Al+pmMqBs+EN3gTQPQUZ4JxJZKuVMr14i+5ct3dj09tddQQFbfLVFsEg8WhGPIKxdacvXFfV54bNUbTa3gVvUvY7qucyXJJHtuBRbaISmcQ5lsH7tREhx9jfJZVt87wb1VpNpMTRNAvxhrE7V/SM/DGvwj3XBfVypAeq5oXry,iv:7TfB3UW9ey8BsdPJQvkHoXQkOw8fA7hgeiYG89uK4Fc=,tag:Td/W1/2IdTCPwo7A/vZnaA==,type:str] palworld: ENC[AES256_GCM,data:iR9nceVotLKrFHnPIVskCYVLev9OzGLLlmfCGQq5hqB1HveXjhjkfm/NMmqnSi9o776+Ezy7l3kkS0R+0cFJ2B9kaWGsdJtdYDwQevmf6Nq5eaBYmvu8kTnaatqZ5e/1BQzcF3to6MA061XL54YGqsAV5FpnDVLhyyzIvaR3gMvMqJ748NL7K+hbBqMFuWcSH3hKXwxtDK7SLtcgx93W5ZgXkZMMumtlH9hSSlZL4yxuQDAQUwHrEBL+rdphA0m27dyS87DA3Av5ZL1MZ+Vlm4uAHM68T+rtVYXTakNImDTc0WrhIP8FZD/UKTAhVYAbA9oz6cbeC574vchuEY1z19SY9+2HshZZBOiPDMqdvrqyszMQCo5I9dUzAJCemQQTlYG8ekREQ0wxARnBYi3iy5PbmgDQWdM3+ff4yhMmGiHtiMQLHzrquKy8nvS9lDp9uT7njkaI0QAt3eNWa2DAQRqXQAtmuRVob5+GS2Nt6XMTWRkbeEb1phwbTqZD5mH4p2TiyMKCn6KOXsgQTxqGr35Izbe+bfptCmUeyscTKq01IZg77w/dvg3AX4iHAcMNgJ9LIHDLibGHQzu9fGN6alpeyy788GDwRY4glYyKxPhCasKkBSj/uhcDAtdg+c63vDTBhqRjNr6+v1NeRW6lVBzrgq+f9QvO5RVKrvdsVHnTA3CMGQzUPAaluNZMpzV5KqxqIrpAAPXnN0ktig==,iv:kkcm/alLHwC84IKK//OJpa36ec9ddOARTIM+KJlOHHs=,tag:jV1DjfNzRgNaCGgJTKIy5g==,type:str]
firefox-sync: ENC[AES256_GCM,data:d9pagfRy2H3gl2NlCt2+LT6/lSXk+A2EjIx2Jv7IHafYGu4Euhfay7Ku1wCdnkM3x95b7x45uLR3X7r/TDnl+kVVc7xSp7wUVJAo1EnUadYOVf61hIUf/+8odRUNfdrozOONPSFKT2va5jggx9r7ahWFf1xAyEHRtLBNkrHxNMYlwjo8x7q5Z00oyILH/6g=,iv:ZXCQ2loWUJXtXwMwxHvy6sOPWi0yLr5l8gbLxFOtmsM=,tag:iPKbePsZkCu76yoJn0218g==,type:str] ark: ENC[AES256_GCM,data:TRTwxqkeUGbtgrWuj1YEFr73+nxCXmt/fR5vVnYR+k4FpNBB2FoY/gXl0kqeFKPDcajwn8nYBs8YE9vmYtAX/Qs4g5OyU9qC/pkmSV7/gbGfqLLqcbIlbWrZzeM8gRW0fp6h1TMPsGO8/iYdF4bmInfuZW+fKr0i7ZRgrtOpPiRCOI/ztPGkFaduuwGIy+yVoS64b9r7ZLRnOZT7ghVv80GKorJuuOQIipNAJMzEqtSA2IqaxWeb13v8wdQoKuMNcD6dCYVJnvgwf4R+,iv:+F9+yJUZBzPSSIt4uLHxjjXAjzRojLxKAyrd8grMXkk=,tag:VrIr4FFbIGTq9RBJMz8/Ig==,type:str]
knot-tsig-key: ENC[AES256_GCM,data:1WH0uQ399Mgm9k5dvVzs4DvB8oF6RLg87vQ8EnxZ/FGK4kSWxfJ9fi98KOo=,iv:KsIWp2qxQ3gb/wx0DOfK1X5o1Joy1m1PJ3EfCzVXe7Y=,tag:5suSs5FalS5yKRcHC+uvAw==,type:str] firefox-sync: ENC[AES256_GCM,data:guNgEVi9n8uJuLkkX2Z3tMY/NVqzQ2tdIutZAqleah9qBri0/3dzVHF2xvztLeAgm/59tN7TtAlAH2SMK6gcfAZDasAWOJ/rGEASxLi6VRjqCe25glDMp2YrA0/mcqZVYMCg+QZ5OPA56b55WDqPHPoBJkPDuTm9axwm6AOxdNi5BkDzMw12fVBxlJL/Rm8=,iv:yD+MkZK5vvZ85vYGd9X2Dv6KkSvMUsMGLrwlJ1pRqlk=,tag:YA379QupHh7aJZKcQxB7bA==,type:str]
mopidy-spotify: ENC[AES256_GCM,data:bhNuJ06YbL07eM/FgAWgxbAqSEI736F5PCM8zBdSisffDk0eU9pVAJMFd48626iS9QvgNsnm1ljpCjGsQWWBqrbv2LeFgB/nGUxOiKOHURBa0aJU2pwLMYbv9WKtytdlYlQ4bBfZ5jSTkhgM1kFP2n6qGZPu9CKK,iv:F9TL2l2mmALwhKyShw1ju4YOfHXav/WwqAsEXFEVxac=,tag:FH5h1xoFjQngaZGy8g/TyQ==,type:str] knot-tsig-key: ENC[AES256_GCM,data:CBFaRKPr+HRVM01fA9/OLWeD1O33axQKEKJuqDRfcGmuDeP3oXf+ccEJhQE=,iv:2O5y24YenpiMc9txPx8kz8x0aO37LpLjIcwlNywPEak=,tag:J4bVZ7RNSR9fiOBQ2HKpnQ==,type:str]
lms-spotify: ENC[AES256_GCM,data:isjXS3VLWC9qF0ssgUXKOFL62WZBZSuzVI53lxo3d6bmsMdYPzXMtgIYh0B4FKIuXmhYH9ZWAXLZCKnPLGcpQJp3ooR03xHxSa4WZ/TcYJkQtQvgYBCHvK+PT5W0W/DHnhviKlzEsDg/cUtmeJpD4932q8y6k7hK+0M0N+2zDuyApCLGgxpA+F2PwNaG4fifWSCADHIMZdPb3+qT3Z2DmIm1tzBq2AJcHc4TcwzT8sbvwUbngxxqaFFHuTDA2R9gu+K7FUHRXg==,iv:bMpkLKDK+uc0YgFZVbJfUvvJ+MvtHbHOeemxp7fZwf0=,tag:aelZoqjfMFTE8cvEywm2YA==,type:str] mopidy-spotify: ENC[AES256_GCM,data:irBeIh2FieNkdf6Hls/Oj+qYxj1U7R7/Ffq6dx+JCS0PdOiFWIHXtccY+PXPKP7RhhaQOgZtIcgPyqTiML52P0c8AwN6UHMl7kgUcKnk60AI0IUZNWorCBZluHhEpf2e2OISlFzDGjSHk+zAzh2eDS1lJ9lCRYEC,iv:r6aZmlVHdRsA9DxkelcIVVpwwm32jaOgP429h61NL/U=,tag:FvPIr0HX/V7+G9kal4nO8w==,type:str]
lms-spotify: ENC[AES256_GCM,data:E53aUSNxE30SSrG6Y6SWKVzmsv0lu8aZvjk1RBgSj3q4m65dPLwGM9HcagN3BPoVTc0tKJaccrjoL2k5FOMnwcTXIz3qgiZGbnB6hVCoOhMrrkoFRN2JzSIA5WxKOT8VuMoC4/a6WaWbY8SWAdhgRQb9uq1hUxdkMCoNRLNJnPqR/0w07lCDVHvkj8XuBV4rGl93VVT3rCzjVTL+Vigv38WZ2il2aANkCz3joNeN8Uod3K/HA5uXLw3cLFmD7eI7LBDSTHpMEg==,iv:iRKrij3TRaufB5BXy7Xhiu3asClZ6hpkbMV14aod7jk=,tag:hpUwP/OHygqfgI6j6q2sKQ==,type:str]
sops: sops:
age: age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWFJ6V0NwY2tWV3JmMitQ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTDFvM2l3Tm5lU0paWXpF
a1NVMUtIMk9HeGVoTklXaWxFbVE0S3JVN0NzCkRPOUE5ZjhhQXFMYjJrajlmeHAv cjVBSFhENW5mNG9DSFM1NXh3UHdaKzlKMGlZCnRmNFBFVWY4N0FqLzF1bUMyUDdL
NnpPRFlGYzYvSEl0U0EyNWc3eXYyWkUKLS0tIDRWOXFhaTVtbmx3ZjhSdXgxWkI3 U091VENiVFhYeEJ5K0xodXlHVkhHKzgKLS0tIGxta3A2TjJiMUtiR2RzcU02Rys5
M04zU2ZZcUNIOGN1cFBCdy9LUC9lRFkK4opoeV03v2v/Zspie9KL3+JG2McNqA07 U1c0SjRKK2UwbTVIQUMrT1pOOVFmOVkKY3UyGNIPZJLE8GG124y0pLgqGub9SMCq
jlN9honxmFK+aWQBwV8o0OetMcx4rwVYBquaq1QaYgMniY/0Lbc/IQ== plK5H+kASOB1X6pK+3PBFuDYT1AbsRxXvWgAEMvVI7eBcxQlSrrB4Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGUW1pVGpZZ3d4YnpMYzJv YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaXBqMGl1UytNL3BkZEhQ
TmdEbnBPQkhoSDZNQjhUZGpUSkVWc1EvUno4CkxCR1ZPSnhHRkFpR0M0UkZpQ0dp S3RFL3lZRVZKTGVRTGFMNlFlWFRCNDNvRTM4CnpWZWovSDZaclQvN2Vwa0dWZGgz
dU5qekhPa3FwYW1KcWNVcGtCdmhybXcKLS0tIGtwRlIrRDdCSUZvZzJkUFVTYWVF Q1ZLM0sveXBxOVpvNHkycWJWWXdmVE0KLS0tIHl2bFk3RE03N01IdDJPWk5HT1Np
ODJFMUFaUldtNnZwNXVhWDdZMnRldVkKqB7wzQTBep1tHGrVfSPzUiOJ4XTX0/jG Qm82Sit3Q0haaDdnbzFjendMUm04Wk0KYp09dxXjzvC4IlH6Ilip8YjTz0mFeu/0
CGdx7b28c0Cw74uLpr4cJ4cdbNiwB42EdxRLCWfoI/uel1o9JAjVLA== 5IDMYjT1BuW5YiKgIJVd+UgOd6ysZLFFwk+Us2AcV7z110xk/askqQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaZzU2NHdoRmZSSWV6eko0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Wkd0YnBQRnExeVdUTGFu
ZVdReHhCQ3YrN0JIdEF4d2F6Y0EvZnYreFZzCjRnR2F4a3NZek1XSnZxTXBRelht N3o3MnF2aTY2NlBmdDJYT01zRytWZ2w1dFg0ClAzcnJ0NFYrVWlBM2JQU1B0SEJi
Wi85R2V0bjJFK01LNzg0VVhYVDdNM0kKLS0tIC9wQy9EK0VzeG00R1N0NXdtSEYv MGE5aVh6KzNmaEoxaHFOTW90K0VmMGsKLS0tIDNkOGZyVmMzME80TlBWMzI5UVR2
MkhsNVMzZTF4Z3o4ei9oMlc2c0Vsd1UKB8CerMKKPOJlScRQs9+jrR2ZjGJiFlRh djB3Y2FIRDFKWlEwTnRBUnRIT3M2OXcK+SIt/7DRdQi6H1AZooJN2Pt2g1EwVTZe
mDl5FHHI2Dq1lbhZ2skuXSVJMEVlhNLUxgzUoyZ7tEijpbc9Xp+BZA== Q14cEt0sLyVYzLJugfz2JWRHDZX6wPueYcTSEs7w3wAPVwvJWju8bg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df - recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRVWkvdzNXWW5xYXI2WnNE YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzQXhNSFBnNUtMdkpwR0th
QjZnWTFlMG5EK0VRR2E0U2pScXQ5SEhrL1JjCm13WXZTVDNpeHlqdTd5L0R6ZlA5 M1NmOVorcUdlZTFDM3dVRHZlYWpJcDZiakVnCit6eTFOeW92SzhPYzJxR0VTem9r
MU83SzFZVk0yY0dnNkFDMEFDL0dNQTAKLS0tIHZ3TjZIVVArVDFlbkNZVTlxT2Rj MSs4cWxRbzVBQmlWaHIwMjB5RUlJMXcKLS0tIHNSVTloOEVVVndDWkVrWmQrYXlD
WDg3QkxpV0wzVEVjcVE3TzJ0eTBud3MK6LdWJ7LD0c1mlf95X7G7ZAUu+hTCAZJd NTd1WGFJWHVLTnFNT3hYbDdtSnMzTTAKBmJOayZLbjmBejwVzVtUSYPki+qPkYwG
EA8kJE+JzqsCiH7MJJmTBgr2fbzxMZ+mUV6CHysNP1ax4Mvd7uz/WQ== xdO3L7n0Z8Cv/kVYZpkuG5GqOUL+nCJuYDjF0g4PaLb6WWd0W8ZGFA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-11-02T12:53:43Z" lastmodified: "2025-12-01T11:01:54Z"
mac: ENC[AES256_GCM,data:7a7iVyM5ZXNLJYFw4+8kyRYQZOg7FQ6SvhnsWebdEsWlcZjqjbGYMLROoH6lxWYp7nunaewlsLpPrRqzAFbCnMs1QKZDJ/yWccXKYrSmSQVyh48PdlNDUu5w2e4HQdxBAvWdkEyKkJZ11q6eDncb7gm4ri4M1/NrMdQKZ+saVC4=,iv:Mmm/wkLCdfNVpjkvUJHUGAHX7wOM23K3HcJZ66HKnHI=,tag:5YPnp2CVWgGC/kPyXPlvhQ==,type:str] mac: ENC[AES256_GCM,data:taGX5HHZCL7Zo4taS2Jz/5WxhvpBNNKZ13ZCtS3x/P17tC1Nrk2UDcxbOZ1pPVbVvvaAHJtDb3owFvBOM4nr2Eve0M9zT4HbXh3hke7AviQ6U7CT1ru6LjY7W8lBjbQ6uCt+Ldxd1PRPPGiyKdK5GAUPKg6avFjpJbhEikh8Gww=,iv:NNs5usVJ5izYvHKnNm1IgjSt4dg0QFQ7cClJ6zh+3wM=,tag:sYYbEWIUgOWthEItdy5PFg==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.11.0

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05 https://channels.nixos.org/nixos-25.11

View File

@@ -240,11 +240,11 @@ in
sops.secrets.dovecot-ldap-password = { }; sops.secrets.dovecot-ldap-password = { };
systemd.services.dovecot2.preStart = '' systemd.services.dovecot.preStart = ''
sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${ldapConfig} > /run/dovecot2/ldap.conf sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${ldapConfig} > /run/dovecot2/ldap.conf
''; '';
systemd.services.dovecot2 = { systemd.services.dovecot = {
wants = [ "acme-imap.${domain}.service" ]; wants = [ "acme-imap.${domain}.service" ];
after = [ "acme-imap.${domain}.service" ]; after = [ "acme-imap.${domain}.service" ];
}; };
@@ -257,7 +257,7 @@ in
"imap-test.${domain}" "imap-test.${domain}"
"imap-02.${domain}" "imap-02.${domain}"
]; ];
postRun = "systemctl --no-block restart dovecot2.service"; postRun = "systemctl --no-block restart dovecot.service";
}; };
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [

View File

@@ -17,10 +17,10 @@ in {
olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem"; olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem";
olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem"; olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem";
olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem"; olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem";
olcTLSCipherSuite = "HIGH:MEDIUM:+3DES:+RC4:+aNULL"; olcTLSCipherSuite = "HIGH:!aNULL:!MD5:!3DES:!RC4";
olcTLSCRLCheck = "none"; olcTLSCRLCheck = "none";
olcTLSVerifyClient = "never"; olcTLSVerifyClient = "never";
olcTLSProtocolMin = "3.1"; olcTLSProtocolMin = "3.3";
olcSecurity = "tls=1"; olcSecurity = "tls=1";
}; };

View File

@@ -128,16 +128,16 @@ in
compatibility_level = "2"; compatibility_level = "2";
# bigger attachement size # bigger attachement size
mailbox_size_limit = "202400000"; mailbox_size_limit = 202400000;
message_size_limit = "51200000"; message_size_limit = 51200000;
smtpd_helo_required = "yes"; smtpd_helo_required = "yes";
smtpd_delay_reject = "yes"; smtpd_delay_reject = "yes";
strict_rfc821_envelopes = "yes"; strict_rfc821_envelopes = "yes";
# send Limit # send Limit
smtpd_error_sleep_time = "1s"; smtpd_error_sleep_time = "1s";
smtpd_soft_error_limit = "10"; smtpd_soft_error_limit = 10;
smtpd_hard_error_limit = "20"; smtpd_hard_error_limit = 20;
smtpd_use_tls = "yes"; smtpd_use_tls = "yes";
smtp_tls_note_starttls_offer = "yes"; smtp_tls_note_starttls_offer = "yes";
@@ -151,14 +151,13 @@ in
smtpd_tls_key_file = "/var/lib/acme/mail.cloonar.com/key.pem"; smtpd_tls_key_file = "/var/lib/acme/mail.cloonar.com/key.pem";
smtpd_tls_CAfile = "/var/lib/acme/mail.cloonar.com/fullchain.pem"; smtpd_tls_CAfile = "/var/lib/acme/mail.cloonar.com/fullchain.pem";
smtpd_tls_dh512_param_file = config.security.dhparams.params.postfix512.path;
smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix2048.path; smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix2048.path;
smtpd_tls_session_cache_database = ''btree:''${data_directory}/smtpd_scache''; smtpd_tls_session_cache_database = ''btree:''${data_directory}/smtpd_scache'';
smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1"; smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1"; smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1";
smtpd_tls_mandatory_ciphers = "medium"; smtpd_tls_mandatory_ciphers = "medium";
tls_medium_cipherlist = "AES128+EECDH:AES128+EDH"; tls_medium_cipherlist = "ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20";
# authentication # authentication
smtpd_sasl_auth_enable = "yes"; smtpd_sasl_auth_enable = "yes";
@@ -225,8 +224,7 @@ in
security.dhparams = { security.dhparams = {
enable = true; enable = true;
params.postfix512.bits = 512; params.postfix2048.bits = 2048;
params.postfix2048.bits = 1024;
}; };
security.acme.certs."mail.${domain}" = { security.acme.certs."mail.${domain}" = {

View File

@@ -119,7 +119,7 @@ in
# systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ]; # systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ];
systemd.services.dovecot2.preStart = '' systemd.services.dovecot.preStart = ''
mkdir -p /var/lib/dovecot/sieve/ mkdir -p /var/lib/dovecot/sieve/
for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do
dest="/var/lib/dovecot/sieve/$(basename $i)" dest="/var/lib/dovecot/sieve/$(basename $i)"

60
hosts/nas/STORAGE.md Normal file
View File

@@ -0,0 +1,60 @@
# NAS Storage Notes
## Current Issue: XFS Metadata Overhead
The XFS filesystem on `/var/lib/multimedia` uses ~100GB more than the actual file data due to metadata overhead.
### Root Cause
The filesystem was created with advanced features enabled:
```
rmapbt=1 # Reverse mapping btree - tracks block ownership
reflink=1 # Copy-on-write support
```
These features add metadata that scales with **filesystem size**, not file count. On a 5TB filesystem with 700GB of data, this results in ~100GB (~2%) overhead.
### Diagnostic Commands
```bash
# Compare file data vs filesystem usage
du -sh /var/lib/multimedia # Actual file data
df -h /var/lib/multimedia # Filesystem reports
# Check XFS features
xfs_info /var/lib/multimedia
# Verify block allocation
xfs_db -r -c "freesp -s" /dev/mapper/vg--data-lv--multimedia
```
## Recommendation: LVM + ext4
For media storage (write-once, read-many), ext4 with minimal reserved space offers the lowest overhead:
```bash
# Create filesystem with 0% reserved blocks
mkfs.ext4 -m 0 /dev/vg/lv
# Or adjust existing ext4
tune2fs -m 0 /dev/vg/lv
```
### Why ext4 over XFS for this use case
| Consideration | ext4 | XFS (current) |
|---------------|------|---------------|
| Reserved space | 0% with `-m 0` | N/A |
| Metadata overhead | ~0.5% | ~2% (with rmapbt) |
| Shrink support | Yes | No |
| Performance for 4K stream | Identical | Identical |
A single 4K remux stream requires ~12 MB/s. Any filesystem handles this trivially.
## Migration Path
1. Backup data from XFS volumes
2. Recreate LVs with ext4 (`mkfs.ext4 -m 0`)
3. Restore data
4. Update `/etc/fstab` or NixOS `fileSystems` config

1
hosts/nas/channel Normal file
View File

@@ -0,0 +1 @@
https://channels.nixos.org/nixos-25.11

100
hosts/nas/configuration.nix Normal file
View File

@@ -0,0 +1,100 @@
# NAS host configuration
{ config, lib, pkgs, ... }:
let
impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz";
in {
nixpkgs.config.allowUnfree = true;
imports = [
"${impermanence}/nixos.nix"
./utils/bento.nix
./utils/modules/sops.nix
./utils/modules/set-nix-channel.nix
./utils/modules/victoriametrics
./utils/modules/promtail
# ./modules/cyberghost.nix
./modules/pyload.nix
./modules/jellyfin.nix
./modules/power-management.nix
./modules/disk-monitoring.nix
./modules/ugreen-leds.nix
./hardware-configuration.nix
];
nixpkgs.overlays = [
(import ./utils/overlays/packages.nix)
];
# Hostname
networking.hostName = "nas";
# Timezone
time.timeZone = "Europe/Vienna";
console.keyMap = "de";
# SSH server
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRQuPqH5fdX3KEw7DXzWEdO3AlUn1oSmtJtHB71ICoH Generated By Termius"
];
# Firewall
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 ];
# SOPS configuration
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml;
# Btrfs maintenance
services.btrfs.autoScrub = {
enable = true;
interval = "monthly";
fileSystems = [ "/nix" ];
};
# Impermanence - persist important directories
# Note: /var/lib/downloads and /var/lib/multimedia are mounted from LVM on RAID
environment.persistence."/nix/persist/system" = {
hideMounts = true;
directories = [
"/var/lib/pyload"
"/var/lib/jellyfin"
"/var/log"
"/var/lib/nixos"
"/var/bento"
"/root/.ssh"
];
files = [
"/etc/machine-id"
{ file = "/etc/ssh/ssh_host_ed25519_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
{ file = "/etc/ssh/ssh_host_ed25519_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
{ file = "/etc/ssh/ssh_host_rsa_key"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
{ file = "/etc/ssh/ssh_host_rsa_key.pub"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
];
};
# System packages
environment.systemPackages = with pkgs; [
vim
screen
];
# Nix settings
nix = {
settings = {
auto-optimise-store = true;
experimental-features = [ "nix-command" "flakes" ];
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
};
system.stateVersion = "24.05";
}

1
hosts/nas/fleet.nix Symbolic link
View File

@@ -0,0 +1 @@
../../fleet.nix

View File

@@ -0,0 +1,103 @@
# Hardware configuration for NAS
# TODO: Update disk labels/UUIDs after installation
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.loader.systemd-boot = {
enable = true;
configurationLimit = 5;
};
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ "dm-snapshot" ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
# Power management kernel parameters
boot.kernelParams = [
"intel_pstate=passive" # Better with powersave governor
"i915.enable_rc6=1" # GPU deep sleep states
"i915.enable_dc=2" # Display C-states (deepest)
"i915.enable_fbc=1" # Frame buffer compression
];
# RAID 1 arrays for data storage
boot.swraid = {
enable = true;
mdadmConf = ''
DEVICE /dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7-part1
DEVICE /dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1M9Z0E7-part1
DEVICE /dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_8582A01SF4MJ-part1
DEVICE /dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_75V2A0H3F4MJ-part1
'';
};
# Tmpfs root filesystem (ephemeral - resets on reboot)
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
options = [ "size=8G" "mode=755" ];
};
# Boot partition - EFI
fileSystems."/boot" = {
device = "/dev/disk/by-partlabel/BOOT";
fsType = "vfat";
};
fileSystems."/nix" = {
device = "/dev/disk/by-partlabel/NIXOS";
fsType = "btrfs";
neededForBoot = true;
options = [
"subvol=@"
"compress=zstd:1"
"noatime"
"commit=120"
];
};
fileSystems."/nix/store" = {
device = "/dev/disk/by-partlabel/NIXOS";
fsType = "btrfs";
neededForBoot = true;
options = [
"subvol=@nix-store"
"compress=zstd:1"
"noatime"
"commit=120"
];
};
fileSystems."/nix/persist" = {
device = "/dev/disk/by-partlabel/NIXOS";
fsType = "btrfs";
neededForBoot = true;
options = [
"subvol=@nix-persist"
"compress=zstd:1"
"noatime"
"commit=120"
];
};
# LVM volumes on RAID array
fileSystems."/var/lib/downloads" = {
device = "/dev/vg-data-fast/downloads";
fsType = "ext4";
};
fileSystems."/var/lib/multimedia" = {
device = "/dev/vg-data-slow/multimedia";
fsType = "ext4";
options = [ "noatime" ];
};
# DHCP networking
networking.useDHCP = lib.mkDefault true;
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

@@ -0,0 +1,103 @@
{ config, pkgs, ... }:
let
localNetwork = "10.42.96.0/20";
in
{
# SOPS secrets for CyberGhost credentials
sops.secrets.cyberghost-auth = {
mode = "0400";
owner = "root";
};
sops.secrets.cyberghost-ca = {
mode = "0400";
owner = "root";
};
sops.secrets.cyberghost-cert = {
mode = "0400";
owner = "root";
};
sops.secrets.cyberghost-key = {
mode = "0400";
owner = "root";
};
environment.systemPackages = [ pkgs.openvpn ];
# OpenVPN client service
services.openvpn.servers.cyberghost = {
autoStart = true;
updateResolvConf = true;
config = ''
client
dev tun
proto udp
remote 87-1-hu.cg-dialup.net 443
resolv-retry infinite
nobind
persist-key
persist-tun
# Authentication
auth-user-pass ${config.sops.secrets.cyberghost-auth.path}
ca ${config.sops.secrets.cyberghost-ca.path}
cert ${config.sops.secrets.cyberghost-cert.path}
key ${config.sops.secrets.cyberghost-key.path}
# Security
data-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC
data-ciphers-fallback AES-256-CBC
auth SHA256
remote-cert-tls server
script-security 2
# Connection
ping 5
explicit-exit-notify 2
route-delay 5
# Split tunnel: Don't pull routes from server, we'll set our own
route-nopull
# Route all traffic through VPN except local network
route 0.0.0.0 128.0.0.0 vpn_gateway
route 128.0.0.0 128.0.0.0 vpn_gateway
# Keep local network route direct
route ${localNetwork} net_gateway
verb 4
'';
};
# Kill switch: Block outgoing traffic if VPN is down
networking.firewall = {
extraCommands = ''
# Allow traffic to local network
iptables -A OUTPUT -d ${localNetwork} -j ACCEPT
# Allow traffic through VPN tunnel
iptables -A OUTPUT -o tun+ -j ACCEPT
# Allow loopback
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established connections (for responses)
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow OpenVPN to establish connection (UDP 443)
iptables -A OUTPUT -p udp --dport 443 -j ACCEPT
# Drop all other outgoing internet traffic (kill switch)
iptables -A OUTPUT ! -d ${localNetwork} -j DROP
'';
extraStopCommands = ''
iptables -D OUTPUT -d ${localNetwork} -j ACCEPT 2>/dev/null || true
iptables -D OUTPUT -o tun+ -j ACCEPT 2>/dev/null || true
iptables -D OUTPUT -o lo -j ACCEPT 2>/dev/null || true
iptables -D OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true
iptables -D OUTPUT -p udp --dport 443 -j ACCEPT 2>/dev/null || true
iptables -D OUTPUT ! -d ${localNetwork} -j DROP 2>/dev/null || true
'';
};
}

View File

@@ -0,0 +1,199 @@
# Disk monitoring for NAS
# - S.M.A.R.T. metrics collection (respects disk spindown)
# - mdadm RAID array status
# - Exports metrics via node_exporter textfile collector
{ config, lib, pkgs, ... }:
let
# Disk identifiers from hardware-configuration.nix
disks = [
"/dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52TBSB"
"/dev/disk/by-id/ata-ST18000NM000J-2TV103_ZR52V9QX"
"/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_8582A01SF4MJ"
"/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_75V2A0H3F4MJ"
"/dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7"
"/dev/disk/by-id/nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1M9Z0E7"
];
textfileDir = "/var/lib/prometheus-node-exporter";
# Script to collect S.M.A.R.T. and mdadm metrics
collectMetricsScript = pkgs.writeShellScript "collect-disk-metrics" ''
set -euo pipefail
TEXTFILE_DIR="${textfileDir}"
METRICS_FILE="$TEXTFILE_DIR/disk_health.prom"
TEMP_FILE="$TEXTFILE_DIR/disk_health.prom.tmp"
mkdir -p "$TEXTFILE_DIR"
: > "$TEMP_FILE"
# Timestamp of collection
echo "# HELP disk_metrics_last_update Unix timestamp of last metrics collection" >> "$TEMP_FILE"
echo "# TYPE disk_metrics_last_update gauge" >> "$TEMP_FILE"
echo "disk_metrics_last_update $(date +%s)" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
echo "# HELP smart_device_active Whether the disk was active (1) or sleeping (0) when checked" >> "$TEMP_FILE"
echo "# TYPE smart_device_active gauge" >> "$TEMP_FILE"
# S.M.A.R.T. metrics for each disk
for disk in ${lib.concatStringsSep " " disks}; do
if [[ ! -e "$disk" ]]; then
echo "Warning: Disk $disk not found, skipping" >&2
continue
fi
# Resolve symlink to get actual device (needed for hdparm/smartctl)
device=$(readlink -f "$disk")
# Extract model+serial from disk-by-id path for stable labeling
# ata-ST18000NM000J-2TV103_ZR52TBSB ST18000NM000J-2TV103-ZR52TBSB
# nvme-KIOXIA-EXCERIA_PLUS_G3_SSD_7FJKS1MAZ0E7 KIOXIA-EXCERIA_PLUS_G3_SSD-7FJKS1MAZ0E7
disk_id=$(basename "$disk")
serial=$(echo "$disk_id" | sed 's/.*_//')
model=$(echo "$disk_id" | sed 's/^[^-]*-//; s/_[^_]*$//')
short_name="$model-$serial"
# Check power state without waking disk
power_state=$(${pkgs.hdparm}/bin/hdparm -C "$device" 2>/dev/null | grep -oP '(standby|active/idle|active|idle)' | head -1 || echo "unknown")
if [[ "$power_state" == "standby" ]]; then
# Disk is sleeping - don't wake it, report inactive
echo "smart_device_active{device=\"$short_name\",serial=\"$serial\"} 0" >> "$TEMP_FILE"
echo "Disk $short_name is in standby, skipping S.M.A.R.T. collection" >&2
continue
fi
# Disk is active - collect S.M.A.R.T. data
echo "smart_device_active{device=\"$short_name\",serial=\"$serial\"} 1" >> "$TEMP_FILE"
# Get S.M.A.R.T. health status
if ${pkgs.smartmontools}/bin/smartctl -H "$device" 2>/dev/null | grep -q "PASSED"; then
health=1
else
health=0
fi
# Get S.M.A.R.T. attributes
smartctl_output=$(${pkgs.smartmontools}/bin/smartctl -A "$device" 2>/dev/null || true)
# Parse key attributes
# Format: ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED WHEN_FAILED RAW_VALUE
get_raw_value() {
local attr_id="$1"
echo "$smartctl_output" | awk -v id="$attr_id" '$1 == id { print $10 }' | head -1
}
reallocated=$(get_raw_value "5")
power_on_hours=$(get_raw_value "9")
temperature=$(get_raw_value "194")
reallocated_event=$(get_raw_value "196")
pending_sector=$(get_raw_value "197")
offline_uncorrectable=$(get_raw_value "198")
udma_crc_error=$(get_raw_value "199")
# Output metrics
cat >> "$TEMP_FILE" << EOF
# S.M.A.R.T. metrics for $short_name
smart_health_passed{device="$short_name",serial="$serial"} $health
EOF
[[ -n "$reallocated" ]] && echo "smart_reallocated_sector_ct{device=\"$short_name\",serial=\"$serial\"} $reallocated" >> "$TEMP_FILE"
[[ -n "$power_on_hours" ]] && echo "smart_power_on_hours{device=\"$short_name\",serial=\"$serial\"} $power_on_hours" >> "$TEMP_FILE"
[[ -n "$temperature" ]] && echo "smart_temperature_celsius{device=\"$short_name\",serial=\"$serial\"} $temperature" >> "$TEMP_FILE"
[[ -n "$reallocated_event" ]] && echo "smart_reallocated_event_count{device=\"$short_name\",serial=\"$serial\"} $reallocated_event" >> "$TEMP_FILE"
[[ -n "$pending_sector" ]] && echo "smart_current_pending_sector{device=\"$short_name\",serial=\"$serial\"} $pending_sector" >> "$TEMP_FILE"
[[ -n "$offline_uncorrectable" ]] && echo "smart_offline_uncorrectable{device=\"$short_name\",serial=\"$serial\"} $offline_uncorrectable" >> "$TEMP_FILE"
[[ -n "$udma_crc_error" ]] && echo "smart_udma_crc_error_count{device=\"$short_name\",serial=\"$serial\"} $udma_crc_error" >> "$TEMP_FILE"
done
# mdadm RAID array status (doesn't access disks)
echo "" >> "$TEMP_FILE"
echo "# HELP mdadm_array_state RAID array state (1=clean/active/resyncing, 0=degraded/other)" >> "$TEMP_FILE"
echo "# TYPE mdadm_array_state gauge" >> "$TEMP_FILE"
echo "# HELP mdadm_array_devices_total Total devices in RAID array" >> "$TEMP_FILE"
echo "# TYPE mdadm_array_devices_total gauge" >> "$TEMP_FILE"
echo "# HELP mdadm_array_devices_active Active devices in RAID array" >> "$TEMP_FILE"
echo "# TYPE mdadm_array_devices_active gauge" >> "$TEMP_FILE"
# Find RAID arrays
for md_device in /dev/md/*; do
[[ -e "$md_device" ]] || continue
array_name=$(basename "$md_device")
# Get array details
mdadm_output=$(${pkgs.mdadm}/bin/mdadm --detail "$md_device" 2>/dev/null || continue)
# Parse state
state=$(echo "$mdadm_output" | grep "State :" | sed 's/.*State : //' | tr -d ' ')
if [[ "$state" == *clean* ]] || [[ "$state" == *active* ]]; then
state_value=1
else
state_value=0
fi
# Parse device counts
total_devices=$(echo "$mdadm_output" | grep "Raid Devices" | awk '{print $4}')
active_devices=$(echo "$mdadm_output" | grep "Active Devices" | awk '{print $4}')
echo "mdadm_array_state{array=\"$array_name\",state=\"$state\"} $state_value" >> "$TEMP_FILE"
[[ -n "$total_devices" ]] && echo "mdadm_array_devices_total{array=\"$array_name\"} $total_devices" >> "$TEMP_FILE"
[[ -n "$active_devices" ]] && echo "mdadm_array_devices_active{array=\"$array_name\"} $active_devices" >> "$TEMP_FILE"
done
# Atomically replace the metrics file
mv "$TEMP_FILE" "$METRICS_FILE"
echo "Disk metrics collection complete"
'';
in
{
# Required packages
environment.systemPackages = with pkgs; [
smartmontools
hdparm
mdadm
];
# Node exporter with textfile collector
services.prometheus.exporters.node = {
enable = true;
enabledCollectors = [
"textfile"
];
extraFlags = [
"--collector.textfile.directory=${textfileDir}"
];
};
# Systemd service to collect metrics
systemd.services.disk-metrics = {
description = "Collect S.M.A.R.T. and RAID metrics";
path = with pkgs; [ coreutils gawk gnugrep gnused ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${collectMetricsScript}";
# Run as root to access disk devices
User = "root";
};
};
# Timer to run every 20 minutes (5min buffer for 15min spindown)
systemd.timers.disk-metrics = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/20"; # Every 20 minutes
RandomizedDelaySec = "1min";
Persistent = true;
};
};
# Ensure textfile directory exists and is persisted
systemd.tmpfiles.rules = [
"d ${textfileDir} 0755 root root -"
];
}

View File

@@ -0,0 +1,186 @@
{ pkgs }:
pkgs.writeShellScriptBin "filebot-process" ''
#!/usr/bin/env bash
set -euo pipefail
# FileBot AMC script for automated media organization
# Called by PyLoad's package_extracted hook with parameters:
# $1 = package_id
# $2 = package_name
# $3 = download_folder (actual path to extracted files)
# $4 = password (optional)
PACKAGE_ID="''${1:-}"
PACKAGE_NAME="''${2:-unknown}"
DOWNLOAD_DIR="''${3:-/var/lib/downloads}"
PASSWORD="''${4:-}"
OUTPUT_DIR="/var/lib/multimedia"
LOG_FILE="/var/lib/pyload/filebot-amc.log"
EXCLUDE_LIST="/var/lib/pyload/filebot-exclude-list.txt"
PASSWORD_FILE="/var/lib/pyload/extraction-passwords.txt"
# Ensure FileBot data directory exists
mkdir -p /var/lib/pyload/.local/share/filebot/data
mkdir -p "$(dirname "$LOG_FILE")"
touch "$EXCLUDE_LIST"
# Install FileBot license if not already installed
if [ ! -f /var/lib/pyload/.local/share/filebot/data/.license ]; then
echo "$(date): Installing FileBot license..." >> "$LOG_FILE"
${pkgs.filebot}/bin/filebot --license /var/lib/pyload/filebot-license.psm || true
fi
echo "===========================================" >> "$LOG_FILE"
echo "$(date): PyLoad package hook triggered" >> "$LOG_FILE"
echo "Package ID: $PACKAGE_ID" >> "$LOG_FILE"
echo "Package Name: $PACKAGE_NAME" >> "$LOG_FILE"
echo "Download Directory: $DOWNLOAD_DIR" >> "$LOG_FILE"
echo "===========================================" >> "$LOG_FILE"
# Check if download directory exists
if [ ! -d "$DOWNLOAD_DIR" ]; then
echo "$(date): Download directory does not exist: $DOWNLOAD_DIR" >> "$LOG_FILE"
exit 0
fi
# --- Archive Extraction ---
# Try extraction with passwords, then without
try_extract() {
local archive="$1"
local outdir
outdir="$(dirname "$archive")"
# Try each password from file
if [ -f "$PASSWORD_FILE" ]; then
while IFS= read -r pass || [ -n "$pass" ]; do
[ -z "$pass" ] && continue
case "$archive" in
*.rar) ${pkgs.unrar}/bin/unrar x -p"$pass" -o+ "$archive" "$outdir/" >/dev/null 2>&1 && return 0 ;;
*.7z) ${pkgs.p7zip}/bin/7z x -p"$pass" -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
*.zip) ${pkgs.p7zip}/bin/7z x -p"$pass" -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
esac
done < "$PASSWORD_FILE"
fi
# Try without password
case "$archive" in
*.rar) ${pkgs.unrar}/bin/unrar x -o+ "$archive" "$outdir/" >/dev/null 2>&1 && return 0 ;;
*.7z) ${pkgs.p7zip}/bin/7z x -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
*.zip) ${pkgs.p7zip}/bin/7z x -aoa -o"$outdir" "$archive" >/dev/null 2>&1 && return 0 ;;
esac
return 1
}
# Delete all parts of a split RAR archive
delete_rar_parts() {
local first_part="$1"
local base dir
dir="$(dirname "$first_part")"
# Extract base name: "foo.part1.rar" -> "foo", "foo.part01.rar" -> "foo"
base="$(basename "$first_part" | ${pkgs.gnused}/bin/sed -E 's/\.part[0-9]+\.rar$//')"
# Delete all parts matching the pattern
find "$dir" -maxdepth 1 -type f -iname "''${base}.part*.rar" -delete
echo "$(date): Deleted all parts: ''${base}.part*.rar" >> "$LOG_FILE"
}
# Extract archives in directory
extract_archives() {
local dir="$1"
local extracted=0
# 1. Handle split RAR archives (*.part1.rar or *.part01.rar - first part only)
while IFS= read -r -d "" archive; do
echo "$(date): Extracting split RAR: $archive" >> "$LOG_FILE"
if try_extract "$archive"; then
echo "$(date): Extraction successful" >> "$LOG_FILE"
delete_rar_parts "$archive"
extracted=1
else
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
fi
done < <(find "$dir" -type f \( -iname "*.part1.rar" -o -iname "*.part01.rar" \) -print0 2>/dev/null)
# 2. Handle single RAR files (not part of split archive)
while IFS= read -r -d "" archive; do
echo "$(date): Extracting RAR: $archive" >> "$LOG_FILE"
if try_extract "$archive"; then
echo "$(date): Extraction successful, deleting: $archive" >> "$LOG_FILE"
rm -f "$archive"
extracted=1
else
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
fi
done < <(find "$dir" -type f -iname "*.rar" ! -iname "*.part*.rar" -print0 2>/dev/null)
# 3. Handle 7z and zip archives
while IFS= read -r -d "" archive; do
echo "$(date): Extracting: $archive" >> "$LOG_FILE"
if try_extract "$archive"; then
echo "$(date): Extraction successful, deleting: $archive" >> "$LOG_FILE"
rm -f "$archive"
extracted=1
else
echo "$(date): Extraction FAILED: $archive" >> "$LOG_FILE"
fi
done < <(find "$dir" -type f \( -iname "*.7z" -o -iname "*.zip" \) -print0 2>/dev/null)
[ "$extracted" -eq 1 ] && return 0 || return 1
}
# Run extraction (loop to handle nested archives)
echo "$(date): Starting archive extraction..." >> "$LOG_FILE"
for i in 1 2 3; do
extract_archives "$DOWNLOAD_DIR" || break
done
echo "$(date): Archive extraction complete" >> "$LOG_FILE"
# --- Media Processing ---
# Check if there are any video/media files to process
if ! find "$DOWNLOAD_DIR" -type f \( -iname "*.mkv" -o -iname "*.mp4" -o -iname "*.avi" -o -iname "*.m4v" -o -iname "*.mov" \) -print -quit | grep -q .; then
echo "$(date): No media files found in: $DOWNLOAD_DIR" >> "$LOG_FILE"
echo "$(date): Skipping FileBot processing" >> "$LOG_FILE"
exit 0
fi
echo "$(date): Starting FileBot processing" >> "$LOG_FILE"
# Run FileBot AMC script
set +e # Temporarily disable exit on error to capture exit code
${pkgs.filebot}/bin/filebot \
-script fn:amc \
--output "$OUTPUT_DIR" \
--action move \
--conflict auto \
-non-strict \
--log-file "$LOG_FILE" \
--def \
excludeList="$EXCLUDE_LIST" \
movieFormat="$OUTPUT_DIR/movies/{n} ({y})/{n} ({y}) - {vf}" \
seriesFormat="$OUTPUT_DIR/tv-shows/{n}/Season {s.pad(2)}/{n} - {s00e00} - {t}" \
ut_dir="$DOWNLOAD_DIR" \
ut_kind=multi \
clean=y \
skipExtract=y
FILEBOT_EXIT_CODE=$?
set -e # Re-enable exit on error
if [ $FILEBOT_EXIT_CODE -ne 0 ]; then
echo "$(date): FileBot processing failed with exit code $FILEBOT_EXIT_CODE" >> "$LOG_FILE"
exit 0 # Don't fail the hook even if FileBot fails
fi
echo "$(date): FileBot processing completed successfully" >> "$LOG_FILE"
# Clean up any remaining empty directories
find "$DOWNLOAD_DIR" -type d -empty -delete 2>/dev/null || true
echo "$(date): All processing completed" >> "$LOG_FILE"
exit 0
''

View File

@@ -0,0 +1,51 @@
{ lib, pkgs, ... }: {
# Intel graphics support for hardware transcoding
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver
vpl-gpu-rt
intel-compute-runtime
];
};
# Set VA-API driver to iHD (modern Intel driver)
environment.sessionVariables = {
LIBVA_DRIVER_NAME = "iHD";
};
# Jellyfin user with render/video groups for GPU access
users.users.jellyfin = {
isSystemUser = true;
group = "jellyfin";
home = "/var/lib/jellyfin";
createHome = true;
extraGroups = [ "render" "video" ];
};
users.groups.jellyfin = {};
# Create jellyfin directory
systemd.tmpfiles.rules = [
"d /var/lib/jellyfin 0755 jellyfin jellyfin - -"
];
services.jellyfin = {
enable = true;
openFirewall = true;
};
# Override systemd hardening for GPU access
systemd.services.jellyfin = {
serviceConfig = {
PrivateUsers = lib.mkForce false; # Disable user namespacing - breaks GPU device access
DeviceAllow = [
"/dev/dri/card0 rw"
"/dev/dri/renderD128 rw"
];
SupplementaryGroups = [ "render" "video" ]; # Critical: Explicit group membership for GPU access
};
environment = {
LIBVA_DRIVER_NAME = "iHD"; # Ensure service sees this variable
};
};
}

View File

@@ -0,0 +1,24 @@
# Power management for NAS
# - CPU powersave governor (scales up on demand for transcoding)
# - Disk spindown after 15 minutes idle
{ config, lib, pkgs, ... }:
{
# CPU Power Management - powersave scales up on demand for transcoding
powerManagement.cpuFreqGovernor = "powersave";
# Disk spindown - hdparm for Seagate 18TB drives
environment.systemPackages = [ pkgs.hdparm ];
services.udev.extraRules = ''
# Seagate 18TB NAS drives - APM 127 allows spindown, -S 180 = 15 min
ACTION=="add", KERNEL=="sd[a-z]", SUBSYSTEM=="block", \
ATTRS{model}=="ST18000NM000J*", \
RUN+="${pkgs.hdparm}/bin/hdparm -B 127 -S 180 /dev/%k"
# Toshiba 20TB NAS drives - same settings
ACTION=="add", KERNEL=="sd[a-z]", SUBSYSTEM=="block", \
ATTRS{model}=="TOSHIBA MG10ACA2*", \
RUN+="${pkgs.hdparm}/bin/hdparm -B 127 -S 180 /dev/%k"
'';
}

View File

@@ -0,0 +1,120 @@
{ config, pkgs, lib, ... }:
let
filebotScript = pkgs.callPackage ./filebot-process.nix {};
in
{
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"unrar"
"filebot"
];
environment.systemPackages = with pkgs; [
unrar # Required for RAR archive extraction
p7zip # Required for 7z and other archive formats
];
# Create directory structure
systemd.tmpfiles.rules = [
"d /var/lib/downloads 0755 pyload pyload - -"
"d /var/lib/multimedia 0775 root jellyfin - -"
"d /var/lib/multimedia/movies 0775 jellyfin jellyfin - -"
"d /var/lib/multimedia/tv-shows 0775 jellyfin jellyfin - -"
"d /var/lib/multimedia/music 0755 jellyfin jellyfin - -"
"d /var/lib/multimedia/audiobooks 0775 jellyfin jellyfin - -"
# PyLoad hook scripts directory (package_finished triggers after download completes)
"d /var/lib/pyload/config 0755 pyload pyload - -"
"d /var/lib/pyload/config/scripts 0755 pyload pyload - -"
"d /var/lib/pyload/config/scripts/package_finished 0755 pyload pyload - -"
"L+ /var/lib/pyload/config/scripts/package_finished/filebot-process.sh - - - - ${filebotScript}/bin/filebot-process"
];
# FileBot license secret (only if secrets.yaml exists)
sops.secrets.filebot-license = {
mode = "0440";
owner = "pyload";
group = "pyload";
path = "/var/lib/pyload/filebot-license.psm";
};
# Extraction passwords for filebot-process script (one password per line)
sops.secrets.pyload-extraction-passwords = {
mode = "0440";
owner = "pyload";
group = "pyload";
path = "/var/lib/pyload/extraction-passwords.txt";
};
# PyLoad user with jellyfin group membership for multimedia access
users.users.pyload = {
isSystemUser = true;
group = "pyload";
home = "/var/lib/pyload";
createHome = true;
extraGroups = [ "jellyfin" ];
};
users.groups.pyload = {};
services.pyload = {
enable = true;
downloadDirectory = "/var/lib/downloads";
listenAddress = "0.0.0.0";
port = 8000;
};
# Configure pyload service
systemd.services.pyload = {
# Add extraction tools to service PATH
path = with pkgs; [
unrar # For RAR extraction
p7zip # For 7z extraction
];
environment = {
# Disable SSL certificate verification
PYLOAD__GENERAL__SSL_VERIFY = "0";
# Download speed limiting (150 Mbit/s = 19200 KiB/s)
PYLOAD__DOWNLOAD__LIMIT_SPEED = "1";
PYLOAD__DOWNLOAD__MAX_SPEED = "19200";
# Disable ExtractArchive plugin (extraction handled by filebot-process script)
PYLOAD__EXTRACTARCHIVE__ENABLED = "0";
# Enable ExternalScripts plugin for hooks
PYLOAD__EXTERNALSCRIPTS__ENABLED = "1";
PYLOAD__EXTERNALSCRIPTS__UNLOCK = "1"; # Run hooks asynchronously
# DdownloadCom plugin: don't fall back to free if premium fails
PYLOAD__DDOWNLOADCOM__FALLBACK = "0";
};
serviceConfig = {
# Bind-mount DNS configuration files into the sandboxed service
BindReadOnlyPaths = [
"/etc/resolv.conf"
"/etc/nsswitch.conf"
"/etc/hosts"
"/etc/ssl"
"/etc/static/ssl"
"/run/secrets" # SOPS secrets access for FileBot license
];
# Bind mount multimedia directory as writable for FileBot hook scripts
BindPaths = [ "/var/lib/multimedia" ];
# Override SystemCallFilter to allow @resources syscalls
# FileBot (Java) needs resource management syscalls like setpriority
# during cleanup operations. Still block privileged syscalls for security.
SystemCallFilter = lib.mkForce [
"@system-service"
"@resources" # Explicitly allow resource management syscalls
"~@privileged" # Still block privileged operations
"fchown" # Re-allow fchown for FileBot file operations
"fchown32" # 32-bit compatibility
];
};
};
# Open firewall for PyLoad web interface
networking.firewall.allowedTCPPorts = [ 8000 ];
}

View File

@@ -0,0 +1,285 @@
# UGREEN DXP4800 LED control
# Based on https://github.com/miskcoo/ugreen_leds_controller
{ config, lib, pkgs, ... }:
let
# Disk mapping: ata port -> LED name
# DXP4800 has bays 1-4, all populated
diskMapping = {
"1" = "disk1";
"2" = "disk2";
"3" = "disk3";
"4" = "disk4";
};
# LED colors (R G B)
colors = {
healthy = "0 255 0"; # Green
activity = "0 255 0"; # Green blink
standby = "0 0 255"; # Dim blue when sleeping
fail = "255 0 0"; # Red
network = "0 255 0"; # Green
power = "255 255 255"; # White
};
brightness = 255;
standbyBrightness = 30; # Dim when in standby
refreshInterval = "0.1"; # Seconds between activity checks
powerCheckInterval = 30; # Seconds between power state checks
# Script to initialize LEDs on boot
initLedsScript = pkgs.writeShellScript "ugreen-leds-init" ''
set -euo pipefail
PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli ]}:$PATH"
# Wait for i2c device to be available
for i in $(seq 1 30); do
if [ -e /dev/i2c-0 ]; then
break
fi
sleep 1
done
# Initialize power LED - solid white
ugreen_leds_cli power -on -color ${colors.power} -brightness ${toString brightness} || true
# Initialize network LED - will be controlled by netdevmon
ugreen_leds_cli netdev -off || true
# Initialize disk LEDs based on mapping
${lib.concatStringsSep "\n" (lib.mapAttrsToList (ata: led: ''
ugreen_leds_cli ${led} -off || true
'') diskMapping)}
echo "UGREEN LEDs initialized"
'';
# Disk activity monitoring script
diskMonitorScript = pkgs.writeShellScript "ugreen-diskiomon" ''
set -euo pipefail
PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli pkgs.coreutils pkgs.gnugrep pkgs.gawk pkgs.smartmontools pkgs.hdparm ]}:$PATH"
# Build device -> LED mapping by checking ata ports
declare -A devices
declare -A diskio_data
declare -A disk_healthy
declare -A disk_standby
# Discover disks based on ata port mapping
for path in /dev/disk/by-path/pci-*-ata-*; do
[ -e "$path" ] || continue
# Skip partitions
[[ "$path" == *-part* ]] && continue
# Extract ata port number (e.g., ata-2 -> 2)
ata_port=$(echo "$path" | grep -oP 'ata-\K[0-9]+' | head -1)
case "$ata_port" in
${lib.concatStringsSep "\n " (lib.mapAttrsToList (ata: led: ''
${ata})
device=$(readlink -f "$path")
short_name=$(basename "$device")
devices["${led}"]="$short_name"
echo "Mapped $short_name (ata-${ata}) -> ${led}"
;;'') diskMapping)}
esac
done
if [ ''${#devices[@]} -eq 0 ]; then
echo "No disks found matching ATA ports, exiting"
exit 1
fi
# Set initial LED state for discovered disks
for led in "''${!devices[@]}"; do
device="''${devices[$led]}"
# Check SMART health (this will wake the disk at boot, which is acceptable)
if smartctl -H "/dev/$device" 2>/dev/null | grep -q "PASSED"; then
disk_healthy["$led"]=1
ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true
else
disk_healthy["$led"]=0
ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true
fi
# Initialize tracking
diskio_data["$led"]=""
disk_standby["$led"]=0
done
echo "Starting disk activity monitoring for ''${#devices[@]} disk(s)"
# Function to update LED based on current state
update_led() {
local led="$1"
local device="''${devices[$led]}"
# Check power state without waking disk
local power_state
power_state=$(hdparm -C "/dev/$device" 2>/dev/null | grep -oP '(standby|active/idle|active|idle)' | head -1 || echo "unknown")
if [[ "$power_state" == "standby" ]]; then
if [[ "''${disk_standby[$led]}" != "1" ]]; then
# Disk just went to standby - dim the LED
disk_standby["$led"]=1
ugreen_leds_cli "$led" -on -color ${colors.standby} -brightness ${toString standbyBrightness} || true
echo "Disk $device entered standby, dimming LED"
fi
else
if [[ "''${disk_standby[$led]}" == "1" ]]; then
# Disk woke up - restore health-based color
disk_standby["$led"]=0
if [[ "''${disk_healthy[$led]}" == "1" ]]; then
ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true
else
ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true
fi
echo "Disk $device woke up, restoring LED"
fi
fi
}
# Background power state checker
check_power_states() {
while true; do
sleep ${toString powerCheckInterval}
for led in "''${!devices[@]}"; do
update_led "$led"
done
done
}
# Start power state checker in background
check_power_states &
POWER_CHECK_PID=$!
trap "kill $POWER_CHECK_PID 2>/dev/null || true" EXIT
# Main activity monitoring loop
while true; do
for led in "''${!devices[@]}"; do
device="''${devices[$led]}"
stat_file="/sys/block/$device/stat"
if [ -f "$stat_file" ]; then
new_stat=$(cat "$stat_file" 2>/dev/null || echo "")
if [ -n "$new_stat" ] && [ "''${diskio_data[$led]}" != "$new_stat" ]; then
# Activity detected - disk must be awake now
if [[ "''${disk_standby[$led]}" == "1" ]]; then
disk_standby["$led"]=0
if [[ "''${disk_healthy[$led]}" == "1" ]]; then
ugreen_leds_cli "$led" -on -color ${colors.healthy} -brightness ${toString brightness} || true
else
ugreen_leds_cli "$led" -on -color ${colors.fail} -brightness ${toString brightness} || true
fi
fi
# Trigger LED blink for activity
if [ -e "/sys/class/leds/$led/shot" ]; then
echo 1 > "/sys/class/leds/$led/shot" 2>/dev/null || true
else
ugreen_leds_cli "$led" -blink 100 100 2>/dev/null || true
sleep 0.05
ugreen_leds_cli "$led" -on 2>/dev/null || true
fi
fi
diskio_data["$led"]="$new_stat"
fi
done
sleep ${refreshInterval}
done
'';
# Network activity monitoring script
netMonitorScript = pkgs.writeShellScript "ugreen-netdevmon" ''
set -euo pipefail
PATH="${lib.makeBinPath [ pkgs.ugreen-leds-cli pkgs.coreutils pkgs.iproute2 ]}:$PATH"
INTERFACE="$1"
CHECK_INTERVAL=60
echo "Starting network monitoring on $INTERFACE"
# Configure LED to trigger on network activity
led_path="/sys/class/leds/netdev"
while true; do
# Check if interface is up
if ip link show "$INTERFACE" 2>/dev/null | grep -q "state UP"; then
# Link is up - set green
ugreen_leds_cli netdev -on -color ${colors.network} -brightness ${toString brightness} || true
# Try to enable hardware trigger for activity indication
if [ -e "$led_path/device_name" ]; then
echo "$INTERFACE" > "$led_path/device_name" 2>/dev/null || true
fi
if [ -e "$led_path/rx" ]; then
echo 1 > "$led_path/rx" 2>/dev/null || true
fi
if [ -e "$led_path/tx" ]; then
echo 1 > "$led_path/tx" 2>/dev/null || true
fi
else
# Link is down - turn off
ugreen_leds_cli netdev -off || true
fi
sleep $CHECK_INTERVAL
done
'';
in
{
# Load i2c-dev kernel module for LED controller communication
boot.kernelModules = [ "i2c-dev" ];
# Install CLI tool
environment.systemPackages = [ pkgs.ugreen-leds-cli ];
# LED initialization service - runs once at boot
systemd.services.ugreen-leds-init = {
description = "Initialize UGREEN NAS LEDs";
wantedBy = [ "multi-user.target" ];
after = [ "local-fs.target" "systemd-modules-load.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${initLedsScript}";
};
};
# Disk activity monitoring service
systemd.services.ugreen-diskiomon = {
description = "UGREEN disk activity LED monitor";
wantedBy = [ "multi-user.target" ];
after = [ "ugreen-leds-init.service" "local-fs.target" ];
requires = [ "ugreen-leds-init.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${diskMonitorScript}";
Restart = "always";
RestartSec = "5s";
};
};
# Network activity monitoring service (template for interface)
systemd.services."ugreen-netdevmon@" = {
description = "UGREEN network LED monitor for %i";
after = [ "ugreen-leds-init.service" "network-online.target" ];
requires = [ "ugreen-leds-init.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${netMonitorScript} %i";
Restart = "always";
RestartSec = "10s";
};
};
# Enable network monitoring for primary interface
systemd.services."ugreen-netdevmon@enp2s0" = {
wantedBy = [ "multi-user.target" ];
};
}

48
hosts/nas/secrets.yaml Normal file
View File

@@ -0,0 +1,48 @@
pyload-extraction-passwords: ENC[AES256_GCM,data:zOvPYcnvcg2OwJaCZovYQz87ZN9DdpKX1Re1/v24daw0WGBG3sGeJn1q+LDfjPIMy487CdY=,iv:loWfUcIw30kVXchmXwAts10FNUGxSsTY2UVRKs0RTJ8=,tag:WlTYugSv2ApR496Uc1KPEg==,type:str]
cyberghost-auth: ENC[AES256_GCM,data:v8PlO2qi06p2FZR1iFbHAVPr0k+X/A==,iv:oEzIIZ7KiVJ5EpMT2YMgvMZSJZwtIsnTWwkMXxl/R4w=,tag:+NOMggSKloW0SOYxopHrYA==,type:str]
cyberghost-ca: ENC[AES256_GCM,data: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,iv:mWBG6fP3do+VzwyBlCVXsWiywRNiyMiWNaxlYFTkms0=,tag:94YPWZtVy1viq0KbclF6LA==,type:str]
cyberghost-cert: ENC[AES256_GCM,data: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,iv:kavsBNAUcK7vHOJnj9nyX4D9dHzgP3aBwCLQb9umBJo=,tag:aeTpc2vpO72R45cjBR+cFw==,type:str]
cyberghost-key: ENC[AES256_GCM,data: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,iv:DmcNUOhsi9doTYta+s65BFpuIgiK7QAjAorfVq/VGLA=,tag:c/mZS7ZnasX5XX4HIx80AA==,type:str]
filebot-license: ENC[AES256_GCM,data:Ib+NXsb0dJfBqm5GYvXxGMOQk3zL28oeS3VXsk4pFIGV5XsakqZRZb307X1oJnafMVXva/eQppraI8CqjwBDzy3AZSnwpFgj96/zvPWZi12IWemnqhFUDFZGaMG4Scg6Ugcq8hwJqU39Mf5JehSbJ/PoULg/Ovekj8R0nG7WlmQsqxpZWXWeAdfYZHw1/4MQTX/IGZG82SdsdFc4J9jTedks0yvW5MLkS3azZSFDqY2WXAMVa5J9jXEcie58k6p0QqAUpWGltoXz1tdN/mTnVV7PA35wReS/Ejm11KS5AsjkTIZRLeJ14BL0vALUh3z6JSdr8kPG1S31MMYxarzGBqjI78LfFmFynGonAlqj/jPoxA7AXOew5xRfoEqnxf8lSUf2F2n7PF+B1SgY/hwFvwUeoaA4ep8uFugT7pCvg3N+FmfsV4TAP6tfU75WjwZ07UKQ0t63C/aDI1lZZjfKDzXDFqliD6omhFPUVxhGIo9gBkvOZj6ujdn7ErWohxAc7JSVsDJ8nBKqVGXMMgecWHyqU5LB9+3CKkE32vMy6fbGVlHhgRI4EofVCiuCn2Opg8txCz7Rh8eU297xotIjfCzhPjaY0CmD0dm/8SVLT9Tt3qQMDzlzolcWg+9o3e4bbjSOzliR+4c4fCGBPxZvBPd4kZ87eo22m6WqtS18tMTD1tq+8nVjpHFyrsyyMZtfv9/QXug0SFhJpUz6izCcl+iiNiQIb+DBi4Oifhr88Ji+Y4iQrj7mPUGzNr/T1x/uSBabv83HPsZUa5+OBrlbvp/Ya2ktIagiyv+JrHmcFMkVGYvFK4idLsREcvteduP7XeWaOXuJZJ5GOPZPDM+8DSxv3eOULH1B25J1/838vaVKZM3ukQkiybYTJWVV2co=,iv:G6bhfqx0go6vbJ2zwXkSbHLt5WdDRwu2o4BsCXw5Rlw=,tag:msCGdlefM3M8lbQWJPcOgA==,type:str]
sops:
age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvemExcjFLT2tjTlhtMlFl
UFlCQWhtUDc5YmFHU21SdDBoMXNCV2dlbXl3CnJzVmpMcXUwbWx1a1VLUVRhSnFu
MmtmYjVESVJpdFh1UmtwSWl5WE5WZFkKLS0tIE5wZzQ0MENna2EwMzVDUU9QcDlk
bitkdVVwM0l4ajJVYldWM2JqV05tUzgKsJem/g4ckwrmiTJgwtHc98zALWlwmVgH
+O0nH3kcU54SjDQYVRKUWdaCNbsXHEN9wqICS9q0Ill7pD2K0ElZLg==
-----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEb1dtSlJMKzFreWtmclBE
YUgvYXBQb1FtaFhZRjlqTWk1aUZkcjhCbjBFCi9rSUMzdWFkL3I3c3E0bUxERkVN
TDFaUCtHWE5xNEN4NzNXTlBWWnpYR2sKLS0tIDQ5dTM3S0JQdGJvaE0rTkZYWXNN
aTJQNUZuMW5kZVNJQ0lObkZRdzVyZ3MKwfD8PgUM1kHCa1aaDAp0Iv3zaSGsOWS8
f3W8gUMV2Qv1FC4hBccbYH2bHuq5ENVhkleIyE51GT+Ckwt5oR14vw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0dE5zaEpBSEYvVE5QTjVF
dldFY2t3YThTalowbkMrckhUWjhGdXFVNG1BCldqV2lOS280Z0xjQXo4THlPenQ2
WVRiWDAxSFgzQUkvSjVZUEpBNzZkR0kKLS0tIC9rOHAvSUZadCs5OXhheFpERzlx
QmRCYldBUW9zeTF3cmtUOXVuV2pOMEEKDJC7lyekw9TQmuwfPRb9UsUgqdbAVaxy
tZYmhSYhUFBOUyJ7xwiIfMgOu5A4D2p/q+T2MPCmeOSLUDyycE8Zuw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzdkUramxKVFVZQVJVMU9E
a1pYck8yOEhpZWZGcktxYkR0U290dll6VmdnCnZWZU9uWXFWZ2ZrVnp0cDBteGdr
azVGVHg5Y0VuZi82UUtkWUtLeDA3UWcKLS0tIDNJZGJQNmpHdFpQUVdiMHh3djhP
WURRbGNNWWJvKzZabGxyd3NXN3lKZ2cKryVInc722ZsjoiYel0YYAQZUsgXDx0by
Ds65yQDcI0ttbmMyFN8oYqD7pnOaD1aZYg6cxqzUVPen9iqCkclMwg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-12-08T12:02:01Z"
mac: ENC[AES256_GCM,data:K1kelYSO6R1kU3hLQVmlPI3vn9p4uEHDQnb7eVgn5PH/HFlqJrRj9HfagD/yKT0hBIehC3R8rxv73SeXacBcCaBx+A1Ty1fj/K18oQdEpFlOWxYhIvRX23NHaaqudFdVRiVg23spOoTgP48+mSzJdE4dk3jQcm94yxiUQy9kBSw=,iv:iSL9knAzk0SLXDJ1m6xy+Vkv6RqtUP2lzcluQTdKG5g=,tag:Z8I+UY/taf/uq4sQ7qIUEg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0

1
hosts/nas/utils Symbolic link
View File

@@ -0,0 +1 @@
../../utils

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05 https://channels.nixos.org/nixos-25.11

View File

@@ -35,6 +35,8 @@ in {
./cachix.nix ./cachix.nix
./users ./users
./modules/epicenter.nix
# ./modules/steam.nix # ./modules/steam.nix
./modules/fingerprint.nix ./modules/fingerprint.nix
./modules/set-nix-channel.nix ./modules/set-nix-channel.nix
@@ -189,6 +191,8 @@ in {
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.extraHosts = '' networking.extraHosts = ''
77.119.230.30 vpn.cloonar.com 77.119.230.30 vpn.cloonar.com
23.88.38.1 api.ebs.amz.at
23.88.38.1 ebs.amz.at
''; '';
# Set your time zone. # Set your time zone.
@@ -287,8 +291,9 @@ in {
settings = { settings = {
auto-optimise-store = true; auto-optimise-store = true;
experimental-features = [ "nix-command" "flakes" ]; experimental-features = [ "nix-command" "flakes" ];
max-jobs = 12; max-jobs = 4;
cores = 2; cores = 4;
max-substitution-jobs = 16;
}; };
gc = { gc = {
automatic = true; automatic = true;
@@ -302,7 +307,7 @@ in {
''; '';
}; };
services.xserver.desktopManager.gnome.extraGSettingsOverrides = '' services.desktopManager.gnome.extraGSettingsOverrides = ''
[org.gnome.desktop.interface] [org.gnome.desktop.interface]
cursor-size=24 cursor-size=24
''; '';

View File

@@ -115,7 +115,7 @@
hardware.graphics = { hardware.graphics = {
enable = true; enable = true;
extraPackages = with pkgs; [ extraPackages = with pkgs; [
vaapiVdpau libva-vdpau-driver
libvdpau-va-gl libvdpau-va-gl
libva libva
libva-utils libva-utils

View File

@@ -26,14 +26,13 @@ in
description = "Bitwarden Desktop"; description = "Bitwarden Desktop";
after = [ "graphical-session.target" "network-online.target" ]; after = [ "graphical-session.target" "network-online.target" ];
wantedBy = [ "graphical-session.target" ]; wantedBy = [ "graphical-session.target" ];
serviceConfig.ExecStart = "${pkgs.bitwarden}/bin/bitwarden"; serviceConfig.ExecStart = "${pkgs.bitwarden-desktop}/bin/bitwarden-desktop";
serviceConfig.Restart = "on-abort"; serviceConfig.Restart = "on-abort";
}; };
#### Handy tools ############################################################# #### Handy tools #############################################################
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
goldwarden bitwarden-desktop
bitwarden
bitwarden-cli bitwarden-cli
fprintd fprintd
lxqt.lxqt-policykit lxqt.lxqt-policykit

View File

@@ -9,7 +9,6 @@ in {
./thunderbird.nix ./thunderbird.nix
./bitwarden.nix ./bitwarden.nix
./rustdesk.nix ./rustdesk.nix
./rustdesk-epicenter.nix
./flatpak-packages.nix ./flatpak-packages.nix
]; ];
@@ -58,10 +57,10 @@ in {
netflix netflix
networkmanagerapplet networkmanagerapplet
nextcloud-client nextcloud-client
onlyoffice-bin onlyoffice-desktopeditors
obs-studio obs-studio
pavucontrol pavucontrol
pinentry pinentry-gnome3
rbw rbw
rofi-rbw rofi-rbw
swayimg swayimg
@@ -104,7 +103,7 @@ in {
fonts.packages = with pkgs; [ fonts.packages = with pkgs; [
noto-fonts noto-fonts
noto-fonts-cjk-sans noto-fonts-cjk-sans
noto-fonts-emoji noto-fonts-color-emoji
nerd-fonts._0xproto nerd-fonts._0xproto
nerd-fonts.droid-sans-mono nerd-fonts.droid-sans-mono
open-sans open-sans

View File

@@ -1,38 +0,0 @@
{ config, pkgs, lib, ... }:
let
wrapperScript = pkgs.writeShellScriptBin "rustdesk-wrapper" ''
CONFIG_FILE="$HOME/.config/rustdesk/RustDesk2.toml"
CONFIG_DIR="$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ]; then
${pkgs.rustdesk-flutter}/bin/rustdesk &
RUSTDESK_PID=$!
sleep 3
kill $RUSTDESK_PID 2>/dev/null || true
sleep 1
fi
if [ -f "$CONFIG_FILE" ]; then
sed -i "s|^rendezvous_server = .*|rendezvous_server = 'tools.epicenter.works:21116'|" "$CONFIG_FILE"
sed -i "s|^custom-rendezvous-server = .*|custom-rendezvous-server = 'tools.epicenter.works'|" "$CONFIG_FILE"
sed -i "/^key\s*=.*/d" "$CONFIG_FILE"
fi
# Launch RustDesk
exec ${pkgs.rustdesk-flutter}/bin/rustdesk "$@"
'';
rustdeskEpicenterDesktopItem = pkgs.makeDesktopItem {
name = "rustdesk-epicenter";
desktopName = "RustDesk Epicenter";
exec = "${wrapperScript}/bin/rustdesk-wrapper";
icon = "rustdesk"; # Using the standard rustdesk icon
categories = [ "Network" "RemoteAccess" ];
comment = "Remote desktop software configured for Epicenter";
};
in {
environment.systemPackages = [
rustdeskEpicenterDesktopItem
];
}

View File

@@ -20,14 +20,24 @@ in {
fi fi
''; '';
home.activation.addChromeDevtoolsMCP = lib.hm.dag.entryAfter [ "installClaudeCli" ] '' # Disabled: chrome-devtools MCP spawns headless Chromium for every Claude session.
# Add via STDIO transport: Claude spawns `npx -y chrome-devtools-mcp ...` # For frontend projects, enable per-project with:
# Browser must be running with remote debugging on 127.0.0.1:9222. # claude mcp add --scope project chrome-devtools \
if ${config.home.homeDirectory}/.nix-profile/bin/claude mcp add --help >/dev/null 2>&1; then # -- npx -y chrome-devtools-mcp \
${config.home.homeDirectory}/.nix-profile/bin/claude mcp add --scope user chrome-devtools \ # --executablePath=${pkgs.ungoogled-chromium}/bin/chromium \
-- npx -y chrome-devtools-mcp --executablePath=${pkgs.ungoogled-chromium}/bin/chromium --isolated=true --headless=true --chromeArg=--ozone-platform=wayland --chromeArg=--enable-features=UseOzonePlatform --chromeArg=--force-device-scale-factor=1 || true # --isolated=true --headless=true \
fi # --chromeArg=--ozone-platform=wayland \
''; # --chromeArg=--enable-features=UseOzonePlatform \
# --chromeArg=--force-device-scale-factor=1
#
# home.activation.addChromeDevtoolsMCP = lib.hm.dag.entryAfter [ "installClaudeCli" ] ''
# # Add via STDIO transport: Claude spawns `npx -y chrome-devtools-mcp ...`
# # Browser must be running with remote debugging on 127.0.0.1:9222.
# if ${config.home.homeDirectory}/.nix-profile/bin/claude mcp add --help >/dev/null 2>&1; then
# ${config.home.homeDirectory}/.nix-profile/bin/claude mcp add --scope user chrome-devtools \
# -- npx -y chrome-devtools-mcp --executablePath=${pkgs.ungoogled-chromium}/bin/chromium --isolated=true --headless=true --chromeArg=--ozone-platform=wayland --chromeArg=--enable-features=UseOzonePlatform --chromeArg=--force-device-scale-factor=1 || true
# fi
# '';
}; };
} }

View File

@@ -5,7 +5,7 @@ let
}; };
in { in {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
unstable.claude-code claude-code # Using custom overlay with version 2.0.55
unstable.code-cursor unstable.code-cursor
unstable.vscode unstable.vscode
# android-studio-full # android-studio-full
@@ -20,7 +20,7 @@ in {
nixpkgs.config.android_sdk.accept_license = true; nixpkgs.config.android_sdk.accept_license = true;
programs.adb.enable = true; # sets up udev + adb group programs.adb.enable = true; # sets up udev + adb group
services.udev.packages = [ pkgs.android-udev-rules ]; # android-udev-rules removed in 25.11 - superseded by built-in systemd uaccess rules
users.users.dominik.extraGroups = [ "adbusers" ]; users.users.dominik.extraGroups = [ "adbusers" ];
} }

View File

@@ -38,6 +38,7 @@ in {
rbw rbw
sops sops
unzip unzip
uv
vim vim
wget wget
wireguard-tools wireguard-tools
@@ -52,11 +53,6 @@ in {
# Socket activation - only start when needed to save battery # Socket activation - only start when needed to save battery
onBoot = "ignore"; onBoot = "ignore";
onShutdown = "shutdown"; onShutdown = "shutdown";
qemu = { # qemu.swtpm.enable = true; # enable if you need TPM emulation, etc.
ovmf = {
enable = true; # Enable OVMF firmware support
};
# swtpm.enable = true; # enable if you need TPM emulation, etc.
};
}; };
} }

View File

@@ -3,7 +3,7 @@
let let
# Wrapper to launch Chromium on Wayland, scale=1, DevTools debugging on 127.0.0.1:9222 # Wrapper to launch Chromium on Wayland, scale=1, DevTools debugging on 127.0.0.1:9222
chromiumWaylandWrapper = pkgs.writeShellScriptBin "chromium-mcp" '' chromiumWaylandWrapper = pkgs.writeShellScriptBin "chromium-mcp" ''
exec ${pkgs.chromium}/bin/chromium \ exec ${pkgs.ungoogled-chromium}/bin/chromium \
--ozone-platform=wayland \ --ozone-platform=wayland \
--enable-features=UseOzonePlatform \ --enable-features=UseOzonePlatform \
--force-device-scale-factor=1 \ --force-device-scale-factor=1 \
@@ -11,32 +11,13 @@ let
--remote-debugging-port=9222 \ --remote-debugging-port=9222 \
"$@" "$@"
''; '';
# Desktop entry that uses our wrapper. The filename will be chromium.desktop
chromiumDesktopOverride = pkgs.makeDesktopItem {
name = "chromium"; # ← important: must match stock filename to override
desktopName = "Chromium";
genericName = "Web Browser";
comment = "Chromium on Wayland (scale=1) with DevTools remote debugging for MCP";
icon = "chromium";
exec = "${chromiumWaylandWrapper}/bin/chromium-mcp %U";
terminal = false;
categories = [ "Network" "WebBrowser" ];
mimeTypes = [
"text/html" "text/xml" "application/xhtml+xml"
"x-scheme-handler/http" "x-scheme-handler/https"
"x-scheme-handler/ftp" "x-scheme-handler/chrome"
];
# If you want extra desktop keys, you can add them as a raw block:
};
in in
{ {
# Tools: Chromium, Node (for MCP server), our wrapper, and the desktop override # Tools: Chromium, Node (for MCP server), our wrapper
environment.systemPackages = [ environment.systemPackages = [
pkgs.chromium pkgs.ungoogled-chromium
pkgs.nodejs_22 # 25.05 ships Node 22 LTS; works great for MCP servers pkgs.nodejs_22 # 25.05 ships Node 22 LTS; works great for MCP servers
chromiumWaylandWrapper chromiumWaylandWrapper
chromiumDesktopOverride # ← keep AFTER pkgs.chromium so our .desktop wins
]; ];
# Where Codex CLI reads config; we make it system-wide # Where Codex CLI reads config; we make it system-wide

View File

@@ -1,3 +1,6 @@
-- Set leader key before any other mappings
vim.g.mapleader = " "
-- vim.opt.expandtab = true -- vim.opt.expandtab = true
-- vim.opt.hidden = true -- vim.opt.hidden = true
-- vim.opt.incsearch = true -- vim.opt.incsearch = true

View File

@@ -1,54 +1,31 @@
local status, lspc = pcall(require, 'lspconfig') -- LSP Capabilities (for nvim-cmp integration)
if (not status) then return end
lspc.clangd.setup{}
local buf_map = function(bufnr, mode, lhs, rhs, opts)
vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts or {
silent = true,
})
end
local protocol = require('vim.lsp.protocol')
local on_attach = function(client, buffnr)
if client.server.capabilities.documentFormattingProvider then
vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("format", { clear = true }),
buffer = buffnr,
callback = function() vim.lsp.buf.formatting_seq_sync() end
})
end
end
local capabilities = vim.lsp.protocol.make_client_capabilities() local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true capabilities.textDocument.completion.completionItem.snippetSupport = true
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities) capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
local servers = { 'ts_ls', 'lua_ls', 'cssls', 'yamlls', 'intelephense', 'gopls' } -- Global LSP configuration
for _, lsp in pairs(servers) do vim.lsp.config('*', {
require('lspconfig')[lsp].setup {
-- on_attach = on_attach,
capabilities = capabilities, capabilities = capabilities,
} })
end
lspc.yamlls.setup({ -- Server-specific configurations
vim.lsp.config('clangd', {})
vim.lsp.config('yamlls', {
settings = { settings = {
yaml = { yaml = {
keyOrdering = false, keyOrdering = false,
}, },
}, },
}); })
-- autoformat json files with jq -- Enable all LSP servers
vim.lsp.enable({ 'clangd', 'ts_ls', 'lua_ls', 'cssls', 'yamlls', 'intelephense', 'gopls' })
-- JSON file formatting with jq
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = "json", pattern = "json",
callback = function(ev) callback = function(ev)
vim.bo[ev.buf].formatprg = "jq" vim.bo[ev.buf].formatprg = "jq"
print("It's a json file")
end, end,
}) })
-- lspc.intelephense.setup()

View File

@@ -1,41 +1,13 @@
config = { local status_ok, project = pcall(require, "project")
---@usage set to false to disable project.nvim.
--- This is on by default since it's currently the expected behavior.
active = true,
on_config_done = nil,
---@usage set to true to disable setting the current-woriking directory
--- Manual mode doesn't automatically change your root directory, so you have
--- the option to manually do so using `:ProjectRoot` command.
manual_mode = false,
---@usage Methods of detecting the root directory
--- Allowed values: **"lsp"** uses the native neovim lsp
--- **"pattern"** uses vim-rooter like glob pattern matching. Here
--- order matters: if one is not detected, the other is used as fallback. You
--- can also delete or rearangne the detection methods.
-- detection_methods = { "lsp", "pattern" }, -- NOTE: lsp detection will get annoying with multiple langs in one project
detection_methods = { "pattern" },
---@usage patterns used to detect root dir, when **"pattern"** is in detection_methods
patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json", "pom.xml" },
---@ Show hidden files in telescope when searching for files in a project
show_hidden = false,
---@usage When set to false, you will get a message when project.nvim changes your directory.
-- When set to false, you will get a message when project.nvim changes your directory.
silent_chdir = true,
---@usage list of lsp client names to ignore when using **lsp** detection. eg: { "efm", ... }
ignore_lsp = {},
}
local status_ok, project = pcall(require, "project_nvim")
if not status_ok then if not status_ok then
return return
end end
project.setup(config) project.setup({
use_lsp = false, -- Use pattern matching only (equivalent to old detection_methods = { "pattern" })
manual_mode = false,
patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json", "pom.xml" },
show_hidden = false,
silent_chdir = true,
ignore_lsp = {},
})

View File

@@ -44,7 +44,7 @@ vim.api.nvim_create_autocmd("BufReadPre", {
pattern = secrets_patterns, pattern = secrets_patterns,
callback = function(args) callback = function(args)
-- Set filetype to yaml before the file is read so syntax highlighting works -- Set filetype to yaml before the file is read so syntax highlighting works
vim.bo.filetype = "yaml" vim.bo[args.buf].filetype = "yaml"
end, end,
}) })
@@ -53,7 +53,7 @@ vim.api.nvim_create_autocmd("BufReadPost", {
group = sops_group, group = sops_group,
pattern = secrets_patterns, pattern = secrets_patterns,
callback = function(args) callback = function(args)
local filepath = vim.fn.expand("%:p") local filepath = vim.api.nvim_buf_get_name(args.buf)
-- Only decrypt if file exists and has content -- Only decrypt if file exists and has content
if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then if vim.fn.filereadable(filepath) == 1 and vim.fn.getfsize(filepath) > 0 then
@@ -98,26 +98,27 @@ vim.api.nvim_create_autocmd("BufReadPost", {
return return
end end
-- Detach LSP clients BEFORE replacing buffer to prevent sync errors
detach_lsp_clients(args.buf)
-- Replace buffer content with decrypted content -- Replace buffer content with decrypted content
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(result, "\n")) -- Strip trailing newline to avoid adding extra empty line
vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(result:gsub("\n$", ""), "\n"))
-- Mark buffer as not modified (since we just loaded it) -- Mark buffer as not modified (since we just loaded it)
vim.bo.modified = false vim.bo[args.buf].modified = false
-- Restore cursor position -- Restore cursor position
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos) pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
-- Disable swap, backup, and undo files for security -- Disable swap, backup, and undo files for security
vim.bo.swapfile = false vim.bo[args.buf].swapfile = false
vim.bo.backup = false vim.bo[args.buf].backup = false
vim.bo.writebackup = false vim.bo[args.buf].writebackup = false
vim.bo.undofile = false vim.bo[args.buf].undofile = false
-- Ensure filetype is set to yaml for syntax highlighting -- Ensure filetype is set to yaml for syntax highlighting
vim.bo.filetype = "yaml" vim.bo[args.buf].filetype = "yaml"
-- Detach LSP clients to prevent sync errors when buffer content is replaced
detach_lsp_clients(0)
vim.notify("SOPS: File decrypted successfully", vim.log.levels.INFO) vim.notify("SOPS: File decrypted successfully", vim.log.levels.INFO)
else else
@@ -132,17 +133,22 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
group = sops_group, group = sops_group,
pattern = secrets_patterns, pattern = secrets_patterns,
callback = function(args) callback = function(args)
local filepath = vim.fn.expand("%:p") local filepath = vim.api.nvim_buf_get_name(args.buf)
if not is_secrets_file(filepath) then
return
end
if is_secrets_file(filepath) then
-- Guard against double-execution -- Guard against double-execution
if currently_saving[filepath] then if currently_saving[filepath] then
return return
end end
currently_saving[filepath] = true currently_saving[filepath] = true
-- Use pcall to ensure guard is always cleared, even on unexpected errors
local ok, err = pcall(function()
-- Get current buffer content -- Get current buffer content
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false)
local content = table.concat(lines, "\n") local content = table.concat(lines, "\n")
-- Check buffer content size before encrypting -- Check buffer content size before encrypting
@@ -153,8 +159,6 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb), string.format("SOPS: Buffer content too large (%sMB > %sMB limit). Cannot encrypt.", size_mb, limit_mb),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
-- Don't write anything, leave buffer marked as modified
currently_saving[filepath] = nil
return return
end end
@@ -162,22 +166,21 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
-- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp) -- This avoids /dev/stdin issues while keeping secrets secure (not in /tmp)
local dir = vim.fn.fnamemodify(filepath, ":h") local dir = vim.fn.fnamemodify(filepath, ":h")
local filename = vim.fn.fnamemodify(filepath, ":t") local filename = vim.fn.fnamemodify(filepath, ":t")
local temp_file = string.format("%s/.%s.sops_tmp_%d", dir, filename, os.time()) local temp_file = string.format("%s/.%s.sops_tmp_%d_%d", dir, filename, os.time(), vim.loop.hrtime() % 1000000)
-- Write plaintext content to temp file -- Write plaintext content to temp file
local temp_f, temp_err = io.open(temp_file, "w") local temp_f, temp_err = io.open(temp_file, "w")
if not temp_f then if not temp_f then
vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR) vim.notify("SOPS: Failed to create temp file: " .. (temp_err or "unknown error"), vim.log.levels.ERROR)
-- Don't write anything, leave buffer marked as modified
currently_saving[filepath] = nil
return return
end end
temp_f:write(content) temp_f:write(content .. "\n")
temp_f:close() temp_f:close()
-- Encrypt temp file with filename override so SOPS matches .sops.yaml rules -- Encrypt temp file with filename override so SOPS matches .sops.yaml rules
-- Uses real filepath for rule matching, temp file for content -- Uses real filepath for rule matching, temp file for content
local cmd = string.format("sops --encrypt --filename-override %s %s", local cmd = string.format("timeout %d sops --encrypt --filename-override %s %s",
SOPS_TIMEOUT,
vim.fn.shellescape(filepath), vim.fn.shellescape(filepath),
vim.fn.shellescape(temp_file)) vim.fn.shellescape(temp_file))
local encrypted = vim.fn.system(cmd) local encrypted = vim.fn.system(cmd)
@@ -186,16 +189,25 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
-- Always clean up temp file, even on error -- Always clean up temp file, even on error
os.remove(temp_file) os.remove(temp_file)
-- Check for timeout (exit code 124 from timeout command)
if sops_exit_code == 124 then
vim.notify(
string.format("SOPS: Encryption timed out after %d seconds.", SOPS_TIMEOUT),
vim.log.levels.ERROR
)
return
end
if sops_exit_code == 0 then if sops_exit_code == 0 then
-- Write encrypted content directly to file -- Write encrypted content directly to file
local file, err = io.open(filepath, "w") local file, file_err = io.open(filepath, "w")
if file then if file then
local success, write_err = file:write(encrypted) local success, write_err = file:write(encrypted)
file:close() file:close()
if success then if success then
-- Mark buffer as saved -- Mark buffer as saved
vim.bo.modified = false vim.bo[args.buf].modified = false
vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO) vim.notify("SOPS: File encrypted and saved successfully", vim.log.levels.INFO)
-- Re-decrypt to show plaintext in buffer -- Re-decrypt to show plaintext in buffer
@@ -207,37 +219,38 @@ vim.api.nvim_create_autocmd("BufWriteCmd", {
-- Save cursor position -- Save cursor position
local cursor_pos = vim.api.nvim_win_get_cursor(0) local cursor_pos = vim.api.nvim_win_get_cursor(0)
-- Detach LSP clients BEFORE replacing buffer to prevent sync errors
detach_lsp_clients(args.buf)
-- Replace buffer with decrypted content -- Replace buffer with decrypted content
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(decrypted, "\n")) -- Strip trailing newline to avoid adding extra empty line
vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(decrypted:gsub("\n$", ""), "\n"))
-- Mark as not modified since we just saved -- Mark as not modified since we just saved
vim.bo.modified = false vim.bo[args.buf].modified = false
-- Restore cursor position -- Restore cursor position
pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos) pcall(vim.api.nvim_win_set_cursor, 0, cursor_pos)
-- Detach LSP clients to prevent sync errors when buffer content is replaced
detach_lsp_clients(0)
else else
vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN) vim.notify("SOPS: Could not re-decrypt after save. Buffer may show encrypted content.", vim.log.levels.WARN)
end end
-- Clear guard after successful save
currently_saving[filepath] = nil
else else
vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR) vim.notify("SOPS: Failed to write encrypted content: " .. (write_err or "unknown error"), vim.log.levels.ERROR)
-- Don't mark as saved, keep buffer marked as modified
currently_saving[filepath] = nil
end end
else else
vim.notify("SOPS: Failed to open file for writing: " .. (err or "unknown error"), vim.log.levels.ERROR) vim.notify("SOPS: Failed to open file for writing: " .. (file_err or "unknown error"), vim.log.levels.ERROR)
-- Don't mark as saved, keep buffer marked as modified
currently_saving[filepath] = nil
end end
else else
vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR) vim.notify("SOPS: Failed to encrypt file - NOT SAVED! Error: " .. encrypted, vim.log.levels.ERROR)
-- Don't write anything, leave buffer marked as modified
currently_saving[filepath] = nil
end end
end)
-- Always clear guard, even if pcall caught an error
currently_saving[filepath] = nil
-- Re-throw unexpected errors so they're visible
if not ok then
vim.notify("SOPS: Unexpected error during save: " .. tostring(err), vim.log.levels.ERROR)
end end
end, end,
}) })
@@ -247,7 +260,7 @@ vim.api.nvim_create_autocmd("BufLeave", {
group = sops_group, group = sops_group,
pattern = secrets_patterns, pattern = secrets_patterns,
callback = function(args) callback = function(args)
if vim.bo.modified then if vim.bo[args.buf].modified then
vim.notify("Warning: Unsaved changes in secrets file!", vim.log.levels.WARN) vim.notify("Warning: Unsaved changes in secrets file!", vim.log.levels.WARN)
end end
end, end,

View File

@@ -1,7 +1,7 @@
-- none-ls -- none-ls (module is still named "null-ls" for backward compatibility)
local status_ok_nls, none_ls_module = pcall(require, "none-ls") local status_ok_nls, none_ls_module = pcall(require, "null-ls")
if not status_ok_nls then if not status_ok_nls then
vim.notify("none-ls plugin not found or failed to load. Check Nix config and plugin paths.", vim.log.levels.WARN) vim.notify("null-ls plugin not found or failed to load. Check Nix config and plugin paths.", vim.log.levels.WARN)
else else
local nb = none_ls_module.builtins local nb = none_ls_module.builtins
none_ls_module.setup({ none_ls_module.setup({

View File

@@ -1,5 +1,3 @@
vim.g.mapleader = " "
local function smart_quit() local function smart_quit()
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local modified = vim.api.nvim_buf_get_option(bufnr, "modified") local modified = vim.api.nvim_buf_get_option(bufnr, "modified")
@@ -27,122 +25,77 @@ end
local wk = require("which-key") local wk = require("which-key")
wk.setup({}) wk.setup({
preset = "classic",
wk.register({ delay = 0,
["<leader>"] = { triggers = {
{ "<auto>", mode = "nxso" },
[";"] = { "<cmd>Alpha<CR>", "Dashboard" }, { " ", mode = "n" }, -- literal space character
["w"] = { "<cmd>w!<CR>", "Save" },
["q"] = { "<cmd>smart_quit()<CR>", "Quit" },
["/"] = { "<Plug>(comment_toggle_linewise_current)", "Comment toggle current line" },
["c"] = { "<cmd>BufferKill<CR>", "Close Buffer" },
["f"] = { find_project_files, "Find File" },
["h"] = { "<cmd>nohlsearch<CR>", "No Highlight" },
["t"] = { "<cmd>TodoTelescope keywords=TODO,FIX<CR>", "Find TODO,FIX" },
b = {
name = "Buffers",
j = { "<cmd>BufferLinePick<cr>", "Jump" },
f = { "<cmd>Telescope buffers<cr>", "Find" },
b = { "<cmd>BufferLineCyclePrev<cr>", "Previous" },
n = { "<cmd>BufferLineCycleNext<cr>", "Next" },
-- w = { "<cmd>BufferWipeout<cr>", "Wipeout" }, -- TODO: implement this for bufferline
e = {
"<cmd>BufferLinePickClose<cr>",
"Pick which buffer to close",
}, },
h = { "<cmd>BufferLineCloseLeft<cr>", "Close all to the left" },
l = {
"<cmd>BufferLineCloseRight<cr>",
"Close all to the right",
},
D = {
"<cmd>BufferLineSortByDirectory<cr>",
"Sort by directory",
},
L = {
"<cmd>BufferLineSortByExtension<cr>",
"Sort by language",
},
},
-- " Available Debug Adapters:
-- " https://microsoft.github.io/debug-adapter-protocol/implementors/adapters/
-- " Adapter configuration and installation instructions:
-- " https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation
-- " Debug Adapter protocol:
-- " https://microsoft.github.io/debug-adapter-protocol/
-- " Debugging
g = {
name = "Git",
g = { Lazygit_toggle, "Lazygit" },
j = { "<cmd>lua require 'gitsigns'.next_hunk({navigation_message = false})<cr>", "Next Hunk" },
k = { "<cmd>lua require 'gitsigns'.prev_hunk({navigation_message = false})<cr>", "Prev Hunk" },
l = { "<cmd>lua require 'gitsigns'.blame_line()<cr>", "Blame" },
p = { "<cmd>lua require 'gitsigns'.preview_hunk()<cr>", "Preview Hunk" },
r = { "<cmd>lua require 'gitsigns'.reset_hunk()<cr>", "Reset Hunk" },
R = { "<cmd>lua require 'gitsigns'.reset_buffer()<cr>", "Reset Buffer" },
s = { "<cmd>lua require 'gitsigns'.stage_hunk()<cr>", "Stage Hunk" },
u = {
"<cmd>lua require 'gitsigns'.undo_stage_hunk()<cr>",
"Undo Stage Hunk",
},
o = { "<cmd>Telescope git_status<cr>", "Open changed file" },
b = { "<cmd>Telescope git_branches<cr>", "Checkout branch" },
c = { "<cmd>Telescope git_commits<cr>", "Checkout commit" },
C = {
"<cmd>Telescope git_bcommits<cr>",
"Checkout commit(for current file)",
},
d = {
"<cmd>Gitsigns diffthis HEAD<cr>",
"Git Diff",
},
},
l = {
name = "LSP",
a = { "<cmd>lua vim.lsp.buf.code_action()<cr>", "Code Action" },
d = { "<cmd>Telescope diagnostics bufnr=0 theme=get_ivy<cr>", "Buffer Diagnostics" },
w = { "<cmd>Telescope diagnostics<cr>", "Diagnostics" },
-- f = { require("lvim.lsp.utils").format, "Format" },
i = { "<cmd>LspInfo<cr>", "Info" },
I = { "<cmd>Mason<cr>", "Mason Info" },
j = {
vim.diagnostic.goto_next,
"Next Diagnostic",
},
k = {
vim.diagnostic.goto_prev,
"Prev Diagnostic",
},
l = { vim.lsp.codelens.run, "CodeLens Action" },
q = { vim.diagnostic.setloclist, "Quickfix" },
r = { vim.lsp.buf.rename, "Rename" },
s = { "<cmd>Telescope lsp_document_symbols<cr>", "Document Symbols" },
S = {
"<cmd>Telescope lsp_dynamic_workspace_symbols<cr>",
"Workspace Symbols",
},
e = { "<cmd>Telescope quickfix<cr>", "Telescope Quickfix" },
},
a = { "<cmd>lua require('telescope.builtin').lsp_code_actions()<cr>", "Code Actions" },
d = { "<cmd>lua require('telescope.builtin').lsp_document_diagnostics()<cr>", "LSP Diagnostics" },
k = { "<cmd>lua vim.lsp.buf.signature_help()<cr>", "Signature Help" },
P = { "<cmd>lua require'telescope'.extensions.projects.projects{}<cr>", "Signature Help" },
}
}) })
wk.register( wk.add({
{ -- Single key mappings
["/"] = { "<Plug>(comment_toggle_linewise_visual)", "Comment toggle linewise (visual)" }, { "<leader>;", "<cmd>Alpha<CR>", desc = "Dashboard" },
}, { "<leader>w", "<cmd>w!<CR>", desc = "Save" },
{ { "<leader>q", smart_quit, desc = "Quit" },
mode = "v", -- VISUAL mode { "<leader>/", "<Plug>(comment_toggle_linewise_current)", desc = "Comment toggle current line" },
prefix = "<leader>", { "<leader>c", "<cmd>BufferKill<CR>", desc = "Close Buffer" },
buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings { "<leader>f", find_project_files, desc = "Find File" },
silent = true, -- use `silent` when creating keymaps { "<leader>h", "<cmd>nohlsearch<CR>", desc = "No Highlight" },
noremap = true, -- use `noremap` when creating keymaps { "<leader>t", "<cmd>TodoTelescope keywords=TODO,FIX<CR>", desc = "Find TODO,FIX" },
nowait = true, -- use `nowait` when creating keymaps
} -- Buffers group
) { "<leader>b", group = "Buffers" },
{ "<leader>bj", "<cmd>BufferLinePick<cr>", desc = "Jump" },
{ "<leader>bf", "<cmd>Telescope buffers<cr>", desc = "Find" },
{ "<leader>bb", "<cmd>BufferLineCyclePrev<cr>", desc = "Previous" },
{ "<leader>bn", "<cmd>BufferLineCycleNext<cr>", desc = "Next" },
{ "<leader>be", "<cmd>BufferLinePickClose<cr>", desc = "Pick which buffer to close" },
{ "<leader>bh", "<cmd>BufferLineCloseLeft<cr>", desc = "Close all to the left" },
{ "<leader>bl", "<cmd>BufferLineCloseRight<cr>", desc = "Close all to the right" },
{ "<leader>bD", "<cmd>BufferLineSortByDirectory<cr>", desc = "Sort by directory" },
{ "<leader>bL", "<cmd>BufferLineSortByExtension<cr>", desc = "Sort by language" },
-- Git group
{ "<leader>g", group = "Git" },
{ "<leader>gg", Lazygit_toggle, desc = "Lazygit" },
{ "<leader>gj", "<cmd>lua require 'gitsigns'.next_hunk({navigation_message = false})<cr>", desc = "Next Hunk" },
{ "<leader>gk", "<cmd>lua require 'gitsigns'.prev_hunk({navigation_message = false})<cr>", desc = "Prev Hunk" },
{ "<leader>gl", "<cmd>lua require 'gitsigns'.blame_line()<cr>", desc = "Blame" },
{ "<leader>gp", "<cmd>lua require 'gitsigns'.preview_hunk()<cr>", desc = "Preview Hunk" },
{ "<leader>gr", "<cmd>lua require 'gitsigns'.reset_hunk()<cr>", desc = "Reset Hunk" },
{ "<leader>gR", "<cmd>lua require 'gitsigns'.reset_buffer()<cr>", desc = "Reset Buffer" },
{ "<leader>gs", "<cmd>lua require 'gitsigns'.stage_hunk()<cr>", desc = "Stage Hunk" },
{ "<leader>gu", "<cmd>lua require 'gitsigns'.undo_stage_hunk()<cr>", desc = "Undo Stage Hunk" },
{ "<leader>go", "<cmd>Telescope git_status<cr>", desc = "Open changed file" },
{ "<leader>gb", "<cmd>Telescope git_branches<cr>", desc = "Checkout branch" },
{ "<leader>gc", "<cmd>Telescope git_commits<cr>", desc = "Checkout commit" },
{ "<leader>gC", "<cmd>Telescope git_bcommits<cr>", desc = "Checkout commit(for current file)" },
{ "<leader>gd", "<cmd>Gitsigns diffthis HEAD<cr>", desc = "Git Diff" },
-- LSP group
{ "<leader>l", group = "LSP" },
{ "<leader>la", "<cmd>lua vim.lsp.buf.code_action()<cr>", desc = "Code Action" },
{ "<leader>ld", "<cmd>Telescope diagnostics bufnr=0 theme=get_ivy<cr>", desc = "Buffer Diagnostics" },
{ "<leader>lw", "<cmd>Telescope diagnostics<cr>", desc = "Diagnostics" },
{ "<leader>li", "<cmd>LspInfo<cr>", desc = "Info" },
{ "<leader>lI", "<cmd>Mason<cr>", desc = "Mason Info" },
{ "<leader>lj", vim.diagnostic.goto_next, desc = "Next Diagnostic" },
{ "<leader>lk", vim.diagnostic.goto_prev, desc = "Prev Diagnostic" },
{ "<leader>ll", vim.lsp.codelens.run, desc = "CodeLens Action" },
{ "<leader>lq", vim.diagnostic.setloclist, desc = "Quickfix" },
{ "<leader>lr", vim.lsp.buf.rename, desc = "Rename" },
{ "<leader>ls", "<cmd>Telescope lsp_document_symbols<cr>", desc = "Document Symbols" },
{ "<leader>lS", "<cmd>Telescope lsp_dynamic_workspace_symbols<cr>", desc = "Workspace Symbols" },
{ "<leader>le", "<cmd>Telescope quickfix<cr>", desc = "Telescope Quickfix" },
-- Direct LSP shortcuts
{ "<leader>a", "<cmd>lua require('telescope.builtin').lsp_code_actions()<cr>", desc = "Code Actions" },
{ "<leader>d", "<cmd>lua require('telescope.builtin').lsp_document_diagnostics()<cr>", desc = "LSP Diagnostics" },
{ "<leader>k", "<cmd>lua vim.lsp.buf.signature_help()<cr>", desc = "Signature Help" },
{ "<leader>P", "<cmd>lua require'telescope'.extensions.projects.projects{}<cr>", desc = "Projects" },
-- Visual mode mappings
{ "<leader>/", "<Plug>(comment_toggle_linewise_visual)", desc = "Comment toggle linewise (visual)", mode = "v" },
})

View File

@@ -13,7 +13,7 @@ in
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
nodePackages.typescript-language-server nodePackages.typescript-language-server
sumneko-lua-language-server lua-language-server
nest nest
nodePackages.intelephense nodePackages.intelephense
nodePackages.vscode-langservers-extracted nodePackages.vscode-langservers-extracted

View File

@@ -0,0 +1,78 @@
{ lib, pkgs, ... }:
let
wrapperScript = pkgs.writeShellScriptBin "rustdesk-epicenter-wrapper" ''
# Grant epicenter user access to the Wayland socket
${pkgs.acl}/bin/setfacl -m u:epicenter:x "$XDG_RUNTIME_DIR"
${pkgs.acl}/bin/setfacl -m u:epicenter:rwx "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
# Run rustdesk as epicenter user with absolute path to Wayland socket
exec /run/wrappers/bin/sudo -u epicenter \
WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" \
XDG_RUNTIME_DIR=/run/user/1001 \
${pkgs.rustdesk-flutter}/bin/rustdesk "$@"
'';
rustdeskEpicenterDesktopItem = pkgs.makeDesktopItem {
name = "rustdesk-epicenter";
desktopName = "RustDesk Epicenter";
exec = "${wrapperScript}/bin/rustdesk-epicenter-wrapper";
icon = "rustdesk";
categories = [ "Network" "RemoteAccess" ];
comment = "Remote desktop software for office user (Epicenter)";
};
in {
environment.systemPackages = [
rustdeskEpicenterDesktopItem
];
users.users.epicenter = {
isNormalUser = true;
extraGroups = [ ]; # Minimal groups
};
users.groups.epicenter = {};
# Allow dominik to run rustdesk as epicenter user without password
security.sudo.extraRules = [
{
users = [ "dominik" ];
runAs = "epicenter";
commands = [
{
command = "${pkgs.rustdesk-flutter}/bin/rustdesk";
options = [ "NOPASSWD" "SETENV" ];
}
];
}
];
home-manager.users.epicenter = {
home.stateVersion = "24.05";
home.username = "epicenter";
home.homeDirectory = "/home/epicenter";
# Add rustdesk to the epicenter user's packages
home.packages = with pkgs; [
rustdesk-flutter
];
# Declaratively configure RustDesk for Epicenter server
home.file.".config/rustdesk/RustDesk2.toml" = {
force = true;
text = ''
rendezvous_server = 'rustdesk.helsinki.tools:21116'
nat_type = 1
serial = 0
unlock_pin = '''
trusted_devices = '''
[options]
av1-test = 'N'
key = '8jkD3HoWK+flkWcAMIqRnyn0jr4r9VPb+JYIbBtb+7k='
api-server = 'https://rustdesk.helsinki.tools'
custom-rendezvous-server = 'rustdesk.helsinki.tools'
'';
};
};
}

View File

@@ -1,6 +1,6 @@
{ config, pkgs, ... }: { { config, pkgs, ... }: {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
chromium ungoogled-chromium
nodejs nodejs
# Graphics and font dependencies # Graphics and font dependencies
freetype freetype
@@ -30,6 +30,6 @@
]; ];
environment.variables = { environment.variables = {
PUPPETEER_EXECUTABLE_PATH = "${pkgs.chromium}/bin/chromium"; PUPPETEER_EXECUTABLE_PATH = "${pkgs.ungoogled-chromium}/bin/chromium";
}; };
} }

View File

@@ -1,46 +1,47 @@
epicenter_vpn_ca: ENC[AES256_GCM,data:DUvpuL92zpQkK0auXGdHDw+f5gzjMARMroBknmgR+eq1LV5aISdA0XOCw7d3VFpMtHY+tPM4pDEWlnGJDoHDJBSFAUdmbLkDq2DvoDR1RBbsidmlXpvu7UnL4OyCgrN9G3I65HQmCh64/453T0Y5MZKUiFZXn9SZJgOU3h0qOnAiIKTQADXmUo6imhLuUdPxjwiLkNp8zHNystbfUuZkF15J+TXV9yndy/E8E4sFs4uysK9E+VM84v0q75zTf48cheE+cBGI9xOP5QMvSND6MloyGYUTPZOiQyz8M9AmJObvQFryysKf5Q1W1GBiTz9FVuSsr1IM7meljdBYwfxQaA5MurdsXVFYfdRgL6NyL5x7WOd367pgtBBwuVyT+cygg5ITIBc6YTuT8thp/q0BsJkq1OdVQrLa4PK12Tg2IOUg2Za/tLJxxiNWqs1gAmTWEIGAeJWmNgCZAjJIISwTGcdbpdWqhjAEgaaLf9ZD0hUXQ5MmSO+KXzP7lIGgqCXoMEc7W7rn3R2VkIrvaVgCBK3psTg6+CxoPQwnYbUKgPLG7ys54eECJyRfc8YPH1957Q67pYkVD166ZP/sDJfplOGH19QyFnaaSLRLoXCAfWuO72NwO/fSljN4+pmB6Ev1cRCe4mXicz7TTqGG740VOam/JW4OJCrnHVs67cs9/MsVSNZOsI+x44TKJjaFph4onaodDh5P7e52IkfRnHnjK6FjEvYPasUr9YDqUR3ucHxhD9UqvINDwhp3L/zyFb2HRuO1KzEQLzmG96xiJpJxBVl8GOTeNrK2owOf6cCuh6o3iPaAFjFok26gI1ujX/mPbBigOxB6S0cLOLoA7oA+E6L22nsoYdIwjU4b6Y/DQvndgsZFnycLsSA4TRYHUH1Q51fGU3S/zAlB66rYchsw3JqODD51axxEdo5uu/2a6K9c8BSDo+stHBPmGvty6IorhM+17IGwSVrnxBFbSICja/Mi9eHmkUuUQWaXe5iWiNGYOIe0Xsbu4PQANhDE0f4U1LboVdI46uVhBV36zLSRJ5hUYARdmaz+aUSfNSE20xwCQiqd4U1cb2W6ZRER5WOfNFa8LCjk1YyhDY1yKCbo5tZrYZtmo4T47EuH/2uyW9vPtDlAhpZWmJJ0LEbZMhl9hIEAgGYmhnxPIVItJWHgq4O+YavYWvu1qgbdBC/FZJ8xx0uSy48oKCbuTUbIBUHQ37/6wp+IC+FAoYc2CDgCKzYvYjGrjMr+l/bhWE6KqI2DE/8yG4sOZIyrKNOYRq/aqDRkaeu96bLSYZECoLpohEKRNLTFQ+J8btjGX+xak0HRNEX9bxx8Zs3ml8mDKfh11uy05zPMVU4jaLrc5VtvmNdCg1EffbtEIRhi88aP5K2flRLxvSsYODd8iisqJ3CEqa8/C/FoHhWqgs7vk9UeRs46CJjGQ2Nx7UeQhAK8ey8FwqqSPQ6hp6jFnAv5ha583GZm3G8CsapajioHOpNcyYRhUW/ekdQ1E7DafOLRRO0hdWws8fsP/96uuWJ1Ir1ec2pepmh8s9zCZl/CKSU6+PUjX03Y9buZDnAYao5nDFsF5hgi2nLCTRbHnCh/S5C4NL/Lss2gi/9HdQUWr3KNONgoGbdRNS4MHtK/t9MtxQT8FOS54fM76XLygYZhQEQuDHUr3vaihOPKXncPNx+M4IGd+tsOoGADfpZk7W4OLd5jl8OiCulKvmRXzGCrmyofifh6XBE/EDa97j4eXt/fZPhUh+kv7i39mLKiccPUqpq9WYA/pqlMc84PAsewRerk3Z7jygFb2oX8LwYX2vDer565q+74n/y+oqz/CQ7jypoGBC8f9a16h2e2ZuvjQZ2sUdBB0xKwmLHC5mXLRkJYZ8Myt0Bzp0iVnC8P,iv:0GfL3sG36nsg/4BPw32kKMB78TmbN+mLq/mqEFp0yas=,tag:x+kxJsS+Fn7VO3MlOmqgwQ==,type:str] clicknload-proxy-config: ENC[AES256_GCM,data:lGoX0iItw21nFT6n5qZRddiz9O0KGjaktXzHamSBmpE7nZv0INhIic1etwrluOnv4n4+0pn6qGeqLcEYsN3S9zDDshdYgrxmPl1xwpVUkvCIA8MGMSg1rdLByYjMOA==,iv:F0sBov54Rk8te34n70s4pPitQXjCfrKQT1+SgQO4ZZk=,tag:x50kpNEr5hYipdIknRQ99A==,type:str]
epicenter_vpn_cert: ENC[AES256_GCM,data:y94SNCZISKCGbG3dtMZKPntzHvmvAK9fr0+TASNUPp+RG0o7sWRZHrAk+zs4x6tTJpRMCN3VJUzH0bkSHrsKHsYLwVOT5Sb3l7y0CUjjT463NWj1ZipaMU94NRbtDC6Q4sMkFQPaLEefaOhjQu2a2qMx0FkQ9OA3T4f4u5GVN72/PTftHw3SybhptlF5L1e3Q69J3H0uVGRuXmmrws6HcLb2th9sAYPAi82yEimdzPa154DKIRcBY78QVkRz4X7VVyLFY3X34TMvGTmKP7MsW5lApVd/qoML0MkqvrWdEClMWG7i4jMRjSUSRIVr33DE8ds4aBx565tKQ9rVZT7KPZU7IbFqgKP6TQN8w7g5mY3aImPlRMB/xb7V8a+qBScOgBiwCgdAnz+PKGaCwcaBba80q0m/gONkxgVy+QKLLdALjb3iUpKiGvWFLwsKr/hQ1O0h7MBFDTGqWniiXZbyb39HABZNVtKAC/4qouG/G+hP6fpk/+TMtjRNSh6wWtyiYvTeFOtCWfi6YEC7IZFautr3vcu24soA+Q4vFwYr6lIEPnQYlpBCr/TLVzEvxWEjsR8G2RaBSWGm2E0tre8qVSFkJwz+niL8FQgaQTjnsYmJGHFS8sxseGuAakuSH+gzopc75H6y2pEnZrpQMOMyDq0GMkIW8xGk7x5Ewq1DZ8ji13Y8xtnbLqiJkhPc19JDoXdpls/40K4Ymrz6dOO8zxzvW8SHF47gOYvp+a5d3vqwXFZH2qDUvdScV5eATmK8ltfGod9PbZWgFzhp336dkba5aAspXlyzAlRRqrVhvpff+V8cyHTIz9qA8fhXv+v2pN0E/Es1NTJhtQo09OGnZn82lfVyR93hLtr6AgbDggwhvAOlJr/pAt0YZ5FhehhhnH0+ekUJC0YemUMj0QVbpxIWc4rt5n7nqewSHC8feo6Zfc4NHfE8sWemGGm2sUOdf6C2F9k0h+Snwfyu95XIWccMAC/Ii14ciu/nj0L67bbj8XAECjWCIlhhCJcMXlpxfU3lZX7zEy4WH+IqV1399KmtvUssuObBb9uAMwHjZl0l2GVQPo3clDea2ZP5HDO9B3kWRqpvIrjawh4IM73O43jbIDgjNXMijnH0GJu8FH8igS+7JOhRHrOBQiNc0unx6EgAfXHo3v6o8ktzbtRcUwU8k+wldU1cu9ugpVpG9j8O+zvPYkaH+0xfdAdxDKXz+4e8462O5zr3IVRp63CBBNtnTn6fcwnxgD9ouhgyyLBylzrwCmRAODHyukJovkNARWmhcYCAnYhrYf9KqaCoeRLmFq8sLMeiYGDTJ/+PTyheZMHboaoZbUbnRtrvuXQiCHqDKxSan0OACW+oXgCBgZSshj8lb58A+zjpMVy4Wis8s6K9HFpiPSP4tmHxXvOod2+qL/eZBV4LhLoahO0gg3+DUPTeJVO+4I7YZYLVXBhf0OC7z8jMGf8Q4SjO8p6Vufx2KXIEpcITto0IJFsIAv9UVfsKVyvVeuGaGGi8VQWhexdQxjDtoURRts91EFNpJ86o9HPbQCAWXBx9hlnBH2zZntqPd5eVA57s67i1dWpuudD/oBzGn3fBv3Fck9tpArMNSrOtbtl3sBc6Calo/5+HWSY4tz1Hnrlv0/IxD2EcgzH5dFu6klTn3gaMdgd288/kRPDzfdcVGqlvV8rQJjTD1XN48FK/Zp2ydCede+SKLidqrqz1ISXOwsya0ivfc4Vdk3hhnnkTnqZT3qR88Z69X+9QkYCVNaoojTiBoRCekqNDI3Ev8MKp1eX0E3rXT3AG3E7xEFGulhe2C9RdMwQg0QG/Ws7n/CtTeVwpv+JG73TlDnBzoVHN6fqKBVchbGPtcObM90N2itJ/wf3jXefhrrsQudLch+Mwp72YcvCKqOMsrO3egvNxIiLKrrVOI8buo0NJrPoQE3/+rZQFzx2r0AHkrwEUHGUvr2oHDJyXwSY2/7zCYbewN44Zjo7n5ofOhSUsaGJNySHgC6V74gyU0UCUEBnF8fZK/Y6QgdtT9gApHVtJwXRvLig2P11bIlyWZjISRaAVAUAMKdS8ir8l+P3NL8kBZstfpH2eeYFHIWeZ7VrVTs4CaHDzkyMizui0EBfvf5irWeJvIekCWnetfo047QKJvrKr+6239QxH5ni7wlzWxiZNcewaOfCK2SqOvYz6NIRp+/blZuxL2pXhTInB/XxbP2zHH64dC8AVvViU8bI2DR/HhzbbpEylzD4ttvil7hLt6AMpugFrmAgWd6JU5yM9lXoTSprmXZ7awYTLcQlCQu9cUNXF0JdtELA/oQipEEUz+2lRt3B+0abYGVWb,iv:MVId1jgmyhY/iUxnjca5IpYwlzUAsa6Nwchg52AKgRc=,tag:1RASj3dFAYVNphJ4zjXxtA==,type:str] epicenter_vpn_ca: ENC[AES256_GCM,data:qWq8Y2JRQIdEzVOKki8YmSMhpxFmFougQSJtGTaSgak2uP4MRzgrwpKGqDqhkPfqXeD1GSHRJNPe4plIv6//en0IEjV4vvupUrJuSp5cwmjhvbJLWWpUSQOhhDM/sN/ODDPLy+JucUoQRIt3bgrOoWRWro3cPuorGcFJfOedOy2tc7oTrpQBYNyx32kDfh3IHzuaasSFtCAnE2y5TVEXsLcMzkzvBIpTGzHUOG+9tn/nR5Ro7EodQKXfs11pL3mvr/iMlRX4QTSGYfl3pJE4O+Jssn1+/zDd0bNrSU1kKunDsuKDlv9Nkp0hIjpCzc55v74Jrda+UATB3f0muLLgULbsFA3JfaqqufLP/rgYjkFZ14qidjby55Z2+IkoH6U6et2jL1O6BcUaxYo7dbsE8kbNPdk4XPoV+5WYSOKOQrx0xXiW33fQnWq0R1x3k6YJ1oVO1CoxMLadbaAkjhoZeAP+VFviKHoX/GqMGlkox5KgdO4dUz4UcJon6F69wG/LzXf+gb1VBacqx1MltrdN+nUFNTowFor0AofUofWMM6xpn4Xy+gLy3AFQi2+3hkBmPbtgcWEWnTuekhixuPUrO8h7CwXMRgpiBBqeJTQVVoQ1puY/1Rw+K1KXY4QdRwd5laynkU4owvJtgHk1jogUIBpfAOtIq18gOznS1HhgH8s0Af0eU7lSAAPDfRoe5awydb9Wejpjed2LypMwtYE0qEC+tR0cq+NTYnINg9bcAwDz5iRSpKtGPmy8Xj320LAlH5eDyhcxQjF4ZHCtJ9/3QcpbBJoIwWlS2+R0fk4qOLq1/q4RqnP2aDc0CLd0MKx+tJs9yBZyMU8dLJtqTnd7z28odhrZ9E7PmJ/Ky2t7WsLNZF9lOWWbJ2og2Mxsu85HCmntQxjX8Jk17Eyl6jOl5NFnFEWMTPfSsaf3J5HcwffGV3RjdM/XAleHCoNP+ydM/hPra+HsFqoyMYqT/HuZPv6ZMLY1KogBy/17OdgO6Pava7kB717v6t9/eO2FH0LGKYrPUJBha8Yq7vUJFhpABO29vTJSdG6gfrRpPafgKjZig5z8BXNU+R03DiGrL9LdraFUFnu+HSF4YVXd0EGrVfQF7mw7KrH/TsAOc1KOtlHaXFkP/SgZ+etwyC48xCy1sgQi/7t2kozPtM+RcAQJMhF0DHGFQUWwj1HesHsKaOl/HdM+eU7DcMwN01is2lD1HUp63HC5OBItET374DcdqfVUBBjiOt3srd1OWuE+FkrqKpCaxLihu2dHP8SR3DYMlSIrGwy8U09Q3iIe1hD863HVOCcn3uQZHj+g1GOXpUytouUhE9EEC0THEaPNwygNQvSu9AmG+YnFaXn6ZWVokbvf4xfttSc9KUD50qK54gd7Aa32fvzsPH9O6gMtQ18wPmeCzTzVEMBFUO33BI38UKpmKB4/r5k6F53L5YOOvVsD6R5H/0ABoIjXPF8gFW2f8RkGuAH+8dOS3VdOWFvk7ZUQxa8QjMrg09n+khjZ4kPPz42W9uaRVQeXAWLhcNOBeT7U/pT23Z0M3Qfs+NK6JRXhIOj1Os77uSkLYVKuf8yfhbI+1J1/Y7WVe7/W3BkHi7SN7Unyt30QntY+FrAfbFb59aqNptkSlmDX0TbVTWxAeD7NGi/nBnZSJyE40iJ86YzqDjv2E/oS3BWRV023tfp8iljRKKD3lLinheKwaFoZE5s+h2MY1tGkO/LeZlRhxsBznouCinQoqxdxinNxCiSpP8tTxcZ1VW5W9/JxWwoRhTR/WyDVa38vVHaVMVmhCruMDCHlckHMU5qo1+5xNkDNM2kirADqIJ5CygRI+qc5CLVWqmmve/ICG5cGff3JvMTENnptNhkWIzHDemMukJpaPCF5pmH9mP6b,iv:HiTBKc+JD2NETPL4JYOfYCHjNmagZXscpWV0o1QXyfE=,tag:SM+tAq6NfBG5xDnrfqN96A==,type:str]
epicenter_vpn_key: ENC[AES256_GCM,data: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,iv:pB/cNgmHi14ugi6kd+J6poWXX79LMHiiakNa03ibZ0Q=,tag:nLfjOesXDm5/QtwHznJROw==,type:str] epicenter_vpn_cert: ENC[AES256_GCM,data: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,iv:jUlERMQxELOGGZBu9ZZ5Z3pIaNdpiV38BnSCfvkS5wg=,tag:k7I+eceH9Kuk45yHjV9EHA==,type:str]
wg_private_key: ENC[AES256_GCM,data:A80vGf9aMxowC2xME4FIVTmKpSRLNB2tWiUQeP1v8vCRk6Gt8BKYOuXYt04=,iv:vr7qvfr78syrI5pIytjLouPwZcw4xvBTvEUzzv7ibnQ=,tag:qjALlFkd8JocLJqMKFERaw==,type:str] epicenter_vpn_key: ENC[AES256_GCM,data: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,iv:IAxV9RV1hqq1Fn6e78a1Iv353mVZ1Qb/yFpzkeFtvjQ=,tag:deb0M9hJPqzsL0shaDJ1Pg==,type:str]
wg_preshared_key: ENC[AES256_GCM,data:bhXoD95ahDRawoHd5Z35FY0G6Xv0PHwWJf300fHQ5jNsGN1TQKHsIswx8YI=,iv:fBsIWkVZUt8pahuO9daaRBIEEIWsSnFW5Velj9uP2ZY=,tag:RvbCYhnRv0OrjTxjsNFW6g==,type:str] wg_private_key: ENC[AES256_GCM,data:XJ2dZINMd+gKlqdQtI9XZw7nQ5UIx0IWbYdj+HWIa4x/zERGPZy35EWcK4c=,iv:6YWnWwuHHw69S1gq4xrS3p0DXpF6lJt1lmZZ7NJ4uGU=,tag:gpgMkD/e2p7XOy7uacv1WA==,type:str]
wg-cloonar-key: ENC[AES256_GCM,data:ZMEeIZApOD0ij3nPMZeQRwJ4MwVx0sHu08F+m/u6IMHBGid5YwMgxZ7qbLk=,iv:OfIZ9TqBLjToIQi7zRUBATrynBtu0bzXeGVI/EAUPhQ=,tag:mJICT/ak5U76JE/IxJsCKw==,type:str] wg_preshared_key: ENC[AES256_GCM,data:k7NdIMYTes35HKKCpDcPCCoGo3SdO5KlwB8C2EvSAJmRM7VFOfum6TYNae8=,iv:gbYR7OTOsIaFQtchYxblHcoj7dc9D5qz+N8F4+Ru9os=,tag:FfxMVal5j1593s/HHprlYg==,type:str]
cyberghost_user_pass: ENC[AES256_GCM,data:Eaz8iEV9vNZh/bJePmacQ06zU8FfIA==,iv:GcnU10VLVJsoeIU6t6eVjopLsBamvk12DpMbM9BsBv0=,tag:rLYoghkD8O9JepnopJfiuw==,type:str] wg-cloonar-key: ENC[AES256_GCM,data:lHJdTGPA+XexVKA3JCckUkGXQm+ev2p5VhPnHpsYfRiao+rr5+N3byoBw3I=,iv:q2djx79niGelafKNl03OBPfMAcIAyPhEqsiN0UUum3k=,tag:96CU7iCd+Dd1iAKmHWcEkw==,type:str]
cyberghost_ca: ENC[AES256_GCM,data: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,iv:8uQbG4ObsDSS0DeKx24lt1vpfeSms2v7KGRQrKoWwds=,tag:2RoiW5VWLXfMgXA4cbnKBg==,type:str] cyberghost_user_pass: ENC[AES256_GCM,data:ogaoPZDUosjpoCVmiIrvSnVEcCYSUg==,iv:QS8HQbcv4t/Utv3RwVi2xS2mSvnr44z0YwaqKGHGXPk=,tag:bwh91qZDkE4N0So2dbd8iQ==,type:str]
cyberghost_cert: ENC[AES256_GCM,data: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,iv:vYJer+NYyRo/jcpGb66askFA42T+TmSfWTm3DKOIIt4=,tag:RlvqzLfvtJN0HloJZTJb1Q==,type:str] cyberghost_ca: ENC[AES256_GCM,data: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,iv:n1d++hy58nH4kZHEyXrELMCdQ3botkc3tHUA/Czm6iw=,tag:Fcd9qGgOwqQ5uZGbbObhLg==,type:str]
cyberghost_key: ENC[AES256_GCM,data:qfPwy1DSooR6eo9O7j6h43wILh4PzxE2X0pdbzh3gw1D98rR8GWEj8sukK/L0TzfGYqYWDPy15OeymaGdm6p1WtevBOlkZtQcXmo7jg3+k0TFUnUctNDm2cOjuKck7CGDFtwbMsOHtCxwhwYouCmes9sQ6KSfolVkutVcvEfI1qhjOvJabsZt4O0P5wB4dQUMQfcPwXYKLuW3xlP2MvIn+Q6XZzfLOqoHnAwQhyxfGsOeMkPV2GXzMsqcCuDhfItU7d1GHzMCOLQsWspwNr1jJd0a56YhY+oPHs+yRSEvzFeL08ZyQjPr7YX/p5w84aJrY7THDlBcAK3d35EQOPvAEghrbmas/aKSxDXPL2W3GKKhok9ztpX6QDywkgwUbetXs+oVUF3sHK+UlyiW4RLPsKoQQPtHYBvgIwtT9ym21PJvgFy4dOJF+XW3CfpZcAOONB2L2mxrhB04CV0is3i+SnQE4tVIqGfQBVrDeuGB/+7Pjy2cW0zkEOAHrwGbdlqG0kgdHFOdbDp2fJl5zTRd9hBhYeXT9mbFtlJ+50KyGOAkJZf9a3O0BwgYRR0sdaEWfOLDMsz5L1DlvzidrChGBwFbHAyuFkjE/hkhddAH+1xiCfRCb+mGqs03rDhIpJbEMZLRZFsLWu+hlXKZ4AXamkuOvkk9KvrYL8KVjkVk1GIIeQqVFE1tiSCtlGeODYblA/5HPTCH1wPrLBNCRS6xkxEwHs/qZx6J0qN6j94lWDWa/Q83g7L3OKL8JTlsyr+D9100veMUVhvo8RiJy85Z7tTqXXEckEWKgnC53dZ1ruILNzupytUYa9rYjNcQ1KXy0dwYbMDQ4kvLCpJtO47pXo1i9V9bDFrYCLipCtyg+s2RUzYOnW4GXpn+de9MDEJ5oOp0pMZ25HG3fpnZDvaIyPyytpyj7apx3VDf62Eq5Rodd49uKuDjlbWDezaaU6MjRqhmpv8dbs7c/7pogZmaztxKTbs+EZ/6l66cek24GS7GjQj0e+N32vY1ACcQUCjg9smCGvhKs0H5kL1YOJkopN9ZWVV9p32p0IsbQHsRebSOHablHT8q6tyYfP6ctHH6BNfoUlfZjHtR52VEiY0OstmnjsJxE2hYPX+l4tpUGNGv18nQ4C2ez3baMWHNXzRTXo+g4zjVkW0Ggi0ecd7I5vR5vUNNA74Hv5Mrd+oAYkf/65XGlTJu5VIM0f7IvlSintmmM0pLB1rmCgnZenlhhbY+0862xS2j1YcZIapox8JT/W2IRUYKnJ7j7B3bc/57JWDzaBM/AukhZvERqdm12fhh9jNrNDjK25HmNwdh4Bsf7IyVqXh/nSsH5MtTCASonn/DG+6pGLwOCxKiQdNmlYD8yiZ2RkSfFs14glr0frUYcgCEOFb18IeF/e6srzKSocRVJqKAoCQhnu4qNMy7YDXpmucnzhWuwcFeKC6gaTKBPj6pxM8OSXM7uqe27So4flZIbNU8yvDp0Ub0LYco/EJGUEL9lwitMydSV0F3gjc8zvEbYMs7qlYg9dD1jxJd5vaPkx59DHCpEMOHn1CjVxazZNvexJLG7j4FBkM6PhF131/C7C4cly8ASLQd5E6nZb54GZdKqDp6MVgipeKJ1Z8A/onBC/Glal1bl6ASeAO2Mkfq7PBJ54CKXaxiQ+TFpCfy7DrOArQzaBJ9s/Gy14EoGfb/o0VuXhj4vwChig16x7vu5MnEFdItPjkfRa638KkILeS4OXm0ae+KKxBUchCwXdRyGK+wn7Kz9E9xOjcKMI/kiDnAB/mTkLC+IA5wRuXgtVyFk3BuvrJaYIHsJVZofP5lYz0JLmXXtPyytI3r/IwSL3B0jupo/KHUZx6MBwjBYG/fKJgTQvvbGDePNFHfk5y/At96P7QZIVO+QlmQg1em0pRpYbO30GVhcDko9LvNG5rLPB7a6SNB/pllOgBjbEKWoNPVwFVe17ILzA2Hl/egkW+wxiJM39bG7Ll1bMN5Q32yNZlUAzWBHDSd4DhM14ock9irRGtLqrhHH0QyMr1WlVoMRXTGT7aMLUBc4IEY7kXnx4Sw/RILqOFZsML6Kz5EEroTR/gzDpr1xYxcIRrRmaBS2YMc6wgfIRstsspMT2kUUT5n3MBJlur2CpyNEpqh5o0oRpACQyqdc9P35BVc5NYdAIg6zYKHtiTpl3I/Py+XDSDT/wC1OrKRJjuw0ES/wgDoygbPxieqdUGCqDkEjuM/UZqmIILAHBBvqniMVB5QLO53NO4lhrTbhmBWDXzw9dpJ77O6In8DmQ6cmuBsOAC1JqlGhFHpp1YMKtlQlOpwsORZSdwc/sfMkai/dXjhoh9Ptpk1fUKH+3AHzd+Ulb6Y7daInVgutnO/XFj832l8WwHzyZPLBTpbRbHeeSQevIHM/x/jLDZlUGVktrN04Dj4CJJstyeETIegzILTrXUhIq/W9cEd9vEdlSRRwTn5Z/E8zfET5fxLWSrMRTTQWOh1jIh7FI+P9d3x0K0fk0CR+vclyvVm506zAgF8KCBPjljDZYgeXyPMJ/icU9QhA72++IGo/XoT81fpB34Zt513PQk4WGmUGhAgiQP1EObnQ/YNaTcdUU8fhvq7YcyUlRA+vfaKMS1S18a8RKfYs0yWxCG4CuCb9pWhRK2ALjNeqYS70dFeeiAHI7UKjV+I5CbtLdIt0/oGDFKokH5xRVl6JkLq8iVPCaCyCOc0FCfBVyPBF3Qaisvh3GyIpCPbx0mp8fDRsiXLJShyqaqC6hpD88PnTDE2mvMyB/bH8Quzh5OrLp2CTDeOvWtg1GxYowiL0CC77ogUQhfM1ut8g+tvjgsmuiXU1uEzpHCsIXnSnUvwJW6tV37S0YUePoVAi6hVJTr60M2s4+0J2IxDnb9h2GVZqmusyQU74cKXJrEOC6r5apkhH2GG2jdVQGn2ETY/b/cbFtI38EtT4IbOa/OQDLf9F0UJPuYi5Ne0mII4asEmWdfco9P5oU4Ssr39WmTfmrBLvEQ0IhPSwKmtZlA1x85FSv5+wKauBYw3GTZa/tsp7E2MHhkyRWG1eMMziceCOrTD7NgRRLQIgN+ejm43i+OdMzi9vjzf1vg+barXOOd34yjY6zqpX9hEAJuxYgT2s8fGj59ljZsqp/qSICBIiImOyjxgKou1nh+AJY0Qs4fIGVTstf7aG540IiOGNTy41sVWdtKBeeahez9FB2lxMZ+A/83IRtmlnMcqXKhIgS7c3ajaRrgjesO/fbHnE557Ocz2A6SdoOn9rqVXD6VGvM2fkbj8no0ooCnvNtQ8JEMu6hyVipSqgRxcZlnW05NGOTtWxV3xRI3RK5tNguYgqqLIQvSARDDgm/HtJoJgom8lqSkLXswGjbxpJ1GP9lWpyov+muy9viwLQZQgEma7w2M0gIB84s3FOuYMIz7pKEDntUO7jRUl5+T8gJoJ9ex32InELTLWgksB8mb4ZuSCOmoISEEaBsPYtSG9wo5bWg5uz0/ToudFf+WRA9JT/MesSzVhehqFx1eMGAKHoRd02oMOtwIzQuc0Xp0DuYBTWOk/aHyRDxfluaFSIyAR/KBkO7fc9jKTu1JJifPCXZWInRXvPogmafCyYicC3gc2ITNumezK+oD8fjbWHW0gDIvk9qQjxhQJz99N0XYiRLTl1SssGMKADMzauPWSMDwn1VQ8t9Yc+NvT9i+9TB5YL7E3XhD3aqqwLbLOlIjvlaEhWeMjoaGlaVaz7u2n6g4zAGcVG5/uocZ2QI7byppVE/8rpjBAs1zQr3A/ajvcgCQ3+3g+sy+3w6C1KmYJj3kyF14FhTDLWekAyX1VlEQCB1BUeB7lc3CuPftTDSRKLU/vOygvIdzLMJdsQu/98qjOawsKwEVSXtSrrLXTcyhHRKvkRhUOOD6z1WRl0UTmSrqnzGraEV737vDQrMzMyq06r3mlV6xnho19F4aH2pHgI0uJcFB7efPz7nxOKtlhoc1BiAb88+sDksRJd7/+4+32hSkFQ/2MfC+wwlryo9NXtgSbUnMBYZqQIW0PfoArFTY6La9mrhPpAZ5HLmj3H6xRGhSyLJGBoW+jDltI6pic6G7oBODfUOmUei+CeQ2JKYGfjMcXmndU1qmR2Wj8ytS7yK/J2SxWRGAgmUiGI0gv5nnoe+aHa20yOL54k9XZLSMin5sghcXzTxtLcNz5sfBUIF3sQzhwYUGR9UfNYx3OkeL0HOXQlcICC6cUFan/zIc+8GXHhxLd6NabTiZiTXdYRuAoJeIt1qNUeK0xvLNm08fHiT3eGXbS7NVjtPTw/nYUx6Ca8LaM9eZBfgNowfn47LvoAuliQP2vLAjuatyjlcC/hFfu9Surfcb9LUM/ZsyMFM=,iv:EgSXZvyWmcBxBkAe6asJ2B12FKaLQPy4tRAtCvkys3M=,tag:B0lyX7IRNHX1CqlvBZaSpg==,type:str] cyberghost_cert: ENC[AES256_GCM,data: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,iv:YokQRmSP1F43BhS51wOry5iiQa3f78Rvs50STxeyphY=,tag:k+ZxxVSmurIr2PiJJIoFjQ==,type:str]
openai_api_key: ENC[AES256_GCM,data:lH5Jf/xvtRRwuGYPM3g39J5DTQJowSKVqLtObgXRo+GOfpY/BKb46/R25rkWrOUv5pYK8PXmSm7obEkVBfoaYG0WufR6SzGH+R7hjrNgCzBA6g5pxmlE7CWPlPy6C6XqVgblL8aP2vD9qxJmIw8FYCLRgFHt5hK4d+Gd23BeBnkmPWIH23xyRFhCcb891CoiEEdXL5p1LNAOAW5ePmAyavc/Dlk=,iv:lWXy8/LlWeRVSect87/qsG2nOkmL5W7jpw/DVSnAbEs=,tag:n3pWUJ5uEMXtgt/kHG8J8w==,type:str] cyberghost_key: ENC[AES256_GCM,data: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,iv:LPskqYo7V7SMKD8HdEXDNwxyiLFLaLr5AdrU5x53R4Q=,tag:zhEYACY4xhl0ba+dBUBT1A==,type:str]
mcp-brave-env: ENC[AES256_GCM,data:FND006AIjGjSkJqx12RVd8NnyDIhqdUBgZxucaAOFlnt7ZRYAHj4xQqgSJuFxg==,iv:Fn/663cGELKYQWg+Ok1uCtNCeIP7zZBJgxhhcsR/Q8M=,tag:v8K6JQ15CYwcLj7vSjyN6g==,type:str] openai_api_key: ENC[AES256_GCM,data:Z5AiYIRQoFSmjErwgJ7mxkxmp5BVOgVy72xwag+PJeRiQAddgNMeeqOh1ka0nP/HLPnbbWgSBgzdXxD+Kx4m6dX3W2AtzO0QE68XDyuM+PKts2pUSDYDmFSWZ7FFlQSQrpuHgnit1xjorpQqRjgVkHXgNObYGvs1wUZwhfUg4zPd9UdTU7daXcUdJ1c9t5G6FuJLMNF3gmqr6j7hMYXPcCGrx18=,iv:SMy/19Jw1CX9OYEQ3VABisZ61fYyVVbGzgeIwlqdtRs=,tag:oakwiviNCiu5jWIwuAQP6Q==,type:str]
rustdesk-cloonar-key: ENC[AES256_GCM,data:nn1PKpOvhs3E1Lqtbx+W3oWon8+j74CrB4KuB+BqjfaHAaKdj+KMstYAv+0=,iv:UGjTZ9LauAALRMvJnqKkHYqGH6rjZMTZPQs0O7/aDd0=,tag:g6IZMCsE1dqZh3QYRuitzw==,type:str] mcp-brave-env: ENC[AES256_GCM,data:JAMYqb9rOYDLM8y8HABGQIvEGZUUKWt/iRJXwLZ/UftHd5iMPLDAHDcRva1MpQ==,iv:wFWJr74H6nX2nOaCUWHdeQkjBDYTEltxE84BTQfqy7I=,tag:cAj9Cjjo9WBVot2rklRWZQ==,type:str]
rustdesk-cloonar-key: ENC[AES256_GCM,data:genBO0paccI1pcH8//X8q+88yBeBFfgEd8jKmglEHbvnf2f9DDxfWVrHlX8=,iv:FEHkJ2wPBAKZFhoRRBbLzjkv28DRDJ4npBkTGVEK6l4=,tag:hx9FPzlyDf96BuMhrEXLEQ==,type:str]
sops: sops:
age: age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYYWRBbk1wckFYbkZxQllS YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByeDBlZ00wSDkxaVc1K3Qr
T09LUEljM0VKbUw2cW5hLys1OUZZdFBBSXlnClRvc3YyRFpaQW9GOTVjR1dpdU1Q aGt2YXlHdXJ5UXNhM3R4dnRQT2ZiNGdvcmtNCmxmdDZYQWt6NktVdGNwQjlUOEZG
TWVsYmwyZ3Nxb29HZVdTSXhmV3F2MWMKLS0tIGNZM0cyWmRiMUNFUkhVUkJhUjRR My9mUHFBT3lWOFc0N0JNcWh2dm00a0kKLS0tIFhud215VVA1SWVFdVZDZHZjY3ZK
SHpCTVR6VW9pdzFTMDBtWkxxY2VIWmMKrReAwG9+6W/R1AoUr8JFw2QQ9WZ+e+Pn ektUZVQ0aTVKRUFnQWUyUUtzYUs5QkUKkLJ2e2+a3VYcVepB2QIrD4Dsv/Vw0WiV
wuTlcKayDrNHuM1ldW6BEYQAV+8Z4Nhbj1ygo+2tqOsXm2YL6uzlBA== ZXqudx8UqY4xbsYaWehEVrIBIaQ/cLqVP+0YKaS6CFvIWHhP+lu/2Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCYk9zVTZUUUl3YWJobFNu YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4R1I0RjBxUWdrelAwY1Nt
bndaSGJyTWdoOWxUZHZYVDNMTjdkQVdyaFY4ClFEUjZuaTJRdVdlcEU5UFIzbmlC Qjk2UXRqVS91MHhDY0xFaVR4THFLVXM2MGhJCmNaeHhwQ0I4bGNsMi9ySi9YMkg1
OE44MFhLOGh4ZkJmeFl1QlNCM05kYVkKLS0tIFNCWnJhMFJ3MG1hKzZVRXlpODl5 NWVrb0pKN0FaZ3QxUkJSSDd0VWQ4M2MKLS0tIHIwYnEvYVhONDhGazZ2ekJ3TnVx
R1dpWnZKNHR4RERSdm5OclI1SncvYkUKBDZKeh6xlTn3tRnZOCD6oe2uFP1NeQe9 WE44dU43QmxPTncycHVGNHNlTG83MU0KQPJWz3YF8RXwxp8SUFRuo39NN/2tSzw2
b7JuigPRPnhah0rWZ6jPjnk38Jp6z/I1Oqh5UJ94H4KNi/h3HKqMSw== bMuv5JWSI8i3TNiWmOELF/Lt34Z4p5yZZ7OA8mfx1bbTsZOJ35I0LA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPTkdSM3hoTWdlcE9McDFt YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJcUFXVi8yOTBWWVhLbjZ4
engvOU5hZUtKaVZjUEFnT0huNWpMWnBjUFZnClZxZ1JrMThrT2kyejFkUnlUVUhU VU9YazRzWW8zTmNzalArcVNtQXpLdVJ0U2kwCnY0ci9nSE5qZTZpSk93MlNpR2pp
bVhTNUovM0ZRZVRRemRsNml2RXcrNk0KLS0tIDZWYWJUTHlXWUFMcE82YlZBby9E WGVENTdWbHUrdTN0K1ZHaTZEUkdpemcKLS0tIFhseFdlL3BpZHFQbE0rNlo3c1dO
clM4RXI2V1pIRVZNeWdaM24xZlFJSjQKY1+Nw3X0FynI6BhhLE5caUpDENqa6S6d SmtzUnk3V0pCakpXcmhvZWVTWURKK3MKuB/3RDvEZh1o7WHmJHFh5njBdjICO34G
HMRhiL7SIZQrmkdIeCSikjRCkvqBFIgn7sff3S+7neYxgGkFp1nzGg== o8KdfkLcAwTnSbOy0mEVLhQjLayhRq/ifMWwJfN4/MUZo1cSBwO+iQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-06-05T16:28:03Z" lastmodified: "2025-12-07T23:35:10Z"
mac: ENC[AES256_GCM,data:NNYwveO78Q4cWOPPt3Pyqh6AtbfRj/ax6D4t2KlVXWSLzKTUZKKaULXGY5PBp/jI2pyhPp5yEMhEyjRPWC8Xhvxjv+NLb6KltgaMfzIBS/jfSNk3dcYx6i8Y2oSG1efLJrRMc2Q/uACeztyivtjV9A7JCrEtb84Wb9HzkI4nZVs=,iv:Q8cTw+/RMJ3WHrkB9lyaAyI2K3O1ZhDnAMUYMJ4JMRk=,tag:JvrLiaKKYXiOmud4oZZZ1w==,type:str] mac: ENC[AES256_GCM,data:5tRkzTUeiKf3WQBFJZBBCa3vnPGVLspbCF/bf+rQEGuQ0ehA5uZXLlc3nBlC3fBqQh3JQUyVw1xcqYJc8Ziu1a5pcG28Mdls6IWr8JOzh+h2fAgqWbn9UGqEU9fNJE0aSzHSptpKA5fqgJYqmb3voz8tFn3jMNrPsGenHFn20C0=,iv:s6lOchFabWnyW2gkoYaXBhOkJyhhXyh2ZbnFTwj83bs=,tag:HosULSsgllcEzoBifQLYSA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.10.2 version: 3.11.0

View File

@@ -1,6 +1,6 @@
{ config, pkgs, ... }: { config, pkgs, ... }:
let let
home-manager = builtins.fetchTarball "https://github.com/nix-community/home-manager/archive/release-24.11.tar.gz"; home-manager = builtins.fetchTarball "https://github.com/nix-community/home-manager/archive/release-25.11.tar.gz";
in in

View File

@@ -7,6 +7,8 @@ let
config = { allowUnfree = true; }; config = { allowUnfree = true; };
}; };
clicknload-proxy = pkgs.callPackage ../utils/pkgs/clicknload-proxy {};
thunderbirdSettings = { thunderbirdSettings = {
"extensions.activeThemeID" = "thunderbird-compact-dark@mozilla.org"; "extensions.activeThemeID" = "thunderbird-compact-dark@mozilla.org";
"browser.theme.content-theme" = 0; "browser.theme.content-theme" = 0;
@@ -135,11 +137,11 @@ let
{ name = "q"; value = "{searchTerms}"; } { name = "q"; value = "{searchTerms}"; }
]; ];
}]; }];
iconUpdateURL = "https://perplexity.ai/favicon.ico"; icon = "https://perplexity.ai/favicon.ico";
definedAliases = [ "@perplexity" ]; definedAliases = [ "@perplexity" ];
}; };
"Google".metaData.hidden = true; "google".metaData.hidden = true;
"Bing".metaData.hidden = true; "bing".metaData.hidden = true;
}; };
}; };
@@ -160,6 +162,9 @@ in
sops.secrets.openai_api_key = { sops.secrets.openai_api_key = {
owner = "dominik"; owner = "dominik";
}; };
sops.secrets.clicknload-proxy-config = {
owner = "dominik";
};
programs.fuse.userAllowOther = true; programs.fuse.userAllowOther = true;
programs.zsh = { programs.zsh = {
@@ -172,7 +177,7 @@ in
home-manager.users.dominik = { lib, pkgs, ... }: { home-manager.users.dominik = { lib, pkgs, ... }: {
# imports = [ "${impermanence}/home-manager.nix" ]; # imports = [ "${impermanence}/home-manager.nix" ];
/* The home.stateVersion option does not have a default and must be set */ /* The home.stateVersion option does not have a default and must be set */
home.stateVersion = "24.05"; home.stateVersion = "25.05";
home.enableNixpkgsReleaseCheck = false; home.enableNixpkgsReleaseCheck = false;
home.sessionVariables = { home.sessionVariables = {
MOZ_ENABLE_WAYLAND = "1"; MOZ_ENABLE_WAYLAND = "1";
@@ -228,6 +233,21 @@ in
Restart = "always"; Restart = "always";
}; };
}; };
clicknload-proxy = {
Unit = {
Description = "Click'n'Load proxy for pyLoad";
After = [ "graphical-session-pre.target" ];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${clicknload-proxy}/bin/clicknload-proxy --config ${config.sops.secrets.clicknload-proxy-config.path}";
Restart = "always";
RestartSec = "10s";
};
};
}; };
programs.chromium = { programs.chromium = {
@@ -286,26 +306,23 @@ in
programs.git = { programs.git = {
enable = true; enable = true;
lfs.enable = true; lfs.enable = true;
package = pkgs.gitAndTools.gitFull; package = pkgs.gitFull;
userName = "Dominik Polakovics";
userEmail = "dominik.polakovics@cloonar.com";
# signing = { # signing = {
# key = "dominik.polakovics@cloonar.com"; # key = "dominik.polakovics@cloonar.com";
# signByDefault = false; # signByDefault = false;
# }; # };
iniContent = { settings = {
user.name = "Dominik Polakovics";
user.email = "dominik.polakovics@cloonar.com";
# Branch with most recent change comes first # Branch with most recent change comes first
branch.sort = "-committerdate"; branch.sort = "-committerdate";
# Remember and auto-resolve merge conflicts # Remember and auto-resolve merge conflicts
# https://git-scm.com/book/en/v2/Git-Tools-Rerere # https://git-scm.com/book/en/v2/Git-Tools-Rerere
rerere.enabled = true; rerere.enabled = true;
}; "url \"gitea@git.cloonar.com:\"" = {
extraConfig = {
"url.gitea@git.cloonar.com:" = {
insteadOf = "https://git.cloonar.com/"; insteadOf = "https://git.cloonar.com/";
}; };
}; };
}; };
programs.thunderbird = { programs.thunderbird = {
@@ -510,7 +527,7 @@ in
settings = firefoxSettings; settings = firefoxSettings;
# userChrome = firefoxUserChrome; # userChrome = firefoxUserChrome;
search = firefoxSearchSettings; search = firefoxSearchSettings;
extensions = firefoxExtensions; extensions.packages = firefoxExtensions;
}; };
social = { social = {
id = 1; id = 1;
@@ -545,7 +562,7 @@ in
id = 3; id = 3;
}; };
}; };
extensions = firefoxExtensions; extensions.packages = firefoxExtensions;
}; };
}; };
}; };

View File

@@ -1 +1 @@
https://channels.nixos.org/nixos-25.05 https://channels.nixos.org/nixos-25.11

View File

@@ -63,7 +63,7 @@
time.timeZone = "Europe/Vienna"; time.timeZone = "Europe/Vienna";
services.logind.extraConfig = "RuntimeDirectorySize=2G"; services.logind.settings.Login.RuntimeDirectorySize = "2G";
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets.yaml; sops.defaultSopsFile = ./secrets.yaml;

View File

@@ -15,7 +15,7 @@
datasourceUid = "vm-datasource-uid"; datasourceUid = "vm-datasource-uid";
model = { model = {
editorMode = "code"; editorMode = "code";
expr = "node_systemd_unit_state{state=\"active\", name=\"dovecot2.service\"} OR on() vector(0)"; expr = "node_systemd_unit_state{state=\"active\", name=\"dovecot.service\"} OR on() vector(0)";
hide = false; hide = false;
intervalMs = 1000; intervalMs = 1000;
legendFormat = "__auto"; legendFormat = "__auto";

View File

@@ -0,0 +1,17 @@
{ lib, pkgs, config, ... }:
let
smartAlertRules = (import ./smart_alerts.nix { inherit lib pkgs config; }).grafanaAlertRuleDefinitions;
raidAlertRules = (import ./raid_alerts.nix { inherit lib pkgs config; }).grafanaAlertRuleDefinitions;
allStorageRules = smartAlertRules ++ raidAlertRules;
in
{
services.grafana.provision.alerting.rules.settings.groups = [
{
name = "Storage Alerts";
folder = "Storage Alerts";
interval = "5m"; # Check every 5 minutes (metrics collected every 20 min)
rules = allStorageRules;
}
];
}

View File

@@ -0,0 +1,102 @@
{ lib, pkgs, config, ... }:
{
grafanaAlertRuleDefinitions = [
# RAID array degraded - critical
{
uid = "raid-array-degraded-uid";
title = "RaidArrayDegraded";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''mdadm_array_state == 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C == 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "RAID array {{ $labels.array }} is degraded";
description = ''
RAID array {{ $labels.array }} on {{ $labels.instance }} is in state "{{ $labels.state }}".
The array is not in a healthy state. Check for failed disks immediately!
'';
};
labels = {
severity = "critical";
category = "storage";
};
}
# RAID missing devices - critical
{
uid = "raid-missing-devices-uid";
title = "RaidMissingDevices";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''mdadm_array_devices_active < mdadm_array_devices_total'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "RAID array {{ $labels.array }} has missing devices";
description = ''
RAID array {{ $labels.array }} on {{ $labels.instance }} has fewer active devices than expected.
A disk may have failed or been removed. Check array status immediately!
'';
};
labels = {
severity = "critical";
category = "storage";
};
}
];
}

View File

@@ -0,0 +1,298 @@
{ lib, pkgs, config, ... }:
{
grafanaAlertRuleDefinitions = [
# S.M.A.R.T. overall health failed - critical
{
uid = "smart-health-failed-uid";
title = "DiskSmartHealthFailed";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''smart_health_passed == 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C == 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "S.M.A.R.T. health check FAILED on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} has failed its S.M.A.R.T. health check.
This indicates imminent disk failure. Replace the disk immediately!
'';
};
labels = {
severity = "critical";
category = "storage";
};
}
# Reallocated sectors - warning (any count > 0 is concerning)
{
uid = "smart-reallocated-sectors-uid";
title = "DiskReallocatedSectors";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''smart_reallocated_sector_ct > 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "Reallocated sectors detected on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} has reallocated sectors.
This indicates disk surface damage. Monitor closely and plan replacement.
'';
};
labels = {
severity = "warning";
category = "storage";
};
}
# Current pending sectors
{
uid = "smart-pending-sectors-uid";
title = "DiskPendingSectors";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''smart_current_pending_sector > 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "Pending sectors detected on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} has pending sectors.
These sectors could not be read and may be reallocated. Monitor for increase.
'';
};
labels = {
severity = "warning";
category = "storage";
};
}
# Offline uncorrectable errors
{
uid = "smart-offline-uncorrectable-uid";
title = "DiskOfflineUncorrectable";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 300; to = 0; };
model = {
expr = ''smart_offline_uncorrectable > 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "Offline uncorrectable errors on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} has offline uncorrectable errors.
This indicates data integrity issues. Consider replacement.
'';
};
labels = {
severity = "warning";
category = "storage";
};
}
# High temperature (Seagate enterprise: warning at 50C)
{
uid = "smart-high-temperature-uid";
title = "DiskHighTemperature";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 600; to = 0; };
model = {
expr = ''smart_temperature_celsius > 50'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "10m";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "High temperature on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} temperature exceeds 50°C.
Check cooling and ventilation.
'';
};
labels = {
severity = "warning";
category = "storage";
};
}
# UDMA CRC errors (cable/connection issues)
{
uid = "smart-udma-crc-errors-uid";
title = "DiskUDMACRCErrors";
condition = "D";
data = [
{
refId = "A";
datasourceUid = "vm-datasource-uid";
relativeTimeRange = { from = 86400; to = 0; };
model = {
expr = ''increase(smart_udma_crc_error_count[24h]) > 0'';
instant = false;
};
}
{
refId = "C";
datasourceUid = "__expr__";
model = {
type = "reduce";
expression = "A";
reducer = "last";
};
}
{
refId = "D";
datasourceUid = "__expr__";
model = {
type = "math";
expression = "$C > 0";
};
}
];
for = "0s";
noDataState = "NoData";
execErrState = "Error";
annotations = {
summary = "UDMA CRC errors on {{ $labels.device }}";
description = ''
Disk {{ $labels.device }} ({{ $labels.serial }}) on {{ $labels.instance }} has new CRC errors.
This typically indicates SATA cable or connection issues. Check cables.
'';
};
labels = {
severity = "warning";
category = "storage";
};
}
];
}

View File

@@ -0,0 +1,17 @@
{ lib, pkgs, ... }:
let
smartDashboard = import ./smart-dashboard.nix { inherit lib pkgs; };
dashboardDir = pkgs.linkFarm "grafana-dashboards" [
{ name = "smart.json"; path = smartDashboard; }
];
in
{
services.grafana.provision.dashboards.settings = {
apiVersion = 1;
providers = [{
name = "nix-dashboards";
type = "file";
options.path = dashboardDir;
}];
};
}

View File

@@ -0,0 +1,463 @@
{ lib, pkgs }:
let
datasourceUid = "vm-datasource-uid";
# Helper to create a panel with common defaults
mkPanel = { id, title, type, gridPos, targets, options ? { }, fieldConfig ? { }, ... }@args:
{
inherit id title type gridPos targets;
datasource = { uid = datasourceUid; type = "prometheus"; };
options = options;
fieldConfig = {
defaults = fieldConfig.defaults or { };
overrides = fieldConfig.overrides or [ ];
};
} // (builtins.removeAttrs args [ "id" "title" "type" "gridPos" "targets" "options" "fieldConfig" ]);
# Dashboard definition
dashboard = {
uid = "smart-disk-health";
title = "S.M.A.R.T Disk Health";
description = "S.M.A.R.T metrics and RAID array status";
tags = [ "disk" "smart" "storage" "nas" ];
timezone = "browser";
editable = false;
refresh = "5m";
schemaVersion = 39;
version = 1;
# Variables
templating.list = [
{
name = "host";
label = "Host";
type = "query";
datasource = { uid = datasourceUid; type = "prometheus"; };
query = "label_values(smart_health_passed, instance)";
regex = "";
sort = 1;
refresh = 1;
includeAll = true;
multi = false;
current = { selected = true; text = "All"; value = "$__all"; };
}
];
# Panels
panels = [
# === OVERVIEW ROW ===
{
id = 1;
type = "row";
title = "Overview";
collapsed = false;
gridPos = { x = 0; y = 0; w = 24; h = 1; };
panels = [ ];
}
# Alert Status - Shows firing disk alerts
{
id = 5;
title = "Alert Status";
type = "alertlist";
gridPos = { x = 0; y = 1; w = 6; h = 5; };
options = {
alertInstanceLabelFilter = "";
alertName = "Disk";
dashboardAlerts = false;
groupBy = [ ];
groupMode = "default";
maxItems = 20;
sortOrder = 1;
stateFilter = {
"error" = true;
firing = true;
noData = false;
normal = false;
pending = false;
};
viewMode = "list";
};
}
# Health Status - Stat panel
(mkPanel {
id = 2;
title = "Disk Health Status";
type = "stat";
gridPos = { x = 6; y = 1; w = 6; h = 5; };
targets = [{
expr = ''smart_health_passed{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "auto";
colorMode = "background";
graphMode = "none";
};
fieldConfig = {
defaults = {
mappings = [
{ type = "value"; options."1" = { text = "PASSED"; color = "green"; index = 0; }; }
{ type = "value"; options."0" = { text = "FAILED"; color = "red"; index = 1; }; }
];
thresholds = {
mode = "absolute";
steps = [
{ color = "red"; value = null; }
{ color = "green"; value = 1; }
];
};
};
};
})
# Temperature Gauge
(mkPanel {
id = 3;
title = "Disk Temperatures";
type = "gauge";
gridPos = { x = 12; y = 1; w = 6; h = 8; };
targets = [{
expr = ''smart_temperature_celsius{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "auto";
showThresholdLabels = false;
showThresholdMarkers = true;
};
fieldConfig = {
defaults = {
unit = "celsius";
min = 0;
max = 70;
thresholds = {
mode = "absolute";
steps = [
{ color = "green"; value = null; }
{ color = "yellow"; value = 45; }
{ color = "red"; value = 55; }
];
};
};
};
})
# RAID Status - Stat panel
(mkPanel {
id = 4;
title = "RAID Array Status";
type = "stat";
gridPos = { x = 18; y = 1; w = 6; h = 8; };
targets = [{
expr = ''mdadm_array_state{instance=~"$host"}'';
legendFormat = "{{array}}";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "auto";
colorMode = "background";
graphMode = "none";
};
fieldConfig = {
defaults = {
mappings = [
{ type = "value"; options."1" = { text = "Healthy"; color = "green"; index = 0; }; }
{ type = "value"; options."0" = { text = "Degraded"; color = "red"; index = 1; }; }
];
thresholds = {
mode = "absolute";
steps = [
{ color = "red"; value = null; }
{ color = "green"; value = 1; }
];
};
};
};
})
# Sector Health Table - Promoted to overview for visibility
(mkPanel {
id = 13;
title = "Sector Health";
type = "table";
gridPos = { x = 0; y = 6; w = 12; h = 4; };
targets = [
{
expr = ''smart_reallocated_sector_ct{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
format = "table";
instant = true;
}
{
expr = ''smart_current_pending_sector{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "B";
format = "table";
instant = true;
}
{
expr = ''smart_offline_uncorrectable{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "C";
format = "table";
instant = true;
}
];
options = {
showHeader = true;
cellHeight = "sm";
};
transformations = [
{ id = "merge"; options = { }; }
{
id = "organize";
options = {
excludeByName = { Time = true; __name__ = true; instance = true; job = true; serial = true; };
renameByName = {
device = "Device";
"Value #A" = "Reallocated Sectors";
"Value #B" = "Pending Sectors";
"Value #C" = "Offline Uncorrectable";
};
};
}
];
fieldConfig = {
defaults = {
thresholds = {
mode = "absolute";
steps = [
{ color = "green"; value = null; }
{ color = "yellow"; value = 1; }
{ color = "red"; value = 10; }
];
};
custom = { displayMode = "color-background-solid"; };
};
};
})
# === DETAILED METRICS ROW ===
{
id = 10;
type = "row";
title = "Detailed Metrics";
collapsed = false;
gridPos = { x = 0; y = 10; w = 24; h = 1; };
panels = [ ];
}
# Temperature Time Series
(mkPanel {
id = 11;
title = "Temperature Over Time";
type = "timeseries";
gridPos = { x = 0; y = 11; w = 12; h = 8; };
targets = [{
expr = ''smart_temperature_celsius{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
legend = { displayMode = "list"; placement = "bottom"; showLegend = true; };
tooltip = { mode = "multi"; sort = "desc"; };
};
fieldConfig = {
defaults = {
unit = "celsius";
custom = {
drawStyle = "line";
lineInterpolation = "smooth";
fillOpacity = 10;
pointSize = 5;
showPoints = "auto";
};
thresholds = {
mode = "absolute";
steps = [
{ color = "green"; value = null; }
{ color = "yellow"; value = 45; }
{ color = "red"; value = 55; }
];
};
};
};
})
# Power On Hours
(mkPanel {
id = 12;
title = "Power On Hours";
type = "stat";
gridPos = { x = 12; y = 11; w = 12; h = 8; };
targets = [{
expr = ''smart_power_on_hours{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "value_and_name";
colorMode = "none";
graphMode = "none";
};
fieldConfig = {
defaults = {
unit = "h";
};
};
})
# === RAID DETAILS ROW ===
{
id = 20;
type = "row";
title = "RAID Details";
collapsed = false;
gridPos = { x = 0; y = 19; w = 24; h = 1; };
panels = [ ];
}
# RAID Devices
(mkPanel {
id = 21;
title = "RAID Array Devices";
type = "stat";
gridPos = { x = 0; y = 20; w = 12; h = 4; };
targets = [
{
expr = ''mdadm_array_devices_active{instance=~"$host"}'';
legendFormat = "{{array}} Active";
refId = "A";
}
{
expr = ''mdadm_array_devices_total{instance=~"$host"}'';
legendFormat = "{{array}} Total";
refId = "B";
}
];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "value_and_name";
colorMode = "value";
graphMode = "none";
};
fieldConfig = {
defaults = {
unit = "short";
};
};
})
# UDMA CRC Errors
(mkPanel {
id = 22;
title = "UDMA CRC Errors";
type = "timeseries";
gridPos = { x = 12; y = 20; w = 12; h = 4; };
targets = [{
expr = ''smart_udma_crc_error_count{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
legend = { displayMode = "list"; placement = "bottom"; showLegend = true; };
tooltip = { mode = "multi"; sort = "desc"; };
};
fieldConfig = {
defaults = {
unit = "short";
custom = {
drawStyle = "line";
lineInterpolation = "stepAfter";
fillOpacity = 0;
pointSize = 5;
showPoints = "auto";
};
};
};
})
# Last Update Timestamp
(mkPanel {
id = 30;
title = "Last Metrics Update";
type = "stat";
gridPos = { x = 0; y = 24; w = 6; h = 5; };
targets = [{
expr = ''time() - disk_metrics_last_update{instance=~"$host"}'';
legendFormat = "Age";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "value";
colorMode = "value";
graphMode = "none";
};
fieldConfig = {
defaults = {
unit = "s";
thresholds = {
mode = "absolute";
steps = [
{ color = "green"; value = null; }
{ color = "yellow"; value = 1800; }
{ color = "red"; value = 3600; }
];
};
};
};
})
# Device Activity Status
(mkPanel {
id = 31;
title = "Device Activity";
type = "stat";
gridPos = { x = 6; y = 24; w = 18; h = 5; };
targets = [{
expr = ''smart_device_active{instance=~"$host"}'';
legendFormat = "{{device}}";
refId = "A";
}];
options = {
reduceOptions = { values = false; calcs = [ "lastNotNull" ]; fields = ""; };
orientation = "horizontal";
textMode = "auto";
colorMode = "background";
graphMode = "none";
};
fieldConfig = {
defaults = {
mappings = [
{ type = "value"; options."1" = { text = "Active"; color = "green"; index = 0; }; }
{ type = "value"; options."0" = { text = "Standby"; color = "blue"; index = 1; }; }
];
thresholds = {
mode = "absolute";
steps = [
{ color = "blue"; value = null; }
{ color = "green"; value = 1; }
];
};
};
};
})
];
};
in
pkgs.writeText "smart-dashboard.json" (builtins.toJSON dashboard)

View File

@@ -31,10 +31,13 @@ in
./alerting/system/default.nix ./alerting/system/default.nix
./alerting/service/default.nix ./alerting/service/default.nix
./alerting/websites/default.nix ./alerting/websites/default.nix
./alerting/storage/default.nix
./datasources/victoriametrics.nix ./datasources/victoriametrics.nix
./datasources/loki.nix ./datasources/loki.nix
./dashboards/default.nix
./alert-cleanup.nix ./alert-cleanup.nix
]; ];
@@ -102,11 +105,12 @@ in
contactPoints = { contactPoints = {
settings = { settings = {
apiVersion = 1; apiVersion = 1;
contactPoints = [{ contactPoints = [
{
orgId = 1; orgId = 1;
name = "cp_dominik"; name = "cp_dominik_emergency";
receivers = [{ receivers = [{
uid = "dominik_pushover_cp_receiver"; uid = "dominik_pushover_emergency";
type = "pushover"; type = "pushover";
settings = { settings = {
apiToken = "\${PUSHOVER_API_TOKEN}"; apiToken = "\${PUSHOVER_API_TOKEN}";
@@ -117,12 +121,28 @@ in
expire = "2m"; expire = "2m";
sound = "siren"; sound = "siren";
okSound = "magic"; okSound = "magic";
message = '' message = ''{{ template "default.message" . }}'';
{{ template "default.message" . }}
'';
}; };
}]; }];
}
{
orgId = 1;
name = "cp_dominik_normal";
receivers = [{
uid = "dominik_pushover_normal";
type = "pushover";
settings = {
apiToken = "\${PUSHOVER_API_TOKEN}";
userKey = "\${PUSHOVER_USER_KEY}";
device = "iphone";
priority = 1;
sound = "siren";
okSound = "magic";
message = ''{{ template "default.message" . }}'';
};
}]; }];
}
];
}; };
}; };
@@ -130,7 +150,15 @@ in
settings = { settings = {
apiVersion = 1; apiVersion = 1;
policies = [{ policies = [{
receiver = "cp_dominik"; receiver = "cp_dominik_normal";
repeat_interval = "999d";
routes = [
{
receiver = "cp_dominik_emergency";
matchers = [ "alertname = HostDown" ];
repeat_interval = "999d";
}
];
}]; }];
}; };
}; };

View File

@@ -17,7 +17,7 @@ buildGoModule rec {
subPackages = [ "." ]; subPackages = [ "." ];
# Optional tuning # Optional tuning
CGO_ENABLED = 0; env.CGO_ENABLED = "0";
ldflags = [ "-s" "-w" ]; ldflags = [ "-s" "-w" ];
doCheck = false; doCheck = false;

View File

@@ -19,9 +19,9 @@ fi
HOSTNAME="$1" HOSTNAME="$1"
# Check if 'nixos-rebuild' command is available # Check if 'nix-instantiate' command is available
if ! command -v nixos-rebuild > /dev/null; then if ! command -v nix-instantiate > /dev/null; then
echo "ERROR: 'nixos-rebuild' command not found. Please ensure it is installed and in your PATH." >&2 echo "ERROR: 'nix-instantiate' command not found. Please ensure Nix is installed and in your PATH." >&2
exit 1 exit 1
fi fi
@@ -38,27 +38,42 @@ if [ ! -f "$CONFIG_PATH" ]; then
exit 1 exit 1
fi fi
# Check for host-specific channel file
CHANNEL_PATH="$SCRIPT_DIR/../hosts/$HOSTNAME/channel"
CHANNEL_OPT=""
if [ -f "$CHANNEL_PATH" ]; then
CHANNEL_URL=$(cat "$CHANNEL_PATH")
# Append /nixexprs.tar.xz to get the actual tarball URL
TARBALL_URL="${CHANNEL_URL}/nixexprs.tar.xz"
echo "INFO: Using channel '$TARBALL_URL' from '$CHANNEL_PATH'."
CHANNEL_OPT="-I nixpkgs=$TARBALL_URL"
else
echo "WARNING: No channel file found at '$CHANNEL_PATH'. Using system default." >&2
fi
echo "INFO: Attempting dry-build for host '$HOSTNAME' using configuration '$CONFIG_PATH'..." echo "INFO: Attempting dry-build for host '$HOSTNAME' using configuration '$CONFIG_PATH'..."
if [ "$VERBOSE" = true ]; then if [ "$VERBOSE" = true ]; then
echo "INFO: Verbose mode enabled, --show-trace will be used." echo "INFO: Verbose mode enabled, --show-trace will be used."
fi fi
# Execute nixos-rebuild dry-build # Execute nix-instantiate to evaluate the configuration
# Store the output and error streams, and the exit code # nix-instantiate fetches fresh tarballs and catches all evaluation errors
NIX_OUTPUT_ERR=$(nixos-rebuild dry-build $SHOW_TRACE_OPT -I nixos-config="$CONFIG_PATH" --show-trace 2>&1) # unlike nixos-rebuild which may use cached results
NIX_OUTPUT_ERR=$(nix-instantiate $SHOW_TRACE_OPT $CHANNEL_OPT -I nixos-config="$CONFIG_PATH" '<nixpkgs/nixos>' -A system 2>&1)
NIX_EXIT_STATUS=$? NIX_EXIT_STATUS=$?
# Check the exit status # Check the exit status
if [ "$NIX_EXIT_STATUS" -eq 0 ]; then if [ "$NIX_EXIT_STATUS" -eq 0 ]; then
echo "INFO: Dry-build for host '$HOSTNAME' completed successfully." echo "INFO: Dry-build for host '$HOSTNAME' completed successfully."
if [ "$VERBOSE" = true ]; then if [ "$VERBOSE" = true ]; then
echo "Output from nixos-rebuild:" echo "Output from nix-instantiate:"
echo "$NIX_OUTPUT_ERR" echo "$NIX_OUTPUT_ERR"
fi fi
exit 0 exit 0
else else
echo "ERROR: Dry-build for host '$HOSTNAME' failed. 'nixos-rebuild' exited with status $NIX_EXIT_STATUS." >&2 echo "ERROR: Dry-build for host '$HOSTNAME' failed. 'nix-instantiate' exited with status $NIX_EXIT_STATUS." >&2
echo "Output from nixos-rebuild:" >&2 echo "Output from nix-instantiate:" >&2
echo "$NIX_OUTPUT_ERR" >&2 echo "$NIX_OUTPUT_ERR" >&2
exit "$NIX_EXIT_STATUS" exit "$NIX_EXIT_STATUS"
fi fi

View File

@@ -1,88 +1,97 @@
promtail-password: ENC[AES256_GCM,data:DykxIRTXttQgJ6vv3oBOhX1h2PrPimLz+dEHZwjFvg34UEGWfQu5nODw7h6qAJrKIGR5217LgTGZzg1HedbM4Dsb2OJW9c39bXIga730eVvGCm6RcMbpv8GDHPuVCfO1NwQox9Fba8veDWDNqNisHQuYDRQrNZrg1QEiKsujZdY=,iv:kM5Ec376USXMoXCVF/4g7F1NbJNbWfTMVd7LKsTnTuE=,tag:y8aEF+Q6/Cm16W2LYF+orA==,type:str] promtail-password: ENC[AES256_GCM,data:jooCw16EEw9JC+W19bXvoOjnCo/KP0H1Bpc0UqfGN+mCqFLK98TDU80hNu54pYQowcAtgjB5ZM64gWt+stqFKWVWihF0d4A3KuTTfpxmXGdGi6ThRcAXhMmXLH5SYR4N96d4WsvHNsFTRGItnUlp2juQMKHnZ2At9RQWgBQqK6Q=,iv:HFttRHz2fIU9qZzP5r24/AKMHTWwDhhIrgQpxw6Ol/Q=,tag:umq2lzodL2nijayGms7ciw==,type:str]
sops: sops:
age: age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxQTkrMDlpM3RnZ0pNZVlM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFT21QaTZ5ditZekVmaitY
dkNya243OFlycmRRS1o3c3Z1Vm1UNWxBbkVBCmw4dDUrQkg0NExaTHJaSk1JYnpY b2locXR6M1NNSXhIL0pjK3Y4dzlYcjRXMlFNCnVtelpDdWovc0FiUGQwWm5TRlk1
UDNHa09Rd081N1FVbXgyRHVWbUtna1EKLS0tICszQ2Z4aWpNV1U5RVNibllGdGlY b1pka2paQ1NBd2c3WG12U0N3N24vbHMKLS0tIHgrMHdxS3J1WWZNdGdiaVRaWjBm
alFRNFZVNDlOUTJRbVQ0T3dRTTlJZUEKx+ftKJc+RMmxXoRxLd6gsvN6Jfnn5Xre cmh0VEx3UDRocFVlckFUN21YblpEb0kKKF7CPzXn6e9o1+BctLSHcLZTWYdYiXQs
48TolLwPoBSr6uSmfWfcXIL+2uzo5cTGhMReCEQrlHOWGxhk+XDmfw== dwX8ohGJc/Q5Ewrrdmm77gu3ttg7Ml/70ToG/yTBExH1lwGb1z7Qag==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVSjAvWC9haUh3blQ2bHJj YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R2hwWmtZVjlTSFZTMUd5
cnh5YkkvUWhEYXFFV3RNbURwVlROZk1yUmc0CjdMdXExWE52WWNyRGdyVFRzT1o1 cXpqbXkwTmQrYS9TT1NYaXZ4azFjRTlOZ0Y0ClBIUGk5R0JGenBrSXhNbExLaWkr
Umo4OWhMYTZjTkJvbW9UaHJVaE1YNG8KLS0tIHBSOEdmQjFCZ25jNGlHMmZoalpW eUJLUVBYYzBFbHg3Z2l4N2JINTBSVjAKLS0tIHFTa3JKUTVPNFM2TktUbm9mSkVo
c0FZUzBVYXRTMHFZSGYxVDdzS2d5a0kK1a/FQ841bIKuXHjVAjV2YPTpkmI0R7fX Vno4TER6SFR4bDM3L3FYSkw1UHJoeXcK0mR/ysz38ZhEAqhEZZXmuH3rykMUeFk4
ohkPSQneoOnwZPXby69PJLSYwX0IcQCckkGXa1z6KLr6iueSpyM6JA== tPvIV3LpRXpU+yiT3zpLJXVi3GDy9vaq/h/uG7rDhE/nPoaIIVBBhg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtYUNabzVBcFpIdFhUTW85 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtV2tlLzhqQWlDd2FaZ0lJ
ZlVjamVZUERSMGNMUkhlUUVVd2ozUkFLWHlzCnh0c2gweU1ud2cxS2p1eXUxNy9j dEt5Q2YvTWR6Q3pBUHl1elJEN3lJcE01YTBrCm1IWlVVd1poVk1Gazd3NnBCa21L
d2tCTVR0YjY5bktQa09tUmFvM0F3aEkKLS0tIFVWMVFUU1RMV1FoaklnS3Z0VzBJ UXByN05KNGdUTW9uckhvNUE4bFVMME0KLS0tIDVSRCtJNnRSdmFzcWVNNHEzV092
bitzcStWdzM3TXBMbGJKNGVZQTNVZ0EK0qjI7PKk9lUDG+0ZeCL/9ILI9KRIEU+z NHFYYTFRdUVXNFh5Tlc0U2twYWpuWDQKTqRXFxn/OuYrjVSlGNyHWtCwmaV+4PMr
6o4AcdGcd44QkUjYboLTwGvdf4QdKZvyfBk6xliUIzn0tbX0CrEHOA== +wpjkS+3pEWYaMtRhoBKJmPXhbE9e0SSzFV/HEYILswUfIWuQNpNUQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ylrpaytkm0k5kcecsxvyv5xd9ts4md0uap48g6wsmj9pwm4lf5esffu0gw - recipient: age1ylrpaytkm0k5kcecsxvyv5xd9ts4md0uap48g6wsmj9pwm4lf5esffu0gw
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQdmhqdXpIblh0QVRuSGdl YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4RFl5UHpOcUIrdEVoREFn
WmpaV20za2d5MFdYbTNTRTEzR3BUVFJLU2tRCkdHT1JFNmVEZGNkRGJ1S1cwU1Ru ZmloRG5nb2d3eHpScmRWVjVLUXpveW9pZzJrClRVa3h1Y2dIL0RCZFJpM3NvTndu
MEp3ck1MN0tYRXBPY2xQR3JIMURpWkEKLS0tIEF2UERsV2J6UzZYUm5sTFdPWGlo VzVMT1dwbWJEOXFKZDNVdEgwV3RHZE0KLS0tIElkTUk1SHRGcFp2d3BpUDgxZWVG
NjVGSDdndDRsQkx4V3U3N3FjNldUTTgKY8ohcy0H+fxkmBksfWzVLZsbfqDfWUzA RE5UQktjTEtzd2I1SmVZWE8xWm5SSDAKOfrr3seS8+UqGZXiJfraGh9wTqx7zFnH
5FUdmqCHdg47Mct3K8qXHSEbvegn/8Hp4vSgkVQcEA2YFcf4J5GRpw== GMBBlCj2SLAHP56efITiPJ6kFISFoc6QgBj024oUXop2HT3CQh5hJw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4 - recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnbFMrNzVMYmlHWE1yb1hj YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoMWhoa1lKWU8yRHZnSUNm
eGw2SS9qNTdKTDdSQUlTNjhnS1dZaTd4NFFzCll6SEhzVnY5UnJUbUtlUzJzZS9N cVhycko2MFczWE56UlNDUGlNdndXNnY0blhvCmlIeGRHUjRuODhCNk5lVlVqZmR1
cUZMYnV0bU5DRjU0MW8vSFpIN2pNT00KLS0tIFdTTlBPT3J0cmF6Y0lnaGRpQW4r ZEpMTUl6dnpoUkhGbTBsOUNzdDliM0UKLS0tIDRHWjllMEZyaDN1OTY0RXpzWUVZ
TjRsa2dlR3hrZkVQTFFWQm1xR1pLQUkK2Kio6ShvcsbJ2n1UG97gxt5AcdqKolMq WklqZW5DT09DclBBOUZ4VmpIMVdCRU0K5c8JtZ5dfzxmtMlnL+3637/6YBWN9qdP
3sdoF7b87Crd3QSzDKx2Rm97EjeQskOBOgpasF2W8GoRYCol05Y0bQ== +/l78vhb0KVt1SOI2d6ZnfkKEXSO/PyBpOkz+AOubxdpQMNOyQsgcA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw - recipient: age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZGZobW1aaThidmkxSEk1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N1BEMjFsZG03K3gxUVli
VXVzdEdIMENnRC9sTGpXQUNwSElPWVlLTVFNClRuQ2pYanFibEJoSllXYVhmSk90 b1hEUEY0NDRRc0xDUkErOXExa05uMkdDK1J3Cm1WUFNRNzZGMWdOOU5kb2kyOFNs
QVlGUVBjMkN4RG9BempCRDlFZHJPancKLS0tIEc3Q29tUzhzYzViMkpzS1RNczBE Y2I1aC90SEZqSER3TXU0RTlVd3VOTHMKLS0tIGFMdDM0YWpJVTFFVEFYcUl3b1Nl
djdYNVdvZHRkOHBWMGk1N3dlb3JLUFEKiruFC9YV3gloPaP9+wY0Sir2xA9NUcPN dlEwNWRmVllHSmtsRWVvb2h3ZGJaZUkKrEzfrlYGgB05NWxc3h6olIzGmdRCYDWj
matBs8oPjlB5dlrCoiHi8kl1i5ROnlu4tlNpLB0PcO9fCUMP1ypAQQ== mr5PEAWo0KGcvPK61lxwpHdThp3NGV0pqAHUU5+7Td/PbguHvaEPhA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df - recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJcFNWb2EvQWF3czQrVVg3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBandodFZnbU05SVN5cVlO
Wm1hVm16bEtYS2pnM0R4elhGMUlqRUZXcUdvCnlJN3dxU2VKUk9JTk10SjdubmVO V1BVaWtLNWdJMmRtUWxaMGdITmNKUDdXdHlRCk5HbGdSWlFnKytTVWxOcWtPZ3VJ
NlgwU3hqMEp2cmF6R0pmdU9EZllJVTAKLS0tIFNwMC9jdjh0MXJpYzU5cE5mc0Jr bi9EZ2p3VURkeEZuN2xsbEhkcWovUUEKLS0tIGtzTnJFOXFsTittUlZ1eCtjWTVX
KzJoVGlKTUNZYXhpV1NMcVVuVXI3SHcK7PIY6HznGsckYauyFGVxmU344FqkPYhm SXdWczV0ZnI2a24zeElIZUsvU0ZSeUEKNX9qLko/2aFcrwW5LaMjvg9IJlNszSKi
1x74NydHuGLAkMd3H7AchnxP9tVzSX3sOD9AqYqgg3nRS7yaIet+sw== 7nl1d1fTLGCeMUvgwZU1uBIyCm/p0HTikBaDob5L5fJAVlSQNZxiBQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2 - recipient: age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBROTV4Y1dQMkZtM1RUQ1lD YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVdDdBTEpMZEI4NFc2aU1F
QVdyUXY1TVRxWURBZlRsZWVYTEhnQ0lMYUU4ClFLcVdUZ01YcDc1OUYwMWpJRUpy aGlwcDk3ajd6NjE0UUhXbW5WT1RCSmR0M1hzCk1zVVV5V3UvUmx5REZuRk1RT3pW
NHVMM1FrK1B4TEU1QUhsbjdCL3M0dmMKLS0tIFRFSndnZ0V0a2VKV2VXY0N2Qjgz WUVocW9iOXppWFBqRkVlZE1TZzFUbmMKLS0tIDlzLzdnWEhkWWFFQm1seVJlVHBw
dFZQbm13d3JOWlZiSXZTcUpkSSsyVTgKI1GJ1uRRcTH/13lkAiUxNhBNmDgf4MFA THlweWtPcFNyT2RCNk9UQVc3Y0lnNFEK/d2fvmsIrRTHc3kBH2sAUBg0MCp4nXNT
5nk6z1/nJglnvajYyGXlAlZF7XofbUtUWZeBbtWwbeWImjIa/+KaSw== imm7SINgt6aH390yL7BWHMBKzdgNHO6hn3plLV8EW8upsETwJCbrfA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvc3ZteDRVQ0xsRUpxY24x
Wmt4MS9ZL1cybUY4cHdPbC9rQU16WGtESkU4ClhoMnhuY2NrT0xyNzl6Z2hJR3dt
VHhLa2hCeUhCcjFPZTkxeUVaazFWa1kKLS0tIEVndkhYaS9GbDJKSzFIb0xQUzQ0
V2EzOXNWNnYwaXc5dkg5b0RDdjBpa1EKfIC1OigtPBRWIgXUyb4SSjpbO2Koqaiw
TQT+hnR+VkThbcfyWPZ+Zpe4lZzfcdMGfr3m7tdv/xY8epwrThInjA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w - recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjTUhiU0RnMTAyV0xycnNL YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0WHdpbGJNcmZhK3ExRi9t
V2xSWEttejB0SWhJR0FoNTZKL2x6MEJkcUZzCnFjVUVWNGV2SW1NZEpkN24rUVpX K3ZHTld5QmxMYnZXTFZmNFVIbEZtS0pFRDM4CmJPK1BURU83UmFVdVNoam1reG0x
dUJ3Wkx5aUlsWDByOTlpaERpNEpIa0UKLS0tIDExTVVJeDFEUStzamw3RGU5cHdE NlpLbDVQY1FNR1RyWm1TWkQrUHcrNUUKLS0tIFpVVGVscFFPclVTMG5IblkvWXlr
WlNqQm1jRnpLWXBzRVRZUjc3Z0c4dncKonlHRgH7P4da+RJkGdWHRPiN76oPbH5U ek5lNTFkMVUvSU5McTFDS2tWWmZ1UmsKyhUXdaSGxKFFZnATRlTh7GzDu7eZ/mkq
DzNuS7mPsRAuajnCAGeqodzqllsGJatZUOVKFem8Of56Wm3pw3yLhg== V+9pqaob2fshwQ3tNVZtXmWTHv1geyIBxmQCFVSOaHIPVLpiC4Bhow==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-11-14T11:28:03Z" lastmodified: "2025-11-29T19:44:44Z"
mac: ENC[AES256_GCM,data:PBPNSGj6UaGoxH1Jq25bD4q/d42HrnBNhe5KFo1MoQCp/bzsphN8v6+tbHIdGh/VAoU7auRZVWXYALOl/3cGnpL52zvJGDaMlPlDVdzz6wHkl24z1ousWM7FKPwBtvGuAWAknYQW7KpQTtpobbBr8QHy/O4dB/NqxXTj/MSsbxY=,iv:1QOFK1LiKPnAuXeXNBJbeL0d73nsMq+DJCpeVruDumE=,tag:hJH3S3ZurYd0hcoWyWOocw==,type:str] mac: ENC[AES256_GCM,data:pRol7WdkK+Vr3fEc7UaEhoHlLvwwm0KdGOCReS6Rz12gD0Fw2UuNYsPnaj1XTdSLSfJITpEorFTmt455BpC6wMCszICSkqRn+EBgu4WWFZrv5v1m6BjSOTsU8bj1iAggiqsx57WS9opMThCzCOSIJD/EEzQmk5/qva/aBIJni/c=,iv:jorZE1XG0xJDGsXgw8EvuX7AL7yCuSynrqaeveCF4SE=,tag:btZHuaiPdZhdRj/+JU9dSA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.11.0

View File

@@ -1,88 +1,97 @@
victoria-agent-env: ENC[AES256_GCM,data:m+o9GgDSm2qYVk90199H6J+RqE0fZH92G7uFjP0Al1JlvclOFjHnNlFJ7y8YfgBcPUFutIU45HN4+I2X2k0+GFyKlrKxevH3wUKNIWG/l/6VlOmZr7SMRQAlVv88aT0EeBIEh+AOilCcWsD4egQPS/faP5yolsqrm/sXltsdbdI6i7EVXBKUUryBjogE6tv5nroN0ter0hXtyspkV4oBxcZIqTK4/t6staEq1OcY/augdpqz6z1aBFVun3c0q70vS5VBP1KIKd+nKVABWWwt,iv:7mnSPuP3jh06uIFQbWUI2VRFF5wbGFLoTS4rf7PUF7M=,tag:BtVruJm2+9pGNYOzQOV32A==,type:str] victoria-agent-env: ENC[AES256_GCM,data:kkbtEi4nVqv7jvL0Y3XCsil0InrVsL1zasBcOSKpA32ix73q0bRIwT8JVXX5hI6G8REYvp1oGNRlMBmSPy7H5YxxUd3RsK/5SbLkPfifTAxl8qz1STKPOmq6dZn016809AIkBiDWuu4FOHvzoSVkaO5EXluzb9wVwGH1HM/5pYBDFzqtkgWbCGbeGRXComgD4bqlFjDg/HRX2CaZf0sBByFQxH+NFvXCwdBeEZ82x5gBmXsaPnsAmh8fJotK7Eyu4+Urnahcp8V/4cTadhvg,iv:eizMQCL7vuTn3F0lY23fQfsvxiE8P3CdHqImth9X4JA=,tag:BMltfLO93YMEKv3sycDF2A==,type:str]
sops: sops:
age: age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhaFZ6dXVqbHppZ21NeWFi YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSjNMUlh2TFB4L2JBSWU3
NU1kV2FOUkVDWlc4RVdUdGNZU21jODdUUXpVCnU4aDdTbXlWNlZ2OUVaOGEwcDkr U2xyQTVoUmlQYUV5R1Y5MWtOa3VSVkkyTURzClN6QVFpZjZDdXdrU2ZUWDUyZDN0
aGNrSzVuZTVyU1BMNXArZTBSWnBacG8KLS0tIFVCMUNDeWdTL0VxLzdYNjgyMHY3 S2RIY1pjdk9vSjg2cUZyTUpQbE51a28KLS0tIDFxSUNiZTQ3UWMySnNCWVZ0MzNZ
QmNCNGdaeFRHOTY4S2Z4RE9LZVB4TG8KQAe6ensRM4QVJKnDgbnFk9ZYoLk4L7iQ NnI1RFhXN3pqdTYyQ0NFZG95UHdvWkUKD23hIRYhZ0BRamLcjfKOpcHIzeatwEFb
4V8jODObt9m2WDCqASJdt/8m/l84E7FTw4g9aimr4fBOU4zOqpGe2w== 0dvIm8RWcZEYK4w96GY5EQPkJsvcoBNWIyMFrJkgtcRfie3+W6kdag==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCYnhvamdLYjN2SG9DRVl1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5dlY4c1FPU21VbkI2bzMx
aEdGSWxQTUwrSTBtQURLS3RiQjl3by8xN1hzCjVLc2hQV1dCbTd1UkRaUC9ZN25V WS9BNDJSU0JyTEVvY1VFQkU4L3J4U0hrd0NZCmlkQWw1N3RxU1MyZVVuNVZpVStk
aDJMczFSNGhCZTZFWnpLSW9sSENoWUEKLS0tIG5wRnBvRytKTXo5TXZ3bEdlVkVX UXlyLzlUaFlWTEdDdUZBV2Q5RGpvV28KLS0tIFRHTkwxWTRuMlFJWTV1SVQwU0Ji
bDF4UUR5bE1JK0tGejJWanFkVDJCMUUKuIbnEMXPsDkJ2eDriJ6gKmwC4gj4ijfU c3JnQmlPd1JMQU9pcnVoaG9teVlBWWcKMOIDN36cLqOemzP62uRgKe/myhRs4F4g
vmuAEsdpjo2UzP2Sjvm6tFSK85LvZi9lDu5NXdVTdwcq13+OYX1TLw== MiNVYUL+d3WR3HQEvhovUU5RowlCD84U6Za8z+ss2sApJ6jdUcSP5A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1aHJlbDFBZlAvbjhRNHkr YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUMS9CbzFrZENWam5BOW5t
angxd01kWkpVckZHcXA3VzJCa0xRSEpuZWlRCjNrUVV5NnM5VWV0YkRmQnE4aU1y K3ExNGplNWZLNTVqMndnQ2ZSOFVIL2YvdHlvClkyS1RvZ3FaQWVkNjlWWExkbDZQ
SDc1eXBDM1hsZHh4UkFYREZmTTFLMW8KLS0tIEJZSmhWa1l2SFpqdXVQWHZuWkc2 NFVPWmkyNkdoTXdjY0xvS3QrMmFLeXMKLS0tIDhHV2hmMmIzY3dKeTVsUnB5ZGhR
ZFNPcDhyc3dIaWx6TlhTMlIrK1NLWmsKhMpNkZEvlaIKWauZsVoLzwYWx0k1sbmk cXpPcklDUDRkcytIcytDY1NYYXNQcTAKvjGjCCrj//KRijPCLOEDAxiexY0CnnqY
KO+pNAUz2RMX+N4ykCRgFfeV6SDMxbaOACFm1/6yyDXHgvaI7zrQTQ== 80l7bSnQPOOs27HO/9YlJerQlhPBRZo/KJ+cY3T2fAIfad/f7XN04g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ylrpaytkm0k5kcecsxvyv5xd9ts4md0uap48g6wsmj9pwm4lf5esffu0gw - recipient: age1ylrpaytkm0k5kcecsxvyv5xd9ts4md0uap48g6wsmj9pwm4lf5esffu0gw
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1WkQxb3h1aUVDVGJTMUd1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaMHUrQWZMNmxtSWtxVllM
RWdXMUQvWUk2TDYwbzd3ZW5ZSEVGZFhUOVg0CmxEd0lOV3EvN014NUFQUEdaWjNq U1VsMGxVOGt4UmtJSTE5TUdQaEdXUVRIc1FBCk1vTlU2c0lHUDh4VmYwMlFwZU44
ZVE5MGZ4SVpXNTRwRnplMHhrQlBHMm8KLS0tIFpUeWZQZmQvWlBrcG9oNERIVEZt Q1p6N1l5ZWlveGY3VFZJNDNpTFpqNEEKLS0tIHJxdlJtTDkrWHFTcmZXbnVCQ0ti
UlBxcFgxVlc2WFRxVkhjWmZCdUdjSEUKPbadYyvWy0Kfbs5EovcpL3Aukj7wiZPJ K2ovOUo4UTN0Y0dubllQM013SW05QmMKQUN03J+ju/JVs6geEMWLnnnlzjPfZOqS
VlDkELrTtbvFYp1aAASFZOQb+0NYUyOCtM/OI5qNYigR7TgJBAf/Ig== rn3ldZv1aC7ckAp2JiVoznkpsBHFOV8T0pxE9vnmDipAU263k/4ktg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4 - recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwcnZrUklWYUZBbm11S2RX YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwa1hoalhrQVdsc0NrNzN6
b0F6UjNGQVlXQm83NDBCTFoybHhhTWd6NWs4ClM2dXNXT3pPZkpOYnpRR1BwV1k1 ZU5JVTdDRjhBWFUzMXhJM2FiaU5HcmlvZzE4CnJ5NDlUYmZmbzZsdC9jYXJBc3Rx
NWlXNXpBeFJHalhxaExybTJ5NG5nMlkKLS0tIDdXa09CKzdVeVp6TFdpei9PempS c1RDZDFOdnVHYjZSc2VNc2tOTGF4aTgKLS0tIFhEK1JaRWk3VzhtUE1GYUxoOVJD
dVNPL051aXpYN09EVVErZkV6czI2dkEK2UZPimVhLwjgjXjj1m7Qc/w36xYDe7sQ ODRHRUxheVRuaEVoZ01sQkNOZGkzMUEKUBpUd30EjSkRFK2cbCARdycf9hHamoVG
D/5gub0EFuycIQj3lw0y59Jds4GoxBImExC0AKIaX9XBGW1BfBFtPQ== XjCfIf1BLGe76+c88zDKaPvp/iAWfGypQkA71tRUoe6pEtrAT8sRgQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw - recipient: age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKM29sYklYMDUxTGJRZGlw YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4a1d1UVVmNDVkaWtiOWlN
V2tVQTFaZXhoa0xSb2VNZldpUER5a0hWQVVVCktiVDRORDBPM25xSjVucTFsOUha enA5RS9WSGgyT0ZqNWNvZ0lVbGpkcWxESzNJClFMMlJJRDJDSnRyVVZmRUZIU0JU
dHdVMjMxWDcvbVVoTDR5czFrMnVXd00KLS0tIEc2V3RtUVJBbHpKWkhneFRzcCti RFlTUm1nSzBFWVFEOGtROFVWMit1R1UKLS0tIEtRRXZvZFBKa0kxaDRXazJEcW1P
NkN5SkZNWlp2dFBmclNjcW1iWXZZY2cK6fqN6xbVFLSTRPfDhvRALVt1yxizvyzI ajdFVHVpSWtjVU1ZSmlmUmxIY2NxMmcKVMc+JpeCxXs9RGjg7RN6c1TC9ndIOvWw
C32AxiKQo9XrXGBmD5Zi6dtTy+Kdm4PqZjk7M1vW0LOJCErlVI0AjQ== uFCTBRbTZJHyDC5fYrQQdhMQprm8UT3Fr51i1RWVWjsHz8GZEBwQVg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df - recipient: age1wq82xjyj80htz33x7agxddjfumr3wkwh3r24tasagepxw7ka893sau68df
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNT1dBY3kycWVINUV1Wm93 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtTm8xeUlpbC8xRndpSGQw
VEMvNTRSQzV5U2N4ODhiSERHcnVVK3c3Q0hFCmxwWXE3Q1BCRFpIMUlFemRQOTRB S2RDSE93ZG8wcnlHZFBXbjNzMXlpeWRIVjBBCnlVRDMzL2paRmFWL2toZVFBOFRk
TERiVzdOZGZDbmxXT2ZYZHdhY0FpOEEKLS0tIHRVVkYvVW9nYnJ3WkFhNEQ4UkVY bi9LRjhWM2ZBcTBFMk9ydTFhaTdrblEKLS0tIFQzUjRpSGR5NzNYcFNVNUNVUks0
US96T0JUUlJjVFcxc1ZSUjY1aVg4NmMKc4jD2CtrKc43wArBm566h117ko1vrsHG R3AreG1Cd3d1NDNIWnlkcm94L3h6NGcKurqFEsNJklMgu3G7mDNq449o2vMX/2t1
ONq9zOp4z90WPvY9octFOEn9cZ2tJvcGYDhWyBGH3rVRYN5hzTRLdg== aUOLRLzi5GTJoQzmuJbveQA+HIgv/B9aNC762YH6df4N0Yk1GRt7pw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2 - recipient: age12msc2c6drsaw0yk2hjlaw0q0lyq0emjx5e8rq7qc7ql689k593kqfmhss2
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUd241b3c1MFUvUTRRc2R3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwUHZ0Yk14c2NBQm9SelNR
dFNLREZ0Qi9kcWlVait0bVhJZ05PZ21ScEFjCitab1NaWDlEWkEwZlpzTmFFQ3RV cjY0K0xYUWREUkhWQVNRUVFUczlvZjZ4V1VnClV2b3hhMnlhTUt2U1FLTERlVThv
YTVWUmx4VEJ2Q1J1TllZZ2VTaVF0OFUKLS0tIGZxdGNaM1Q0Z1JEbkFpQm0xaW5T K2Nqd0dPVWZLTXI2Y2VhaG9Yano1d1kKLS0tIExRY3NlZ0x2VWtDZWhJc0dyM0ow
bHkyeHVtbCtzb2pKOTUvcm91MnBockkKCrwgT0auKuuRYNwajiqBVzpgG13li8KO dlp5SzFqZmNncGQzYzR0THkyeVR6WXcKUb9DJ1u3/VnYKXIgzpTdImSgE2lWdCOD
W52Fy9lIc9poyxEnvyIqpPz29gQdL4aqGHX735f2+n2fq/SIgiKHfw== yVOJXwY5IrU6NJgBYbZlbIysIS+ihO9pLodjaYhbZduNCasdrhwu7A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1x3elhtccp4u8ha5ry32juj9fkpg0qg7qqx4gduuehgwwnnhcxp8s892hek
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5cFJka2xuSDhyaXowSzVw
cXJSR1ZUWnhDdWRwbFpmenJDZURmbXhDNVhRCm1GMTZ3VHA1S0o3ekVsR2loS3Bi
SmJrOEFBUzJZVUlxazh1TVhkZ0kzVm8KLS0tIDZwajVOUWlIS1FHWHNpZy9jSXhK
RHBjNjRBRzVaUlMrL0d5ZGZTdUcvYkUK0hf09zou0OqZSV81G8HMeNRx+uz9Nade
dhyQ1CRVWT5Q5HW/4t/abB0hSxFId8+X7ufJIjjVSbywLZ7WHLKCSw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w - recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrNzB5bHVMU2MySmtaOHA4 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBndjYrRnFRd2RDeTRDQ2Uz
K0N3RnZINWpTbkxiTWJnVTZ4Z3d1L1N3T0hjCmxieHdhc1VFWXM1WHExdWNPdW16 UXk2WE1ORERTOWtOcXI3eXVHZzJJQ2RMRTMwClBPR2J6cmMzSzR1cXJWaHJJUlJa
ZVQwTzdGcFVWamMva1RpZGV6U2hpSUEKLS0tIER1T3ZJYUQ0RE5TVTk2dVdmYXhU ZVd1S3hpWEdFYXpvUXd3ZUs3SVJrZHcKLS0tIGN5MUFET0M3WWZkcHRlV1QyTEQx
eUNKWkczb3dwMnB3SS8rVjRHU1VBQkkKOdh1+qrx9WX0NGSVrGdptFNU8C1ZZFNi eTFJUnZpM2haMUZBekh3SjBQRGFsQUUKUI4laym7zGzu1iIlMYnnKjyDHUqblcn6
3lJmzNvJRyVMxXNjWqpXUfCDWQONvCKEGuAvt7Zra+z7/2GtcFTNZw== K49AdBT+U8WUnVNwAOc5Irf86GfNvJ0S0qk6+v4CpPDgKFksc3flFg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2025-11-14T12:05:25Z" lastmodified: "2025-11-28T22:51:04Z"
mac: ENC[AES256_GCM,data:DuD/sqYIwfW2XW+QojfuwLOKI+Y3lXCYOmI5ayCqQlWU9F0W3H1WSnVyfp4YanwY9zjNJukZy9vQNB7wk7DoPGrYTgPihEFmt1GWV10kCFhOxQCMaB9Pt8a7JucF1k8+4jbT4SW282KLAoITDqIAe4qXBPEKKx1+SH34VhKZh3Y=,iv:j5U41i0LHVntwyhTjHExkS65K/gWpBdNy68Cb+bIXmg=,tag:Wu46HI+T+ZzeTb4Rf35vdg==,type:str] mac: ENC[AES256_GCM,data:pXXNkrHSC4mp+K2NfiJ7RucJ299BkVv7wQiWlV2SboLwS7le753CKf2yBGsOYEgxelhLAnIFW3biDR/v+P+7kvUinRUPIYyxeYzMT864Rz8Fb+FBjf4Bd/j/oBdSWk1QM6aOPQRcAiNAol+AcdajhSmy68prUp/lR0gvDPGuczA=,iv:t18AtziHtA58uMoVSsDXsn4PPQUSJCNCG6jg9VBvM0w=,tag:RKQRWjI3NE3Rzb+JdMrOng==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.11.0 version: 3.11.0

View File

@@ -5,6 +5,18 @@ self: super: {
openaudible = (super.callPackage ../pkgs/openaudible.nix { }); openaudible = (super.callPackage ../pkgs/openaudible.nix { });
openmanus = (super.callPackage ../pkgs/openmanus.nix { }); openmanus = (super.callPackage ../pkgs/openmanus.nix { });
ai-mailer = self.callPackage ../pkgs/ai-mailer.nix { }; ai-mailer = self.callPackage ../pkgs/ai-mailer.nix { };
claude-code = self.callPackage ../pkgs/claude-code { claude-code = super.claude-code; };
# Python packages
python3 = super.python3.override {
packageOverrides = pself: psuper: {
aia-chaser = pself.callPackage ../pkgs/aia-chaser { };
mini-racer = pself.callPackage ../pkgs/mini-racer.nix { };
};
};
python3Packages = self.python3.pkgs;
pyload-ng = self.callPackage ../pkgs/pyload-ng { pyload-ng = super.pyload-ng; };
# vscode-insiders = (super.callPackage ../pkgs/vscode-insiders.nix { }); # vscode-insiders = (super.callPackage ../pkgs/vscode-insiders.nix { });
} }

View File

@@ -0,0 +1,32 @@
{ lib
, buildPythonPackage
, fetchPypi
, cryptography
}:
buildPythonPackage rec {
pname = "aia-chaser";
version = "3.3.0";
format = "wheel";
src = fetchPypi {
pname = "aia_chaser";
inherit version format;
dist = "py3";
python = "py3";
hash = "sha256-L0aBV3kfAVI1aJH7VgiiEXzGBSP/HU2zAlahkHeT8hk=";
};
dependencies = [
cryptography
];
pythonImportsCheck = [ "aia_chaser" ];
meta = with lib; {
description = "Retrieve missing certificates to complete SSL certificate chains";
homepage = "https://github.com/dirkjanm/aia-chaser";
license = licenses.mit;
maintainers = [ ];
};
}

View File

@@ -0,0 +1,30 @@
{ lib, pkgs, runCommand, claude-code }:
let
version = "2.0.55";
src = pkgs.fetchzip {
url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${version}.tgz";
hash = "sha256-wsjOkNxuBLMYprjaZQyUZHiqWl8UG7cZ1njkyKZpRYg=";
};
# Create a modified source with our package-lock.json
srcWithLock = runCommand "claude-code-src-${version}" {} ''
cp -r ${src} $out
chmod -R +w $out
cp ${./package-lock.json} $out/package-lock.json
'';
in
(claude-code.override {}).overrideAttrs (oldAttrs: {
inherit version;
src = srcWithLock;
npmDeps = pkgs.fetchNpmDeps {
src = srcWithLock;
hash = "sha256-cFvPoCmh3XpJe/5LPZizfBz6F6xAPYnBNimrK4+VbPw=";
};
# Remove the old postPatch since srcWithLock already includes package-lock.json
postPatch = "";
})

319
utils/pkgs/claude-code/package-lock.json generated Normal file
View File

@@ -0,0 +1,319 @@
{
"name": "claude-code",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@anthropic-ai/claude-code": "^2.0.55"
}
},
"node_modules/@anthropic-ai/claude-code": {
"version": "2.0.55",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.0.55.tgz",
"integrity": "sha512-IVY6J2KgTP5BiCbLmuP3kAl8jbXfd6yGoXtvc0L0eiZwxJUMa+cubUU0U8qHRnVkNmDAis+O4P00KmeuGzSLWg==",
"license": "SEE LICENSE IN README.md",
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-linuxmusl-arm64": "^0.33.5",
"@img/sharp-linuxmusl-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
}
}
}

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p nodejs nix-prefetch cacert
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
# Get latest version from npm
version=$(npm view @anthropic-ai/claude-code version)
echo "Latest version: $version"
# Update version in default.nix
sed -i "s/version = \".*\";/version = \"$version\";/" default.nix
# Fetch and update source hash
echo "Fetching source tarball..."
url="https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${version}.tgz"
hash=$(nix-prefetch-url --unpack --type sha256 "$url" 2>/dev/null)
sri_hash=$(nix-hash --type sha256 --to-sri "$hash")
# Update only the source hash (in fetchzip block)
sed -i "/src = pkgs.fetchzip/,/};/ s|hash = \"sha256-.*\";|hash = \"$sri_hash\";|" default.nix
# Generate new package-lock.json
echo "Generating package-lock.json..."
npm i --package-lock-only @anthropic-ai/claude-code@"$version"
rm -f package.json
# Set placeholder for npmDepsHash only (in fetchNpmDeps block)
sed -i "/npmDeps = pkgs.fetchNpmDeps/,/};/ s|hash = \"sha256-.*\";|hash = \"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";|" default.nix
echo ""
echo "Updated to version $version"
echo "Source hash: $sri_hash"
echo ""
# Go to repo root (3 levels up from utils/pkgs/claude-code)
repo_root="$(cd ../../.. && pwd)"
cd "$repo_root"
# Build to get the correct npmDepsHash
echo "Building package to determine npmDepsHash..."
build_output=$(NIXPKGS_ALLOW_UNFREE=1 nix build --impure --no-link --expr 'with import <nixpkgs> {}; callPackage ./utils/pkgs/claude-code { inherit lib pkgs runCommand claude-code; }' 2>&1 || true)
# Extract the hash from build output
npm_deps_hash=$(echo "$build_output" | grep -oP "got:\s+sha256-[A-Za-z0-9+/=]+" | tail -1 | awk '{print $2}')
if [ -z "$npm_deps_hash" ]; then
echo "Error: Could not determine npmDepsHash from build output"
echo "Build output:"
echo "$build_output"
exit 1
fi
echo "npmDepsHash: $npm_deps_hash"
# Update npmDepsHash in default.nix
cd "$repo_root/utils/pkgs/claude-code"
sed -i "/npmDeps = pkgs.fetchNpmDeps/,/};/ s|hash = \"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";|hash = \"$npm_deps_hash\";|" default.nix
# Verify the build works
echo ""
echo "Verifying build..."
cd "$repo_root"
if NIXPKGS_ALLOW_UNFREE=1 nix build --impure --no-link --expr 'with import <nixpkgs> {}; callPackage ./utils/pkgs/claude-code { inherit lib pkgs runCommand claude-code; }'; then
echo ""
echo "✓ Successfully updated claude-code to version $version"
echo " Source hash: $sri_hash"
echo " npmDepsHash: $npm_deps_hash"
else
echo ""
echo "✗ Build failed after updating hashes"
exit 1
fi

View File

@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""
Click'n'Load proxy - receives CNL requests and forwards to pyLoad API.
Implements the Click'n'Load protocol:
1. GET /jdcheck.js -> responds with jdownloader=true
2. POST /flash/addcrypted2 -> decrypts links and sends to pyLoad
"""
import argparse
import base64
import html
import json
import re
import sys
import traceback
import urllib.request
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
from Crypto.Cipher import AES
def log(msg):
"""Log with immediate flush for systemd journal visibility."""
print(f"[CNL] {msg}", flush=True)
def fetch_package_name(url):
"""Fetch package name from source page by extracting <h2> tag (like JDownloader)."""
if not url or not url.startswith("http"):
return None
try:
log(f"Fetching package name from {url}")
req = urllib.request.Request(
url,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
)
with urllib.request.urlopen(req, timeout=10) as resp:
content = resp.read().decode("utf-8", errors="ignore")
# Extract <h2> content like JDownloader does
match = re.search(r'<h2[^>]*>([^<]+)<', content)
if match:
name = html.unescape(match.group(1)).strip()
log(f"Extracted package name: {name}")
return name
log("No <h2> tag found on page")
return None
except Exception as e:
log(f"Failed to fetch package name: {e}")
return None
def extract_package_name_from_links(links):
"""Extract package name from common prefix of link filenames."""
if not links:
return None
# Extract filenames from URLs
filenames = []
for link in links:
parsed = urllib.parse.urlparse(link)
path = urllib.parse.unquote(parsed.path)
filename = path.split("/")[-1] if path else ""
# Remove extension
if "." in filename:
filename = filename.rsplit(".", 1)[0]
if filename:
filenames.append(filename)
if not filenames:
return None
if len(filenames) == 1:
# Single file - use its name
name = filenames[0]
log(f"Single file, using name: {name}")
return name
# Find common prefix among all filenames
prefix = filenames[0]
for filename in filenames[1:]:
while prefix and not filename.startswith(prefix):
# Remove last character or segment
if "." in prefix:
prefix = prefix.rsplit(".", 1)[0]
elif "-" in prefix:
prefix = prefix.rsplit("-", 1)[0]
elif "_" in prefix:
prefix = prefix.rsplit("_", 1)[0]
else:
prefix = prefix[:-1]
# Clean up trailing separators
prefix = prefix.rstrip(".-_ ")
if prefix and len(prefix) >= 3:
log(f"Common prefix from {len(filenames)} files: {prefix}")
return prefix
# Fallback: use first filename
name = filenames[0]
log(f"No common prefix, using first filename: {name}")
return name
class ClickNLoadHandler(BaseHTTPRequestHandler):
pyload_url = None
pyload_user = None
pyload_pass = None
def log_message(self, format, *args):
log(f"{self.command} {self.path}")
def send_cors_headers(self):
"""Add CORS headers to allow cross-origin requests."""
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def do_OPTIONS(self):
"""Handle CORS preflight requests."""
self.send_response(200)
self.send_cors_headers()
self.end_headers()
def do_GET(self):
if self.path == "/jdcheck.js":
self.send_response(200)
self.send_header("Content-Type", "text/javascript")
self.send_cors_headers()
self.end_headers()
self.wfile.write(b"jdownloader=true;")
else:
self.send_response(404)
self.send_cors_headers()
self.end_headers()
def do_POST(self):
if self.path == "/flash/addcrypted2":
content_length = int(self.headers.get("Content-Length", 0))
post_data = self.rfile.read(content_length).decode("utf-8")
params = urllib.parse.parse_qs(post_data)
jk = params.get("jk", [""])[0]
crypted = params.get("crypted", [""])[0]
source = params.get("source", ["Click'n'Load"])[0]
# Get actual page URL from Referer header
referer = self.headers.get("Referer", "")
log(f"Received addcrypted2: source={source}, referer={referer}")
log(f" jk_len={len(jk)}, crypted_len={len(crypted)}")
try:
links = self.decrypt_links(jk, crypted)
if links:
# Try to get package name: referer page -> link filenames -> source
package_name = (
fetch_package_name(referer) or
extract_package_name_from_links(links) or
source
)
self.add_to_pyload(links, package_name)
self.send_response(200)
self.send_cors_headers()
self.end_headers()
self.wfile.write(b"success")
else:
log("Error: No links found after decryption")
self.send_response(500)
self.send_cors_headers()
self.end_headers()
self.wfile.write(b"No links found")
except Exception as e:
log(f"Error: {e}")
log(traceback.format_exc())
self.send_response(500)
self.send_cors_headers()
self.end_headers()
self.wfile.write(str(e).encode())
else:
self.send_response(404)
self.send_cors_headers()
self.end_headers()
def decrypt_links(self, jk, crypted):
"""Decrypt Click'n'Load encrypted links."""
# Extract hex key from JavaScript function
# Format: function f(){ return '...hex...'; }
match = re.search(r"return\s*['\"]([0-9a-fA-F]+)['\"]", jk)
if not match:
log(f"Could not extract key from jk: {jk[:100]}...")
raise ValueError("Could not extract key from jk")
key_hex = match.group(1)
key = bytes.fromhex(key_hex)
log(f"Extracted key: {len(key)} bytes")
# Decrypt (AES-128-CBC, key is also IV)
cipher = AES.new(key, AES.MODE_CBC, iv=key)
encrypted = base64.b64decode(crypted)
decrypted = cipher.decrypt(encrypted)
# Remove PKCS7 padding
pad_len = decrypted[-1]
decrypted = decrypted[:-pad_len]
# Split into links
links = decrypted.decode("utf-8").strip().split("\n")
links = [l.strip() for l in links if l.strip()]
log(f"Decrypted {len(links)} links")
return links
def add_to_pyload(self, links, package_name):
"""Add links to pyLoad via API using HTTP Basic Auth."""
if not self.pyload_url:
log("No pyLoad URL configured, printing links:")
for link in links:
log(f" {link}")
return
log(f"Adding package to pyLoad at {self.pyload_url}")
# Build Basic Auth header
credentials = f"{self.pyload_user}:{self.pyload_pass}"
auth_header = base64.b64encode(credentials.encode()).decode()
# Add package using new API endpoint with HTTP Basic Auth
add_url = f"{self.pyload_url}/api/add_package"
add_data = json.dumps({
"name": package_name or "Click'n'Load",
"links": links
}).encode()
req = urllib.request.Request(
add_url,
data=add_data,
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {auth_header}"
}
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
result = resp.read().decode()
log(f"Added package to pyLoad: {result}")
except urllib.error.HTTPError as e:
body = e.read().decode() if e.fp else ""
log(f"Failed to add package: {e.code} {e.reason} - {body}")
raise
except urllib.error.URLError as e:
log(f"Failed to add package: {e}")
raise
def main():
parser = argparse.ArgumentParser(description="Click'n'Load proxy for pyLoad")
parser.add_argument("--port", type=int, default=9666, help="Port to listen on")
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
parser.add_argument("--pyload-url", help="pyLoad URL (e.g., https://pyload.example.com)")
parser.add_argument("--pyload-user", help="pyLoad username")
parser.add_argument("--pyload-pass", help="pyLoad password")
parser.add_argument("--config", help="Config file with pyLoad credentials (JSON)")
args = parser.parse_args()
# Load config from file if provided
if args.config:
with open(args.config) as f:
config = json.load(f)
args.pyload_url = config.get("pyloadUrl", args.pyload_url)
args.pyload_user = config.get("pyloadUser", args.pyload_user)
args.pyload_pass = config.get("pyloadPW", args.pyload_pass)
ClickNLoadHandler.pyload_url = args.pyload_url
ClickNLoadHandler.pyload_user = args.pyload_user
ClickNLoadHandler.pyload_pass = args.pyload_pass
server = HTTPServer((args.host, args.port), ClickNLoadHandler)
log(f"Click'n'Load proxy listening on {args.host}:{args.port}")
if args.pyload_url:
log(f"Forwarding to pyLoad at {args.pyload_url}")
else:
log("No pyLoad URL configured, will print links to stdout")
try:
server.serve_forever()
except KeyboardInterrupt:
log("Shutting down")
server.shutdown()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,34 @@
{ lib
, python3
, makeWrapper
}:
let
python = python3.withPackages (ps: [ ps.pycryptodome ]);
in
python3.pkgs.buildPythonApplication {
pname = "clicknload-proxy";
version = "1.0.0";
format = "other";
src = ./.;
nativeBuildInputs = [ makeWrapper ];
propagatedBuildInputs = [ python3.pkgs.pycryptodome ];
installPhase = ''
mkdir -p $out/bin
cp clicknload-proxy.py $out/bin/clicknload-proxy
chmod +x $out/bin/clicknload-proxy
wrapProgram $out/bin/clicknload-proxy \
--prefix PYTHONPATH : "${python}/${python.sitePackages}"
'';
meta = with lib; {
description = "Click'n'Load proxy that forwards links to pyLoad";
license = licenses.mit;
mainProgram = "clicknload-proxy";
};
}

41
utils/pkgs/mini-racer.nix Normal file
View File

@@ -0,0 +1,41 @@
{ lib
, buildPythonPackage
, fetchPypi
, stdenv
, autoPatchelfHook
}:
buildPythonPackage rec {
pname = "mini-racer";
version = "0.12.4";
format = "wheel";
src = fetchPypi {
pname = "mini_racer";
inherit version format;
dist = "py3";
python = "py3";
abi = "none";
platform = "manylinux_2_31_x86_64";
hash = "sha256-aaHETQKpBpuIFoTO8VotdH/gdD3ynq3Igf2nACquX9I=";
};
nativeBuildInputs = [
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];
# Don't strip binaries, it breaks V8
dontStrip = true;
meta = with lib; {
description = "Minimal Python wrapper for V8 JavaScript engine";
homepage = "https://github.com/bpcreech/PyMiniRacer";
license = licenses.isc;
maintainers = [ ];
platforms = [ "x86_64-linux" ];
};
}

View File

@@ -0,0 +1,26 @@
{ lib, pyload-ng, fetchFromGitHub, python3Packages }:
pyload-ng.overridePythonAttrs (oldAttrs: rec {
version = "0.5.0b3.dev93+git";
src = fetchFromGitHub {
owner = "pyload";
repo = "pyload";
rev = "71f2700184ee9344dc313d9833ca7a6bb36007db"; # [DdownloadCom] fix #4537
hash = "sha256-XAa+XbC3kko+zvEMZkPXRoaHAmEFGsNBDxysX+X06Jc=";
};
patches = [
./patches/declarative-env-config.patch
];
# Add new dependencies required in newer versions
propagatedBuildInputs = (oldAttrs.propagatedBuildInputs or []) ++ (with python3Packages; [
aia-chaser
mini-racer
packaging
pydantic
flask-wtf
defusedxml
]);
})

View File

@@ -0,0 +1,38 @@
--- a/src/pyload/core/__init__.py
+++ b/src/pyload/core/__init__.py
@@ -130,6 +130,14 @@ class Core:
else:
self._debug = max(0, int(debug))
+ # Process core config environment variables (NixOS declarative config)
+ for env, value in os.environ.items():
+ if not env.startswith("PYLOAD__"):
+ continue
+ parts = env.removeprefix("PYLOAD__").lower().split("__", 1)
+ if len(parts) == 2 and parts[0] in self.config.config:
+ self.config.set(parts[0], parts[1], value)
+
# If no argument set, read storage dir from config file,
# otherwise save setting to config dir
if storagedir is None:
@@ -226,6 +234,20 @@ class Core:
self.acm = self.account_manager = AccountManager(self)
self.thm = self.thread_manager = ThreadManager(self)
self.cpm = self.captcha_manager = CaptchaManager(self)
+
+ # Process plugin config environment variables BEFORE AddonManager (NixOS declarative config)
+ # This must happen before AddonManager reads the enabled flag to decide which addons to start
+ # Build case-insensitive lookup map for plugin names
+ plugin_name_map = {name.lower(): name for name in self.config.plugin.keys()}
+
+ for env, value in os.environ.items():
+ if not env.startswith("PYLOAD__"):
+ continue
+ parts = env.removeprefix("PYLOAD__").lower().split("__", 1)
+ if len(parts) == 2 and parts[0] in plugin_name_map:
+ actual_plugin_name = plugin_name_map[parts[0]]
+ self.config.set_plugin(actual_plugin_name, parts[1], value)
+
self.adm = self.addon_manager = AddonManager(self)
def _setup_permissions(self):

107
utils/pkgs/pyload-ng/update.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Script and repo directories
cd "$(dirname "${BASH_SOURCE[0]}")"
PKG_DIR="$(pwd)"
REPO_ROOT="$(cd ../../.. && pwd)"
# Check if commit SHA is provided
if [ $# -ne 1 ]; then
echo -e "${RED}Error: Git commit SHA required${NC}"
echo "Usage: $0 <commit-sha>"
echo "Example: $0 e5fe7038f3e116d878c58059323b682e426f9c84"
echo ""
echo "To find the latest commit:"
echo " Visit: https://github.com/pyload/pyload/commits/main"
exit 1
fi
COMMIT_SHA="$1"
# Validate commit SHA format (40 character hex string)
if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo -e "${RED}Error: Invalid commit SHA format${NC}"
echo "Commit SHA must be a 40-character hexadecimal string"
exit 1
fi
echo -e "${GREEN}==> Updating pyload-ng to commit: ${COMMIT_SHA}${NC}"
# File to update
PKG_FILE="$PKG_DIR/default.nix"
if [ ! -f "$PKG_FILE" ]; then
echo -e "${RED}Error: Package file not found: $PKG_FILE${NC}"
exit 1
fi
# Step 1: Update commit SHA in package file
echo -e "${YELLOW}Step 1: Updating commit SHA in package file...${NC}"
sed -i "s/rev = \"[0-9a-f]*\";/rev = \"$COMMIT_SHA\";/" "$PKG_FILE"
echo " ✓ Updated commit SHA in $PKG_FILE"
# Step 2: Set hash to a fake value to trigger hash discovery
echo -e "${YELLOW}Step 2: Setting hash to fake value...${NC}"
sed -i 's/hash = "sha256-[^"]*";/hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";/' "$PKG_FILE"
echo " ✓ Updated hash in $PKG_FILE"
# Step 3: Build package to discover the correct hash
echo -e "${YELLOW}Step 3: Building package to discover hash...${NC}"
cd "$REPO_ROOT"
BUILD_OUTPUT=$(nix-build --impure -E "with import <nixpkgs> { overlays = [ (import ./utils/overlays/packages.nix) ]; }; callPackage ./utils/pkgs/pyload-ng { }" 2>&1 || true)
# Extract hash from error message
HASH=$(echo "$BUILD_OUTPUT" | grep -oP '\s+got:\s+\Ksha256-[A-Za-z0-9+/=]+' | head -1)
if [ -z "$HASH" ]; then
echo -e "${RED}Error: Failed to extract hash from build output${NC}"
echo "Build output:"
echo "$BUILD_OUTPUT"
exit 1
fi
echo " ✓ Discovered hash: $HASH"
# Step 4: Update package file with the correct hash
echo -e "${YELLOW}Step 4: Updating hash in package file...${NC}"
sed -i "s|hash = \"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\";|hash = \"$HASH\";|" "$PKG_FILE"
echo " ✓ Updated hash in $PKG_FILE"
# Step 5: Verify the build succeeds
echo -e "${YELLOW}Step 5: Verifying build with correct hash...${NC}"
if nix-build --impure -E "with import <nixpkgs> { overlays = [ (import ./utils/overlays/packages.nix) ]; }; callPackage ./utils/pkgs/pyload-ng { }" > /dev/null 2>&1; then
echo " ✓ Build verification successful"
else
echo -e "${RED}Error: Build verification failed${NC}"
exit 1
fi
# Step 6: Test configuration for fw host (which uses pyload)
echo -e "${YELLOW}Step 6: Testing fw configuration...${NC}"
if ./scripts/test-configuration fw > /dev/null 2>&1; then
echo " ✓ Configuration test passed"
else
echo -e "${RED}Warning: Configuration test failed${NC}"
echo "This may be due to missing secrets or other issues unrelated to the hash update."
fi
# Success summary
echo -e "${GREEN}"
echo "======================================"
echo "✓ pyload-ng updated successfully!"
echo "======================================"
echo "Commit: $COMMIT_SHA"
echo "Hash: $HASH"
echo -e "${NC}"
echo "Next steps:"
echo " 1. Review changes: git diff $PKG_FILE"
echo " 2. Test locally if needed"
echo " 3. Commit changes: git add $PKG_FILE && git commit -m 'update: pyload-ng to commit ${COMMIT_SHA:0:8}'"
echo " 4. Push to trigger automatic deployment"