diff --git a/hosts/amzebs-01/EMAIL_SETUP.md b/hosts/amzebs-01/EMAIL_SETUP.md new file mode 100644 index 0000000..72aadfb --- /dev/null +++ b/hosts/amzebs-01/EMAIL_SETUP.md @@ -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= +``` + +## 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. diff --git a/hosts/amzebs-01/configuration.nix b/hosts/amzebs-01/configuration.nix index 9ccb1e1..11336ed 100644 --- a/hosts/amzebs-01/configuration.nix +++ b/hosts/amzebs-01/configuration.nix @@ -8,6 +8,8 @@ ./modules/web/stack.nix ./modules/laravel-storage.nix ./modules/blackbox-exporter.nix + ./modules/postfix.nix + ./modules/rspamd.nix ./utils/modules/autoupgrade.nix ./utils/modules/promtail diff --git a/hosts/amzebs-01/modules/laravel-storage.nix b/hosts/amzebs-01/modules/laravel-storage.nix index 92e8884..513e8d4 100644 --- a/hosts/amzebs-01/modules/laravel-storage.nix +++ b/hosts/amzebs-01/modules/laravel-storage.nix @@ -5,6 +5,7 @@ systemd.tmpfiles.rules = [ # 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/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/views 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 "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/views 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 "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/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 -" diff --git a/hosts/amzebs-01/modules/postfix.nix b/hosts/amzebs-01/modules/postfix.nix new file mode 100644 index 0000000..6c21698 --- /dev/null +++ b/hosts/amzebs-01/modules/postfix.nix @@ -0,0 +1,55 @@ +{ pkgs +, lib +, config +, ... +}: +{ + # Header checks file for validating email headers + environment.etc."postfix/header_checks".text = '' + # Warn about missing critical headers (but don't reject from localhost) + # These help identify misconfigured applications + /^$/ WARN Missing headers detected + ''; + + services.postfix = { + 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:/etc/postfix/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 + }; + }; +} diff --git a/hosts/amzebs-01/modules/rspamd.nix b/hosts/amzebs-01/modules/rspamd.nix new file mode 100644 index 0000000..fa3dd80 --- /dev/null +++ b/hosts/amzebs-01/modules/rspamd.nix @@ -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"; + }; +} diff --git a/hosts/amzebs-01/secrets.yaml b/hosts/amzebs-01/secrets.yaml index 36c2241..2407a9e 100644 --- a/hosts/amzebs-01/secrets.yaml +++ b/hosts/amzebs-01/secrets.yaml @@ -1,45 +1,46 @@ -borg-passphrase: ENC[AES256_GCM,data:6T00Em+a5TcrmQNvtoCoij5aks6KIZkCAAaPXLirkQlZ6x1p1bX9KXU2ZvBAtVPrUuTeZLPTKqT/iL5Io+WKGw==,iv:gB9cktzKa8khmZZ8xwLS6oEX+Ag3APmf2jIQNLa1g/Y=,tag:sWfVbKQHgaaSRWuqYdpKTQ==,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] -mysql-readonly-password: ENC[AES256_GCM,data:KQiL0ZJGkJEqX7wADmY2YucT79Grt+tCQA/aER7llHgqUIvjJHO8C2yw+VI=,iv:M3QchAeKXp7BjP2FfaWgUNiGPs0qQHe9P5lttxO5+Fg=,tag:TPGUWXYQsf40hlVu7PGEEg==,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: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: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: age: - recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvWjd4K3BueGVxNGs2OC9t - YURtcTIzNytCMDdYd29KenZOdm4vMk9mWEZFCkJrZlVZdkVJeW4rNzc0N3NBY3hM - dE1ORzRHRHlONEQ2dW83R051aE45QVEKLS0tIE0zOVpVbWphNitPaUg0cmxUbW5m - ZHViVHJrOWREb0pYR3hOTm0zSHZ3N0EKeNcZOM+H0XZN3Ji1ubBoHMgycuJFX3+C - YvJ795wSwtXMU+mCDB04tcYPSAI0RC82wGT9r3XLNZgbF/xP0Er3nw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhQUpWNUgxVnhuTXd2TkF0 + SVVHemFKRWlYczZ0TnBESVNRczhuRUNnUG1BCmJKQ2JZbHhFcXJidHJzci9OaFBm + ZTd0MGhsaVBic3dMb3psUHRCRnR3ODQKLS0tIERrSG1GVTRHdkJpVWpqdTZ4Yytq + OHhlZjV6MjRVbXFsWjlQSU03ZDNwYm8KAswHRSdV0BW/oJyZx63iZRHsF7SZ6PO+ + hajQqmEyfcVfEu39zZzxQ2mtWlOr69I++irOhE3NeiFeJ1yIRQDJEQ== -----END AGE ENCRYPTED FILE----- - recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTnpIdHBmMGdBQzk2N0xW - SGlNMUxjYU5SZHJLK0wvWnZyYkEzK04vdWd3CmVGWW50RUQ2K0lLRC90WW9KY1hj - NGVpMEdzaHUyTUVBaDcxb2w5MC9BUjAKLS0tIFdSSjlFTHl1Z3NYNVhxaSs0WkJE - eGVWZHdnMkhaNzlDNlhCa24wZzlvNmsK7pLzsxtlMevP2o9nJOjVgDAjrYdEgRUu - NlJHfO0m9U7fJfeu6XSWQgGYRJm7tSmTZKvsJgTS+pKcynHz8B9rkQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUaUNnY0hpdDAzMTNIUS9D + RmdKbmplUk9DRXlLRXEvSnVjT05sQjcvTnpJCkd6bGRINm5yYUZOUTVzWEdjRmtG + Mmx0ci93N2wvTWV5MzlRVnlYdUxoUWsKLS0tIEVHUlNWYStWTG01RzRrVnNXc3BW + VkRkUXROU3plNmwvTUVhYmhCS2syQkEKKgC0EmUu1u2vZ/SZTnam+h846gZSyY4V + JyMzkws8O5TY9juWdDzXJIU67mIgc4qrWWN3uh8k28JBZGc078b5bg== -----END AGE ENCRYPTED FILE----- - recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNMk9VL2s0ZmoxSzY3NmFQ - MTZEaml5Q0kxRDFwSEN5dG90SzZmcXZLUjJBClpVNUJEZEdaa09hM1BQUU1jVVQz - M2w4QXdmZnJya2pCTEV1QW9keXgyTWMKLS0tIFA1VWl3RzF5V2FMUE1mZ2NYRnBU - OURWSFZnM0lEMXJEcjVPL3hnZ0pIQ1kKVvoCVQuayH/XRfddMKq2d8TssXOS5e1o - bIL6F+tRBle2UgVuXSMkyggCnvLePA8OxfAdMMg5npSFkPgTZrAYYQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoK2JUNVdYTzkvM1BBWXRm + citCNlE4Z1NLdEZ2R0tNZTVSMlFSeGxGOURnClJnYURYa0JZaVprQWdBcmVnOWVj + TGVCK1JWMVlueHJUaTZZYmROM0E5aDAKLS0tIEJxYkdadGtZM250d2d6Ujl2UU9C + YUpkVll2S2RpT0I1UVZiZFRKS1prMEEKp/bGImanJ/58vTQG/gUun/Y2QdmOEi3h + hVS0V2QcfuGgi0/YofLOM3+M6k6ViXw07XfXmR+puvLIHKr2y11x1Q== -----END AGE ENCRYPTED FILE----- - recipient: age1xcgc6u7fmc2trgxtdtf5nhrd7axzweuxlg0ya9jre3sdrg6c6easecue9w enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3QmR3T3FYN2RpR1JrV0w0 - aVMycUJUN1NKME1UKzVsZmpNNTAzTmxUUUZNClhmdUF5N0Q4K09IeVhNOWhNNEc2 - UTNzeGJ4NlpxMUtEaHZDWDZOeHdvSU0KLS0tIFNTaThWbklXeE85c3hSMWZwNTNN - RWFUVXVXWjdsSHM5ZGljd3YyQW5ja2MKvAhwHL5PcLFxuU7MfV/cWtNfzTb9yoqR - 3iD4UJsDDagCIkpvjKods4ydlzh3agOyLHswDSX/WmUur9J5pd4PAg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDSGdEZnZEaDRpWUJVcnds + VGFSQklvczBZdEdEbXhodW8vME9wMUpVRENjClFZcnVqYkJxdlBiZFhma0tmZjgz + YXlIdlRDTDU4MHg1dzhGVDRJb2FGYVUKLS0tIDBXSWZ2NkxzdEk0ZlFRM00ybFNy + M0doaWl5R2cwU2RxQm5DbWxXeTZ5S2MKwrB3SysmgzCThQOhEVx18dxIfko0+oZY + 9BSZOoFbfuwiLbtpL4J8bzxDvxn6sXxB8EBJH1hbpID53AquWDsxSw== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-14T11:33:59Z" - mac: ENC[AES256_GCM,data:AnEs3yzpOJ5/wyCL/sHV6U5V7FBhZZlBQeA+mCGfZ25JZAL3Yb6yD6xJhmGC8AqIFS6PIFSWa2r0suDRQAoVO2AwFVwd9Y/TEwjPGnXvWfwB82+mnyLIakyzM/pcLjiMePUqr5nnJ8tWoKzuqs/jQHuMOGkItqwjkVDr9/hx3lc=,iv:kc08z63phDfs7gzruHjnQA9bXAvWMGkE14/0Kyfhuds=,tag:9wedcRCTAv0HMtSap55JOw==,type:str] + lastmodified: "2025-11-19T11:16:25Z" + 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 version: 3.11.0 diff --git a/hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix b/hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix index 4d2dd25..2e2edfd 100644 --- a/hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix +++ b/hosts/amzebs-01/sites/ebs-mobile.cloonar.dev.nix @@ -18,7 +18,7 @@ in { # React client-side routing support locations."/".extraConfig = '' index index.html; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html$is_args$args; ''; # Cache static assets diff --git a/hosts/amzebs-01/sites/ebs.amz.at.nix b/hosts/amzebs-01/sites/ebs.amz.at.nix index e6ec6a4..1caa1a3 100644 --- a/hosts/amzebs-01/sites/ebs.amz.at.nix +++ b/hosts/amzebs-01/sites/ebs.amz.at.nix @@ -18,7 +18,7 @@ in { # React client-side routing support locations."/".extraConfig = '' index index.html; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html$is_args$args; ''; # Cache static assets diff --git a/hosts/amzebs-01/sites/ebs.cloonar.dev.nix b/hosts/amzebs-01/sites/ebs.cloonar.dev.nix index ef4aa78..218b9e2 100644 --- a/hosts/amzebs-01/sites/ebs.cloonar.dev.nix +++ b/hosts/amzebs-01/sites/ebs.cloonar.dev.nix @@ -18,7 +18,7 @@ in { # React client-side routing support locations."/".extraConfig = '' index index.html; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html$is_args$args; ''; # Cache static assets diff --git a/hosts/amzebs-01/sites/stage.ebs.amz.at.nix b/hosts/amzebs-01/sites/stage.ebs.amz.at.nix index 60dc9bc..ee258d6 100644 --- a/hosts/amzebs-01/sites/stage.ebs.amz.at.nix +++ b/hosts/amzebs-01/sites/stage.ebs.amz.at.nix @@ -18,7 +18,7 @@ in { # React client-side routing support locations."/".extraConfig = '' index index.html; - try_files $uri $uri/ /index.html; + try_files $uri $uri/ /index.html$is_args$args; ''; # Cache static assets diff --git a/hosts/fw/configuration.nix b/hosts/fw/configuration.nix index 87c6312..ecfb518 100644 --- a/hosts/fw/configuration.nix +++ b/hosts/fw/configuration.nix @@ -49,7 +49,8 @@ ./modules/firefox-sync.nix ./modules/fivefilters.nix - + ./modules/pyload.nix + # home assistant ./modules/home-assistant ./modules/deconz.nix @@ -87,6 +88,18 @@ "ai-mailer" ]; + # 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"; services.logind.extraConfig = "RuntimeDirectorySize=2G"; diff --git a/hosts/fw/modules/ai-mailer.nix b/hosts/fw/modules/ai-mailer.nix index cf1f77a..35d81d7 100644 --- a/hosts/fw/modules/ai-mailer.nix +++ b/hosts/fw/modules/ai-mailer.nix @@ -33,6 +33,7 @@ context: urls: + - "https://paraclub.cloonar.dev/de/tandemfallschirmspringen/faq/" - "https://paraclub.at/de/" - "https://paraclub.at/de/tandemfallschirmspringen/alle-infos/" - "https://paraclub.at/de/tandemfallschirmspringen/kosten-tandemsprung/" diff --git a/hosts/fw/modules/deconz.nix b/hosts/fw/modules/deconz.nix index bfbf632..eea6b2a 100644 --- a/hosts/fw/modules/deconz.nix +++ b/hosts/fw/modules/deconz.nix @@ -2,21 +2,33 @@ virtualisation = { oci-containers.containers = { deconz = { - autoStart = false; + autoStart = true; image = "marthoc/deconz"; volumes = [ "/etc/localtime:/etc/localtime:ro" "/var/lib/deconz:/root/.local/share/dresden-elektronik/deCONZ" + "/dev/bus/usb:/dev/bus/usb:ro" + "/run/udev:/run/udev:ro" ]; environment = { DECONZ_DEVICE = "/dev/ttyACM0"; TZ = "Europe/Vienna"; + DECONZ_UID = "0"; + DECONZ_GID = "0"; + DECONZ_START_VERBOSE = "1"; }; extraOptions = [ "--network=server" "--ip=${config.networkPrefix}.97.22" "--device=/dev/ttyACM0" "--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" ]; }; }; diff --git a/hosts/fw/modules/dnsmasq.nix b/hosts/fw/modules/dnsmasq.nix index 70d5ef9..c323f0b 100644 --- a/hosts/fw/modules/dnsmasq.nix +++ b/hosts/fw/modules/dnsmasq.nix @@ -133,6 +133,10 @@ "/foundry-vtt.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" "/ddl-warez.to/172.67.184.30" diff --git a/hosts/fw/modules/home-assistant/light.nix b/hosts/fw/modules/home-assistant/light.nix index 03e218f..26579e7 100644 --- a/hosts/fw/modules/home-assistant/light.nix +++ b/hosts/fw/modules/home-assistant/light.nix @@ -396,12 +396,7 @@ all = true; entities = [ "light.livingroom_switch" - "light.livingroom_bulb_1_rgbcw_bulb" - "light.livingroom_bulb_2_rgbcw_bulb" - "light.livingroom_bulb_3_rgbcw_bulb" - "light.livingroom_bulb_4_rgbcw_bulb" - "light.livingroom_bulb_5_rgbcw_bulb" - "light.livingroom_bulb_6_rgbcw_bulb" + "light.living_room" ]; } { diff --git a/hosts/fw/modules/pyload.nix b/hosts/fw/modules/pyload.nix new file mode 100644 index 0000000..9a0b1b0 --- /dev/null +++ b/hosts/fw/modules/pyload.nix @@ -0,0 +1,191 @@ +{ config, pkgs, ... }: +let + cids = import ./staticids.nix; + networkPrefix = config.networkPrefix; + + pyloadUser = { + isSystemUser = true; + uid = cids.uids.pyload; + group = "pyload"; + home = "/var/lib/pyload"; + createHome = true; + }; + 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 multimedia directory structure on the host + systemd.tmpfiles.rules = [ + "d /var/lib/multimedia 0755 root root - -" + "d /var/lib/multimedia/downloads 0755 pyload pyload - -" + "d /var/lib/multimedia/movies 0755 jellyfin jellyfin - -" + "d /var/lib/multimedia/tv-shows 0755 jellyfin jellyfin - -" + "d /var/lib/multimedia/music 0755 jellyfin jellyfin - -" + "d /var/lib/jellyfin 0755 jellyfin jellyfin - -" + ]; + + 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; + }; + "/multimedia" = { + hostPath = "/var/lib/multimedia"; + isReadOnly = false; + }; + }; + + config = { lib, config, pkgs, ... }: { + nixpkgs.overlays = [ + (import ../utils/overlays/packages.nix) + ]; + + + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ + "unrar" + ]; + + environment.systemPackages = with pkgs; [ + unrar # Required for RAR archive extraction + ]; + + # 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"; + }; + + networking = { + hostName = "pyload"; + useHostResolvConf = false; + defaultGateway = { + address = "${networkPrefix}.97.1"; + interface = "eth0"; + }; + nameservers = [ "${networkPrefix}.97.1" ]; + firewall.enable = false; + }; + + services.pyload = { + enable = true; + downloadDirectory = "/multimedia/downloads"; + listenAddress = "0.0.0.0"; + port = 8000; + }; + + 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 + }; + }; + + # Disable SSL certificate verification + systemd.services.pyload = { + environment = { + PYLOAD__GENERAL__SSL_VERIFY = "0"; + }; + + # Bind-mount DNS configuration files and system tools into the chroot + serviceConfig = { + BindReadOnlyPaths = [ + "/etc/resolv.conf" + "/etc/nsswitch.conf" + "/etc/hosts" + "/etc/ssl" + "/etc/static/ssl" + # Make all system packages (including unrar) accessible + "/run/current-system/sw/bin" + ]; + }; + }; + + # 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"; + }; + }; +} diff --git a/hosts/fw/modules/staticids.nix b/hosts/fw/modules/staticids.nix index 498d9db..ae57ad5 100644 --- a/hosts/fw/modules/staticids.nix +++ b/hosts/fw/modules/staticids.nix @@ -5,6 +5,8 @@ gitea-runner = 10003; podman = 10004; foundry-vtt = 10005; + pyload = 10006; + jellyfin = 10007; }; gids = { unbound = 10001; @@ -12,5 +14,7 @@ gitea-runner = 10003; podman = 10004; foundry-vtt = 10005; + pyload = 10006; + jellyfin = 10007; }; } diff --git a/hosts/fw/modules/web/proxies.nix b/hosts/fw/modules/web/proxies.nix index 87878cd..5991ea5 100644 --- a/hosts/fw/modules/web/proxies.nix +++ b/hosts/fw/modules/web/proxies.nix @@ -33,4 +33,51 @@ 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}.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}.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; + ''; + }; + }; } diff --git a/hosts/nb/configuration.nix b/hosts/nb/configuration.nix index e1b6db5..f23c92c 100644 --- a/hosts/nb/configuration.nix +++ b/hosts/nb/configuration.nix @@ -189,6 +189,8 @@ in { networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. networking.extraHosts = '' 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. @@ -287,8 +289,9 @@ in { settings = { auto-optimise-store = true; experimental-features = [ "nix-command" "flakes" ]; - max-jobs = 12; - cores = 2; + max-jobs = 4; + cores = 4; + max-substitution-jobs = 16; }; gc = { automatic = true; diff --git a/hosts/nb/modules/development/mcp-chromium.nix b/hosts/nb/modules/development/mcp-chromium.nix index 0ab6c52..f357393 100644 --- a/hosts/nb/modules/development/mcp-chromium.nix +++ b/hosts/nb/modules/development/mcp-chromium.nix @@ -3,7 +3,7 @@ let # Wrapper to launch Chromium on Wayland, scale=1, DevTools debugging on 127.0.0.1:9222 chromiumWaylandWrapper = pkgs.writeShellScriptBin "chromium-mcp" '' - exec ${pkgs.chromium}/bin/chromium \ + exec ${pkgs.ungoogled-chromium}/bin/chromium \ --ozone-platform=wayland \ --enable-features=UseOzonePlatform \ --force-device-scale-factor=1 \ @@ -11,32 +11,13 @@ let --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 { - # Tools: Chromium, Node (for MCP server), our wrapper, and the desktop override + # Tools: Chromium, Node (for MCP server), our wrapper environment.systemPackages = [ - pkgs.chromium + pkgs.ungoogled-chromium pkgs.nodejs_22 # 25.05 ships Node 22 LTS; works great for MCP servers chromiumWaylandWrapper - chromiumDesktopOverride # ← keep AFTER pkgs.chromium so our .desktop wins ]; # Where Codex CLI reads config; we make it system-wide diff --git a/hosts/nb/modules/puppeteer.nix b/hosts/nb/modules/puppeteer.nix index c84529b..4207f30 100644 --- a/hosts/nb/modules/puppeteer.nix +++ b/hosts/nb/modules/puppeteer.nix @@ -1,6 +1,6 @@ { config, pkgs, ... }: { environment.systemPackages = with pkgs; [ - chromium + ungoogled-chromium nodejs # Graphics and font dependencies freetype @@ -30,6 +30,6 @@ ]; environment.variables = { - PUPPETEER_EXECUTABLE_PATH = "${pkgs.chromium}/bin/chromium"; + PUPPETEER_EXECUTABLE_PATH = "${pkgs.ungoogled-chromium}/bin/chromium"; }; } diff --git a/hosts/nb/users/dominik.nix b/hosts/nb/users/dominik.nix index 90035c4..cee91e4 100644 --- a/hosts/nb/users/dominik.nix +++ b/hosts/nb/users/dominik.nix @@ -228,6 +228,21 @@ in Restart = "always"; }; }; + pyload-tunnel = { + Unit = { + Description = "SSH tunnel for pyLoad Click'n'Load"; + After = [ "graphical-session-pre.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = "${pkgs.openssh}/bin/ssh -N -L 9666:10.42.97.11:9666 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 root@fw.cloonar.com"; + Restart = "always"; + RestartSec = "10s"; + }; + }; }; programs.chromium = { diff --git a/scripts/update-pyload-hash b/scripts/update-pyload-hash new file mode 100755 index 0000000..100ee8b --- /dev/null +++ b/scripts/update-pyload-hash @@ -0,0 +1,106 @@ +#!/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 directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Check if commit SHA is provided +if [ $# -ne 1 ]; then + echo -e "${RED}Error: Git commit SHA required${NC}" + echo "Usage: $0 " + 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="$REPO_ROOT/utils/pkgs/pyload-ng-updated.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}" +BUILD_OUTPUT=$(nix-build --impure -E "with import { overlays = [ (import $REPO_ROOT/utils/overlays/packages.nix) ]; }; callPackage $PKG_FILE { }" 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 { overlays = [ (import $REPO_ROOT/utils/overlays/packages.nix) ]; }; callPackage $PKG_FILE { }" > /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}" +cd "$REPO_ROOT" +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" diff --git a/utils/overlays/packages.nix b/utils/overlays/packages.nix index cffe405..811ae3b 100644 --- a/utils/overlays/packages.nix +++ b/utils/overlays/packages.nix @@ -6,5 +6,15 @@ self: super: { openmanus = (super.callPackage ../pkgs/openmanus.nix { }); ai-mailer = self.callPackage ../pkgs/ai-mailer.nix { }; + # Python packages + python3 = super.python3.override { + packageOverrides = pself: psuper: { + mini-racer = pself.callPackage ../pkgs/mini-racer.nix { }; + }; + }; + python3Packages = self.python3.pkgs; + + pyload-ng = self.callPackage ../pkgs/pyload-ng-updated.nix { pyload-ng = super.pyload-ng; }; + # vscode-insiders = (super.callPackage ../pkgs/vscode-insiders.nix { }); } diff --git a/utils/pkgs/mini-racer.nix b/utils/pkgs/mini-racer.nix new file mode 100644 index 0000000..3d00cc1 --- /dev/null +++ b/utils/pkgs/mini-racer.nix @@ -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" ]; + }; +} diff --git a/utils/pkgs/pyload-ng-updated.nix b/utils/pkgs/pyload-ng-updated.nix new file mode 100644 index 0000000..195eebf --- /dev/null +++ b/utils/pkgs/pyload-ng-updated.nix @@ -0,0 +1,21 @@ +{ lib, pyload-ng, fetchFromGitHub, python3Packages }: + +pyload-ng.overridePythonAttrs (oldAttrs: rec { + version = "0.5.0b3.dev93+git"; + + src = fetchFromGitHub { + owner = "pyload"; + repo = "pyload"; + rev = "3115740a2210fd57b5d050cd0850a0e61ec493ed"; # [DdownloadCom] fix #4537 + hash = "sha256-g1eEeNnr3Axtr+0BJzMcNQomTEX4EsUG1Jxt+huPyoc="; + }; + + # Add new dependencies required in newer versions + propagatedBuildInputs = (oldAttrs.propagatedBuildInputs or []) ++ (with python3Packages; [ + mini-racer + packaging + pydantic + flask-wtf + defusedxml + ]); +})