Compare commits

...

4 Commits

Author SHA1 Message Date
09e381ecc4 feat: add attic cache 2025-10-14 22:30:20 +02:00
7fd35b79c4 fix: blacklist attic website exporter 2025-10-14 22:29:44 +02:00
c9900e4314 fix: atticd server 2025-10-14 22:24:35 +02:00
5ea3bac570 feat: add update-keys script 2025-10-14 20:02:42 +02:00
8 changed files with 346 additions and 47 deletions

View File

@@ -4,7 +4,7 @@
# for a more complex example. # for a more complex example.
keys: keys:
- &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key - &bitwarden age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7 # nixos age key
- &dominik age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d - &dominik age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
- &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch - &dominik2 age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
- &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58 - &git-server age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58
- &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw - &web-02 age1gjm4c3swt8u88e36gf2qlg3syxfc0ly94u64c42f2tsf24npw4csa6e4fw
@@ -94,12 +94,13 @@ creation_rules:
- *netboot - *netboot
- *fw - *fw
- *fw-new - *fw-new
- path_regex: utils/modules/plausible/[^/]+\.yaml$ - path_regex: utils/modules/attic-cache/[^/]+\.yaml$
key_groups: key_groups:
- age: - age:
- *bitwarden - *bitwarden
- *dominik - *dominik
- *dominik2 - *dominik2
- *nb
- path_regex: utils/modules/promtail/[^/]+\.yaml$ - path_regex: utils/modules/promtail/[^/]+\.yaml$
key_groups: key_groups:
- age: - age:

View File

