Compare commits

...

11 Commits

26 changed files with 1123 additions and 63 deletions

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.

View File

@@ -8,6 +8,8 @@
./modules/web/stack.nix ./modules/web/stack.nix
./modules/laravel-storage.nix ./modules/laravel-storage.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

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,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
};
};
}

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

@@ -49,6 +49,7 @@
./modules/firefox-sync.nix ./modules/firefox-sync.nix
./modules/fivefilters.nix ./modules/fivefilters.nix
./modules/pyload.nix
# home assistant # home assistant
./modules/home-assistant ./modules/home-assistant
@@ -87,6 +88,18 @@
"ai-mailer" "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"; time.timeZone = "Europe/Vienna";
services.logind.extraConfig = "RuntimeDirectorySize=2G"; services.logind.extraConfig = "RuntimeDirectorySize=2G";

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/"

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

@@ -133,6 +133,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,7 @@
all = true; all = true;
entities = [ entities = [
"light.livingroom_switch" "light.livingroom_switch"
"light.livingroom_bulb_1_rgbcw_bulb" "light.living_room"
"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"
]; ];
} }
{ {

191
hosts/fw/modules/pyload.nix Normal file
View File

@@ -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";
};
};
}

View File

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

View File

@@ -33,4 +33,51 @@
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}.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;
'';
};
};
} }

View File

@@ -189,6 +189,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 +289,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;

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,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

@@ -228,6 +228,21 @@ in
Restart = "always"; 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 = { programs.chromium = {

106
scripts/update-pyload-hash Executable file
View File

@@ -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 <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="$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 <nixpkgs> { 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 <nixpkgs> { 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"

View File

@@ -6,5 +6,15 @@ self: super: {
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 { };
# 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 { }); # vscode-insiders = (super.callPackage ../pkgs/vscode-insiders.nix { });
} }

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,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
]);
})