@@ -15,9 +15,10 @@ in {
[ [
"${impermanence}/nixos.nix" "${impermanence}/nixos.nix"
./utils/bento.nix ./utils/bento.nix
./utils/modules/sops.nix ./utils/modules/sops.nix
./utils/modules/nur.nix ./utils/modules/nur.nix
./utils/modules/attic-cache
./modules/appimage.nix ./modules/appimage.nix
./modules/desktop ./modules/desktop
./modules/development ./modules/development

View File

@@ -27,29 +27,25 @@ in {
# API endpoint configuration # API endpoint configuration
api-endpoint = "https://${atticHost}/"; api-endpoint = "https://${atticHost}/";
# Allow automatic registration (set to false for production if you want to control access)
allow-registration = false;
# Require tokens for all operations # Require tokens for all operations
require-proof-of-possession = true; require-proof-of-possession = true;
# Chunking settings for large uploads # Chunking settings for large uploads
chunking = { chunking = {
# Minimum chunk size: 16 MiB nar-size-threshold = 65536;
min-size = 16 * 1024 * 1024; min-size = 16384;
# Average chunk size: 64 MiB avg-size = 65536;
avg-size = 64 * 1024 * 1024; max-size = 262144;
# Maximum chunk size: 256 MiB
max-size = 256 * 1024 * 1024;
}; };
# Garbage collection # Garbage collection
garbage-collection = { garbage-collection = {
# GC interval in seconds (12 hours) # GC interval in seconds (12 hours)
interval = 12 * 60 * 60; interval = "12 hours";
# Delete unreferenced chunks after 7 days # Delete unreferenced chunks after 7 days
default-retention-period = 7 * 24 * 60 * 60; default-retention-period = "6 months";
}; };
# Storage configuration # Storage configuration
@@ -57,7 +53,7 @@ in {
# Use local filesystem storage # Use local filesystem storage
type = "local"; type = "local";
# Store in /var/lib/atticd # Store in /var/lib/atticd
path = "/var/lib/atticd/storage"; path = "/var/lib/atticd-storage";
}; };
# Optional: S3-compatible storage (commented out) # Optional: S3-compatible storage (commented out)
@@ -70,7 +66,8 @@ in {
# Database configuration # Database configuration
database = { database = {
url = "postgresql://atticd@/atticd?host=/run/postgresql"; # url = "postgresql://atticd@/atticd?host=/run/postgresql";
url = "postgresql:///atticd?host=/run/postgresql&user=atticd";
}; };
# Compression # Compression
@@ -82,33 +79,34 @@ in {
}; };
}; };
# Create state directory with proper permissions # Create state directory with proper permissions
systemd.services.atticd = { # systemd.services.atticd = {
serviceConfig = { # serviceConfig = {
StateDirectory = "atticd"; # StateDirectory = "atticd";
StateDirectoryMode = "0750"; # StateDirectoryMode = "0750";
# Security hardening # # Security hardening
PrivateTmp = true; # PrivateTmp = true;
ProtectSystem = "strict"; # ProtectSystem = "strict";
ProtectHome = true; # ProtectHome = true;
NoNewPrivileges = true; # NoNewPrivileges = true;
RestrictNamespaces = true; # RestrictNamespaces = true;
RestrictRealtime = true; # RestrictRealtime = true;
RestrictSUIDSGID = true; # RestrictSUIDSGID = true;
LockPersonality = true; # LockPersonality = true;
ProtectProc = "invisible"; # ProtectProc = "invisible";
ProtectClock = true; # ProtectClock = true;
ProtectKernelLogs = true; # ProtectKernelLogs = true;
ProtectControlGroups = true; # ProtectControlGroups = true;
ProtectKernelModules = true; # ProtectKernelModules = true;
ProtectKernelTunables = true; # ProtectKernelTunables = true;
ProtectHostname = true; # ProtectHostname = true;
SystemCallFilter = "@system-service"; # SystemCallFilter = "@system-service";
SystemCallErrorNumber = "EPERM"; # SystemCallErrorNumber = "EPERM";
# Resource limits # # Resource limits
LimitNOFILE = 65536; # LimitNOFILE = 65536;
}; # };
}; # };
# Nginx reverse proxy configuration # Nginx reverse proxy configuration
services.nginx.virtualHosts."${atticHost}" = { services.nginx.virtualHosts."${atticHost}" = {
@@ -193,11 +191,18 @@ in {
services.postgresql.ensureDatabases = [ "atticd" ]; services.postgresql.ensureDatabases = [ "atticd" ];
services.postgresqlBackup.databases = [ "atticd" ]; services.postgresqlBackup.databases = [ "atticd" ];
services.borgbackup.jobs.default.exclude = [ services.borgbackup.jobs.default.exclude = [
"/var/lib/atticd" "/var/lib/atticd-storage"
]; ];
fileSystems."/var/lib/atticd/storage" = { systemd.tmpfiles.rules = [
"d /var/lib/atticd-storage 0755 atticd atticd -"
];
environment.systemPackages = [ pkgs.cifs-utils ];
fileSystems."/var/lib/atticd-storage" = {
device = "//u149513.your-backup.de/u149513-sub9/"; device = "//u149513.your-backup.de/u149513-sub9/";
fsType = "cifs"; fsType = "cifs";
options = let options = let

View File

@@ -24,6 +24,7 @@ in {
config = { config = {
services.blackbox-exporter = { services.blackbox-exporter = {
blacklistDomains = [ blacklistDomains = [
"attic.cloonar.com"
"autoconfig.cloonar.com" "autoconfig.cloonar.com"
"cloonar.dev" "cloonar.dev"
"loki.cloonar.com" "loki.cloonar.com"

File diff suppressed because one or more lines are too long

170
scripts/update-secrets-keys Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env bash
set -Euo pipefail
# Script to update sops keys for all secrets.yaml files in the project
# Uses .sops.yaml configuration to determine encryption keys
AGE_KEY_FILE=""
DRY_RUN=false
VERBOSE=false
# Parse options
while [[ $# -gt 0 ]]; do
case $1 in
-k|--age-key-file)
AGE_KEY_FILE="$2"
shift 2
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
cat <<EOF
Usage: $0 [OPTIONS]
Updates sops encryption keys for all secrets.yaml files in the project.
Uses .sops.yaml configuration to determine which keys to use.
OPTIONS:
-k, --age-key-file PATH Path to age key file for decryption (default: uses SOPS_AGE_KEY_FILE env var)
-n, --dry-run Show which files would be updated without making changes
-v, --verbose Show detailed output
-h, --help Show this help message
EXAMPLES:
# Update all secrets using default age key
$0
# Use specific age key file
$0 --age-key-file ~/.config/sops/age/keys.txt
# Dry run to see which files would be updated
$0 --dry-run --verbose
ENVIRONMENT:
SOPS_AGE_KEY_FILE Default age key file location (if -k not specified)
EOF
exit 0
;;
*)
echo "ERROR: Unknown option: $1" >&2
echo "Use --help for usage information." >&2
exit 1
;;
esac
done
# Check if 'sops' command is available
if ! command -v sops > /dev/null; then
echo "ERROR: 'sops' command not found. Please ensure it is installed and in your PATH." >&2
echo "Install with: nix-shell -p sops" >&2
exit 1
fi
# Determine the absolute directory where the script itself is located
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
PROJECT_ROOT=$(readlink -f "$SCRIPT_DIR/..")
# Check if .sops.yaml exists
SOPS_CONFIG="$PROJECT_ROOT/.sops.yaml"
if [ ! -f "$SOPS_CONFIG" ]; then
echo "ERROR: .sops.yaml not found at '$SOPS_CONFIG'" >&2
exit 1
fi
# Export age key file if provided
if [ -n "$AGE_KEY_FILE" ]; then
if [ ! -f "$AGE_KEY_FILE" ]; then
echo "ERROR: Age key file not found at '$AGE_KEY_FILE'" >&2
exit 1
fi
export SOPS_AGE_KEY_FILE="$AGE_KEY_FILE"
if [ "$VERBOSE" = true ]; then
echo "INFO: Using age key file: $AGE_KEY_FILE"
fi
elif [ -z "${SOPS_AGE_KEY_FILE:-}" ]; then
echo "WARNING: SOPS_AGE_KEY_FILE not set. You may not be able to decrypt secrets." >&2
echo " Use --age-key-file to specify a key file, or set SOPS_AGE_KEY_FILE environment variable." >&2
fi
# Find all secrets.yaml files
echo "INFO: Searching for secrets.yaml files in $PROJECT_ROOT..."
mapfile -t SECRET_FILES < <(find "$PROJECT_ROOT" -name "secrets.yaml" -type f | sort)
if [ ${#SECRET_FILES[@]} -eq 0 ]; then
echo "WARNING: No secrets.yaml files found in the project." >&2
exit 0
fi
echo "INFO: Found ${#SECRET_FILES[@]} secrets.yaml file(s)"
if [ "$VERBOSE" = true ] || [ "$DRY_RUN" = true ]; then
for file in "${SECRET_FILES[@]}"; do
relative_path="${file#$PROJECT_ROOT/}"
echo " - $relative_path"
done
fi
if [ "$DRY_RUN" = true ]; then
echo "INFO: Dry-run mode enabled. No files will be modified."
exit 0
fi
# Update keys for each file
UPDATED_COUNT=0
FAILED_COUNT=0
SKIPPED_COUNT=0
for file in "${SECRET_FILES[@]}"; do
relative_path="${file#$PROJECT_ROOT/}"
if [ "$VERBOSE" = true ]; then
echo "INFO: Updating keys for: $relative_path"
else
echo -n "Updating $relative_path... "
fi
# Check if file is encrypted with sops
if ! grep -q "^sops:" "$file" 2>/dev/null; then
echo "SKIP (not encrypted)"
((SKIPPED_COUNT++))
continue
fi
# Run sops updatekeys
if sops updatekeys --yes "$file" 2>&1 | {
if [ "$VERBOSE" = true ]; then
cat
else
grep -v "^$" || true
fi
}; then
if [ "$VERBOSE" != true ]; then
echo "OK"
fi
((UPDATED_COUNT++))
else
EXIT_STATUS=$?
echo "FAILED (exit code: $EXIT_STATUS)" >&2
((FAILED_COUNT++))
fi
done
# Summary
echo ""
echo "===== Summary ====="
echo "Total files found: ${#SECRET_FILES[@]}"
echo "Successfully updated: $UPDATED_COUNT"
echo "Failed: $FAILED_COUNT"
echo "Skipped: $SKIPPED_COUNT"
if [ $FAILED_COUNT -gt 0 ]; then
exit 1
fi
echo "INFO: All secrets.yaml files have been updated successfully."
exit 0

View File

@@ -0,0 +1,87 @@
{ config, lib, pkgs, ... }:
with lib;
let
cacheUrl = "https://attic.cloonar.com";
cacheName = "cloonar-nixos";
publicKey = "cloonar-nixos:u0S8Q3CShMkXeBk/eo8iooqrcSBTwNGBxQDS9HfkseE=";
authTokenFile = config.sops.secrets.attic_auth_token.path;
# Post-build hook script that pushes to Attic
atticPushHook = pkgs.writeShellScript "attic-push-hook" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
# Load configuration from sops secrets at runtime
ATTIC_CACHE="${cacheName}"
ATTIC_URL="${cacheUrl}"
# Check if we have the required configuration
if [[ -z "$ATTIC_CACHE" ]] || [[ -z "$ATTIC_URL" ]]; then
echo "Attic cache not configured, skipping push" >&2
exit 0
fi
# Read the auth token from sops if available
ATTIC_AUTH_TOKEN=$(cat "${authTokenFile}")
# Function to check if a path exists in cache
path_in_cache() {
local path="$1"
${pkgs.attic-client}/bin/attic cache info "$ATTIC_CACHE" "$path" &>/dev/null
}
# Function to push a path to cache
push_to_cache() {
local path="$1"
echo "Pushing $path to Attic cache..." >&2
if ${pkgs.attic-client}/bin/attic push "$ATTIC_CACHE" "$path"; then
echo "Successfully pushed $path" >&2
else
echo "Failed to push $path (non-fatal)" >&2
fi
}
# Read paths from stdin (provided by Nix post-build-hook)
while IFS= read -r path; do
if [[ -e "$path" ]]; then
# Check if already in cache before pushing
if ! path_in_cache "$path"; then
push_to_cache "$path"
else
echo "Path $path already in cache, skipping" >&2
fi
fi
done
echo "Attic cache push completed" >&2
'';
in {
sops.secrets.attic_auth_token = {
sopsFile = ./secrets.yaml;
};
# Install attic client
environment.systemPackages = with pkgs; [
attic-client
];
# Configure Nix settings
nix.settings = {
substituters = [ cacheUrl ];
trusted-public-keys = [ publicKey ];
post-build-hook = atticPushHook;
};
# Create a systemd service for manual cache operations
systemd.services.attic-push-closure = {
description = "Push a closure to Attic cache";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.attic-client}/bin/attic push ${cacheName} $CLOSURE_PATH'";
EnvironmentFile = authTokenFile;
};
};
}

View File

@@ -0,0 +1,34 @@
attic_auth_token: ENC[AES256_GCM,data:O9wRQe+llEvCE/9mx7VckgCY/5/ZryUFz+0qpgauFRsnNWiB31yOTXo1sOn1lPldGpfsSpUZnGDTLvg5S6mzZ9UYdhDTcSk6V+E9YV5wXLFJv6HGVVI7TVhkSSBIUrxx8sbQvC/hYQ+YQ0zzfreaIz7eMVbHgk+FNnNr3pFNcLYLTacugvMOyZDwkEJcKIFMcWj+zCGu90s3W7LutfudJ37LB4M9sU1Ifjj46NGTe3fAj+lmS1IyJ+2ZUlVoQd4pCbWB0wm3bTpwjJhDYhjQj5gJPuMjBQcCpP7uBvelcmBo+8V/LJ9HY6pRFxPlp48+tOwlGGrzb5WyqWPE3sP3F2eQj4EnlQoULrfi6ARO0xO4qs0FJhN2YhvHJYRyd9leNWNLIe1SdRQ9PK5ksvuoM1rTlbgrPotPYa1PkfmgFuWBMwI+hBf0+DMJtZxJpVES3WAcOuibZukeA5lvQ+AAFTpHRW8AiZF2ry3gWxStLsUrqNQTTt1gZQq6WrHbYbXr3DCuTXxqVLX4mXO1Slbm7JLxni7Sn5nCfUiKCAmFdxuL0L22RMa5yd9+7+wdcFJfhqu9pZ8U5KoTMuaJKnxp0KISog3gDVAfxkrrtfhLnHtJkkLB+/Aa3Ypqowle9iAq1I0IdH6Nzwl63C2nbqPafL5mcXkFMwPktHlkqrflUl/QKnJqBBvcgThdHZIbsQUq2xo589cpvDLouWL2xUHNpIqWotowF5m4n/iN53i6/cJayNpLWMEWWLtslXtG1CN7arjoYYJOuEqdzkqTjornSU7Q1kF6/eLgB8e2BVnMKfBT59F4sX2c6kuK0QXPohcpLWI1ZEYxnSv/44W0i/Ij5NvqZOQ1pEsyMIlPbuh37khw1gv+fMKrbEUUyquzHTX7DEGYEECzWjHQ3/WeDuRmiHlrZC/3StMf9888qm5v2yw/Vk5rQwNY1nbeTf8yWg47qKi2GgSSdsqrUrW5yWLs4MWKF2cSSDMC/kbgvGkHoS9KVI5dBGJhGuAf98tzOBO/UO39X0TjTzcay0AQ27/r8+QeIimviaZO41/GQOjoMzzSDHGxWEf2Nf/40nrM5Vcqj3I6hvRFE4u2m6jxCMqCqsIuy9avW8EuZyC6zJMoHSe/lUnYrx4tSUf3VhN85o6QSSJqfIFUN0jPQwNFpsz3X+A=,iv:X6xSygAtem7ekQruSZirdW/LKwf0kw+/Iq35wAcNyyQ=,tag:gRuPBxM5VeoJHimC6sbSow==,type:str]
sops:
age:
- recipient: age14grjcxaq4h55yfnjxvnqhtswxhj9sfdcvyas4lwvpa8py27pjy2sv3g6v7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2ZnRYdjlOZHB3ZWNkVG42
OWZDdGNjNmNoQmozeGlxSHBJSXNWTjc2VUE0ClZISko5d0piR3RRMGNNSVZjc2Rr
NnZHZWtCQytNeG5FeGQyRVBCcE9tdzAKLS0tIFdaVXBWK1ppYUpaQVJTUzVUOUR4
Nmd6KzN5Unh2bFdyaEd6aTluUEt6aFEKfxYq7UwQOwSGUpXnS8+8EsqA8mk3a2oT
eCssbrBsKvjkqOMvDNieBah5h5k13r5JJKHIENkJK6rhTUkvxJUdaw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1exny8unxynaw03yu8ppahu5z28uermghr8ag34e7kdqnaduq9stsyettzz
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuZDI4V2RBWUIvVzhEUzBX
Tng2MTAyOHl1TDhnVTY3OG1YUkZLem1TSGd3Ckorbi9wRTlodFZnVkpkUDgwdTRi
ZUZOUFFMUENzQzQvbk9XNnZsWVRIRjgKLS0tIGxoUUYvTUsyZ3BweGlrdzRhSUxr
YzRBbEZZMyt2VVpiTFJQUi8vS1BpMXcK8zmi27Kvp+0ujLQ+UEnq90bHjq5j+EWu
CnaKbqpQzCm+7TphBDR7tmUj2QoS2P0EXwub3DZtjZv6lnyMeD2DXQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1v6p8dan2t3w9h94fz4flldl32082j3s9x6zqq7u5j66keth9aphsd6pvch
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMldwQzhMOXhWcUYvSDFT
d29kRFNWZzZ2TkNIeFBTVk5HNjZMdmlOQWdBCkpmUjJDdHdXRzVZUzlVVGM2ak1l
dnlHMURTUjR3MEhwd2RLbGsvWkRIbncKLS0tIHNVQUl3QXBwUEM5ZzUyczlHUXA3
UzVENGtNSnZVcDQvR1hDR2oyZDh5KzAKhg+AQNdiJM/RvCdMNLH5er25U+yvcnM2
4Z0rOkkYsT6TerZHLllbm5AAyOLnKUn4PhZFMvKvGhVbc1Xg9t2XDg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-10-14T20:22:06Z"
mac: ENC[AES256_GCM,data:dt+rZ7GTlooTFhQOxRQvVpqKJksEJC5I5vsjSQ6GWPsi4EewGl2NY2gyjF6bVjYj6DHWuw/Kp79KGzJajmlYtQFdL54ydjaJUz4oMhoKO3xR4TxshW9XYEfOWavlMVqHHZQ6mPR1pyWQkonzwyni9ug8XmOJ0cN2OmZmKwdWzZQ=,iv:6AJocLlXZcNGG3nuXLc+ycfm6OA/oZOUFqFw4OoBetU=,tag:Qpa1RKS1/nqbDiAL5Jrb7w==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0