Compare commits

...
Author SHA1 Message Date
Richie 2d9dcc8a7e configuring builders for x86-64-v3 2026-09-02 07:28:20 -04:00
Richie 2882960a8e feat(gitea): replace GITEA_TOKEN with JEEVES_BOT_TOKEN in workflows and script
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 1m59s
test ebook search / test-ebook-search (push) Successful in 31s
build_systems / build-bob (push) Successful in 36s
test ebook search / test-ebook-search (pull_request) Successful in 35s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m15s
2026-08-29 09:38:36 -04:00
Richie bdf67e96f0 feat(monitoring): add Portal-1 and Vultr dashboards
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m14s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
build_systems / build-brain (push) Successful in 43s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 36s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-bob (push) Successful in 43s
build_systems / build-rhapsody-in-green (push) Successful in 55s
build_systems / build-jeeves (push) Successful in 2m7s
- scrape Portal-1 node metrics over Tailscale
- add a hardened Vultr billing and bandwidth exporter
- provision host health and infrastructure cost dashboards
2026-08-27 17:49:56 -04:00
Richie d95b8b99ce feat(jeeves): centralize firewall configuration
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m11s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 29s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 28s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-jeeves (push) Successful in 1m59s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 46s
- split networking into interface and firewall modules
- define named service ports in one location
- remove scattered service-level firewall rules
2026-08-27 16:59:44 -04:00
Richie 7e6908ef3d feat(networking): migrate host services from ZeroTier to Tailscale
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-brain (pull_request) Successful in 52s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 36s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-rhapsody-in-green (push) Successful in 49s
build_systems / build-jeeves (push) Successful in 2m2s
- replace legacy IP addresses with Tailscale MagicDNS names
- route Prometheus scraping and exporter firewall access through Tailscale
- wait for Tailscale before starting Prometheus
- restrict service UI ports to the Tailscale interface
- update PostgreSQL and Transmission access rules
- remove Jeeves ZeroTier configuration and Docker host overrides
2026-08-27 12:25:27 -04:00
Richie dd51e89abf fix(haproxy): use IPv6 wildcard for dual-stack listeners
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 34s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 35s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-bob (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
Bind HTTP, HTTPS, and Gitea SSH to [::] with v4v6 support.
2026-08-27 09:08:18 -04:00
Richie 677364fe74 removed zerotier.nix for common and removed zerotier from all systems except jeeves
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m15s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-bob (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-brain (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 1m58s
2026-08-26 13:11:34 -04:00
Richie f5a0038ab7 feat(portal): proxy Gitea SSH through HAProxy
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 31s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-bob (push) Successful in 37s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
- expose Gitea SSH through Portal on port 2223
- advertise the correct Gitea SSH user and domain
- add the Gitea host to Richie’s SSH config
- correct the local ACME challenge backend port
2026-08-26 10:43:21 -04:00
Richie b406b92622 fix(networking): restore Gitea HTTP port in allowedTCPPorts
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 33s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-jeeves (pull_request) Successful in 2m16s
2026-08-26 08:37:22 -04:00
Richie e0c14973b0 refactor(haproxy): consolidate ACME configuration
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m11s
build_systems / build-jeeves (pull_request) Failing after 7m19s
build_systems / build-brain (pull_request) Failing after 7m19s
test ebook search / test-ebook-search (pull_request) Failing after 7m18s
build_systems / build-bob (pull_request) Failing after 7m23s
pytest / pytest (pull_request) Failing after 7m26s
treefmt / nix fmt (pull_request) Failing after 7m21s
Merge the ACME module into the HAProxy configuration and start HAProxy only after certificates and Tailscale connectivity are available.
2026-08-26 08:28:41 -04:00
Richie 515cbae3f8 fix(audiobookshelf): updated host
test ebook search / test-ebook-search (pull_request) Failing after 7m8s
build_systems / build-jeeves (pull_request) Failing after 7m13s
pytest / pytest (pull_request) Failing after 7m17s
build_systems / build-bob (pull_request) Failing after 7m27s
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m13s
treefmt / nix fmt (pull_request) Failing after 7m12s
build_systems / build-brain (pull_request) Failing after 7m18s
2026-08-26 07:39:19 -04:00
Richie 219aea9b5f fix(acme) used the correct ip for acme 2026-08-26 07:33:31 -04:00
Richie df8908b708 feat(services): remove haproxy from validate_system.toml 2026-08-26 07:33:31 -04:00
Richie cb83fc6ea5 fix 2 imports 2026-08-26 07:33:31 -04:00
Richie 7f08a6f788 feat(portal): move edge proxy services from Jeeves
- move HAProxy and ACME certificate management to Portal
- route application backends to Jeeves over Tailscale
- restrict Jeeves backend ports to the Tailscale interface
- expose Gems for remote proxy access
2026-08-26 07:33:31 -04:00
Richie 8245d0f82e feat(tailscale): enable mesh networking across NixOS hosts
- enable Tailscale on Bob, Brain, Jeeves, and Rhapsody
- configure the Jeeves binary cache through Tailscale MagicDNS
- trust the existing workshop cache signing key
2026-08-26 07:33:31 -04:00
Richie 1c4e0c0828 renamed portal_1 to portal-1 2026-08-26 07:33:31 -04:00
Richie 5bc9ee5de9 feat(home): expand Richie’s minimal profile for Portal
- move essential CLI tools into the minimal package set
- remove duplicated packages from the full programs profile
- add the portal-1 SSH host on port 278
2026-08-26 07:33:31 -04:00
Richie 6fa93e1b96 feat(portal): add Vultr NixOS host configuration
- use the shared global base and Tailscale module
- configure Portal SSH and VM memory settings
- add Richie's minimal Home Manager profile
- document SOPS and nixos-anywhere deployment
2026-08-26 07:33:31 -04:00
Richie b8b0605279 refactor(nixos): make ZFS configuration optional
- move ZFS and snapshot manager settings out of the global base
- group snapshot files under common/optional/zfs
- enable the ZFS module on existing ZFS hosts
- make the global tmpfs setting overridable
2026-08-26 07:33:31 -04:00
Richie 03537310cb feat(tailscale): add Tailscale service configuration and update secrets 2026-08-26 07:33:31 -04:00
Richie a23c91c4ac feat(global): refactor configuration by moving settings to minimal.nix 2026-08-26 07:33:31 -04:00
Richie f24a14920e feat(flake): add disko to flake.nix for nixos-anywhere 2026-08-26 07:33:31 -04:00
Richie 48806c84fe fix(default.nix): remove permitted insecure packages
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 51s
build_systems / build-jeeves (pull_request) Successful in 2m15s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 32s
build_systems / build-jeeves (push) Successful in 2m1s
2026-08-25 16:13:38 -04:00
Richie 03c3b36f9c feat(home_assistant): add energy meter sensors filter
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-jeeves (pull_request) Successful in 2m14s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 1m59s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 37s
2026-08-25 12:11:06 -04:00
63 changed files with 2696 additions and 288 deletions
@@ -19,5 +19,5 @@ jobs:
python -m python.gitea_flake_lock merge
--repo "${{ github.repository }}"
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
run: nix flake update
- name: Create or update flake.lock PR
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com
run: >-
nix develop .#devShells.x86_64-linux.default -c
+5
View File
@@ -1,6 +1,9 @@
# Generate AGE keys from SSH keys with:
# ssh-keygen -A
# nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
# update keys after addin/removing a key
# nix-shell -p sops --run "sops updatekeys users/secrets.yaml" users/secrets.yaml
keys:
- &admin_richie age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c # cspell:disable-line
@@ -8,6 +11,7 @@ keys:
- &system_brain age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm # cspell:disable-line
- &system_jeeves age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv # cspell:disable-line
- &system_rhapsody age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4 # cspell:disable-line
- &system_portal_1 age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a # cspell:disable-line
creation_rules:
- path_regex: users/secrets\.yaml$
@@ -18,3 +22,4 @@ creation_rules:
- *system_brain
- *system_jeeves
- *system_rhapsody
- *system_portal_1
+1 -19
View File
@@ -17,16 +17,11 @@
./nix.nix
./programs.nix
./ssh.nix
./snapshot_manager.nix
];
boot = {
tmp.useTmpfs = true;
tmp.useTmpfs = lib.mkDefault true;
kernelPackages = lib.mkDefault pkgs.linuxPackages_6_12;
zfs = {
package = lib.mkDefault pkgs.zfs_2_4;
forceImportRoot = lib.mkDefault false;
};
};
hardware.enableRedistributableFirmware = true;
@@ -42,9 +37,6 @@
overlays = builtins.attrValues outputs.overlays;
config = {
allowUnfree = true;
permittedInsecurePackages = [
"openssl-1.1.1w" # This is for discord-canary
];
};
};
@@ -53,16 +45,6 @@
# firmware update
fwupd.enable = true;
snapshot_manager = {
enable = lib.mkDefault true;
PYTHONPATH = "${inputs.self}/";
};
zfs = {
trim.enable = lib.mkDefault true;
autoScrub.enable = lib.mkDefault true;
};
};
powerManagement.powertop.enable = lib.mkDefault true;
+1 -1
View File
@@ -5,7 +5,7 @@
...
}:
let
monitoringInterface = "ztwfunumly";
monitoringInterface = "tailscale0";
nodeTextfileDir = "/var/lib/prometheus-node-exporter-textfile";
mkProcessNameTemplate =
+37
View File
@@ -0,0 +1,37 @@
{
config,
inputs,
...
}:
{
nix.settings = {
trusted-substituters = [ "http://jeeves:5000" ];
substituters = [ "http://jeeves:5000/?priority=1&want-mass-query=true" ];
trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ];
};
services.tailscale = {
enable = true;
openFirewall = true;
authKeyFile = config.sops.secrets.tailscale_auth_key.path;
# OAuth client secrets create ephemeral nodes by default. NixOS machines
# are persistent and should enroll without interactive device approval.
authKeyParameters = {
ephemeral = false;
preauthorized = true;
};
extraUpFlags = [ "--advertise-tags=tag:nixos" ];
};
sops = {
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
secrets.tailscale_auth_key = {
sopsFile = "${inputs.self}/users/secrets.yaml";
owner = "root";
mode = "0400";
};
};
}
-11
View File
@@ -1,11 +0,0 @@
{
services.zerotierone = {
enable = true;
joinNetworks = [ "e4da7455b2ae64ca" ];
};
nix.settings = {
trusted-substituters = [ "http://192.168.90.40:5000" ];
substituters = [ "http://192.168.90.40:5000/?priority=1&want-mass-query=true" ];
trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ];
};
}
+26
View File
@@ -0,0 +1,26 @@
{
inputs,
lib,
pkgs,
...
}:
{
imports = [ ./snapshot.nix ];
boot.zfs = {
package = lib.mkDefault pkgs.zfs_2_4;
forceImportRoot = lib.mkDefault false;
};
services = {
snapshot_manager = {
enable = lib.mkDefault true;
PYTHONPATH = "${inputs.self}/";
};
zfs = {
trim.enable = lib.mkDefault true;
autoScrub.enable = lib.mkDefault true;
};
};
}
Generated
+21
View File
@@ -1,5 +1,25 @@
{
"nodes": {
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781152676,
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
"owner": "nix-community",
"repo": "disko",
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"firefox-addons": {
"inputs": {
"nixpkgs": [
@@ -124,6 +144,7 @@
},
"root": {
"inputs": {
"disko": "disko",
"firefox-addons": "firefox-addons",
"home-manager": "home-manager",
"nixos-hardware": "nixos-hardware",
+5
View File
@@ -35,6 +35,11 @@
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
+2 -2
View File
@@ -37,7 +37,7 @@ class RerankConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="EBOOK_SEARCH_RERANK_", frozen=True, protected_namespaces=())
enabled: bool = True
base_url: str = "http://192.168.90.25:8001"
base_url: str = "http://bob:8001"
model: str = "qwen3-reranker-06b"
candidates: int = 24
timeout_seconds: float = 30.0
@@ -67,7 +67,7 @@ class EbookSearchConfig(BaseSettings):
)
chat_model: str = "deepseek-v4-flash"
answer_enabled: bool = True
embedding_base_url: str = "http://192.168.90.25:8000/v1"
embedding_base_url: str = "http://bob:8000/v1"
embedding_api_key: str = "not-needed"
embedding_model: str = "qwen3-embedding-0.6b"
embedding_batch_size: int = 32
@@ -9,8 +9,6 @@ services:
restart: unless-stopped
ports:
- "${EBOOK_SEARCH_PORT:-8070}:8070"
extra_hosts:
- "jeeves:192.168.90.40"
env_file:
- ../../../.env
environment:
+5 -5
View File
@@ -84,13 +84,13 @@ def push_branch(*, branch: str) -> None:
run_cmd(["git", "push", "origin", f"HEAD:{branch}", "--force"])
def _required_gitea_token() -> str:
def _required_jeeves_bot_token() -> str:
"""Read the required Gitea token from the environment."""
token = getenv("GITEA_TOKEN")
token = getenv("JEEVES_BOT_TOKEN")
if token:
return token
msg = "GITEA_TOKEN environment variable is required"
msg = "JEEVES_BOT_TOKEN environment variable is required"
raise RuntimeError(msg)
@@ -111,7 +111,7 @@ def update(
owner, repo_name = split_repo_name(repo)
with GiteaClient(
base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL),
token=_required_gitea_token(),
token=_required_jeeves_bot_token(),
) as client:
pull_request = ensure_flake_lock_pull_request(
client,
@@ -134,7 +134,7 @@ def merge(
owner, repo_name = split_repo_name(repo)
with GiteaClient(
base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL),
token=_required_gitea_token(),
token=_required_jeeves_bot_token(),
) as client:
pull_request = find_flake_lock_pull_request(client, owner=owner, repo=repo_name)
if not pull_request:
+4 -3
View File
@@ -6,15 +6,16 @@
"${inputs.self}/users/steve"
"${inputs.self}/common/global"
"${inputs.self}/common/optional/docker.nix"
"${inputs.self}/common/optional/scanner.nix"
"${inputs.self}/common/optional/monitoring-agent.nix"
"${inputs.self}/common/optional/nvidia.nix"
"${inputs.self}/common/optional/scanner.nix"
"${inputs.self}/common/optional/steam.nix"
"${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/yubikey.nix"
"${inputs.self}/common/optional/zerotier.nix"
"${inputs.self}/common/optional/nvidia.nix"
"${inputs.self}/common/optional/zfs"
./hardware.nix
./syncthing.nix
./llms.nix
+2 -1
View File
@@ -7,8 +7,9 @@
"${inputs.self}/common/optional/ssh_decrypt.nix"
"${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/zerotier.nix"
"${inputs.self}/common/optional/zfs"
./docker
./hardware.nix
./programs.nix
+2 -1
View File
@@ -21,6 +21,7 @@
packages = {
victron_modbuss = "!include ${./home_assistant/victron_modbuss.yaml}";
battery_sensors = "!include ${./home_assistant/battery_sensors.yaml}";
energy_meters = "!include ${./home_assistant/energy_meters.yaml}";
gps_location = "!include ${./home_assistant/gps_location.yaml}";
heater = "!include ${./home_assistant/heater.yaml}";
van_weather = "!include ${./home_assistant/van_weather_template.yaml}";
@@ -85,7 +86,7 @@
esphome = {
enable = true;
openFirewall = true;
address = "192.168.90.35";
address = "0.0.0.0";
};
};
}
@@ -0,0 +1,71 @@
template:
- sensor:
- name: "JK0 Energy In Clean"
unique_id: jk0_energy_in_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.jk0_energy_in')) }}
state: >-
{{ states('sensor.jk0_energy_in') | float }}
- name: "JK0 Energy Out Clean"
unique_id: jk0_energy_out_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.jk0_energy_out')) }}
state: >-
{{ states('sensor.jk0_energy_out') | float }}
- name: "JK1 Energy In Clean"
unique_id: jk1_energy_in_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.jk1_energy_in')) }}
state: >-
{{ states('sensor.jk1_energy_in') | float }}
- name: "JK1 Energy Out Clean"
unique_id: jk1_energy_out_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.jk1_energy_out')) }}
state: >-
{{ states('sensor.jk1_energy_out') | float }}
- name: "Solar Yield Clean"
unique_id: solar_yield_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.solar_yield_daily')) }}
state: >-
{{ states('sensor.solar_yield_daily') | float }}
- name: "DC Load Energy Clean"
unique_id: dc_load_energy_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.dc_load_energy_daily')) }}
state: >-
{{ states('sensor.dc_load_energy_daily') | float }}
- name: "DC Charger Energy Clean"
unique_id: dc_charger_energy_clean
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
availability: >-
{{ is_number(states('sensor.dc_charger_energy')) }}
state: >-
{{ states('sensor.dc_charger_energy') | float }}
+2 -2
View File
@@ -19,7 +19,7 @@
local all richie trust
host all richie 127.0.0.1/32 trust
host all richie ::1/128 trust
host all richie 192.168.90.1/24 trust
host all richie 100.64.0.0/10 trust
host all richie 192.168.99.1/24 trust
local vaninventory vaninventory trust
@@ -28,7 +28,7 @@
local hass hass trust
# ipv4
host hass hass 192.168.90.1/24 trust
host hass hass 100.64.0.0/10 trust
host hass hass 127.0.0.1/32 trust
# ipv6
+1 -1
View File
@@ -3,7 +3,7 @@
services.syncthing = {
overrideFolders = false;
guiAddress = "192.168.90.35:8384";
guiAddress = "0.0.0.0:8384";
settings = {
"dotfiles" = {
path = "/home/richie/dotfiles";
+13 -5
View File
@@ -1,4 +1,4 @@
{ inputs, ... }:
{ inputs, lib, ... }:
let
vars = import ./vars.nix;
in
@@ -13,19 +13,21 @@ in
"${inputs.self}/common/optional/monitoring-agent.nix"
"${inputs.self}/common/optional/ssh_decrypt.nix"
"${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/zerotier.nix"
"${inputs.self}/common/optional/zfs"
./monitoring
./docker
./services
./web_services
./hardware.nix
./networking.nix
./networking
./programs.nix
./runners
./syncthing.nix
];
nix.settings.system-features = lib.mkAfter [ "gccarch-x86-64-v3" ];
services = {
openssh.ports = [ 629 ];
@@ -36,7 +38,13 @@ in
EnvironmentFile = "${vars.secrets}/services/snapshot_manager";
};
zerotierone.joinNetworks = [ "a09acf02330d37b9" ];
zerotierone = {
enable = true;
joinNetworks = [
"e4da7455b2ae64ca"
"a09acf02330d37b9"
];
};
};
users.groups = {
@@ -2,9 +2,6 @@ let
vars = import ../vars.nix;
in
{
networking.firewall.allowedTCPPorts = [
8989
];
virtualisation.oci-containers.containers.signal_cli_rest_api = {
image = "bbernhard/signal-cli-rest-api:0.199-dev";
ports = [
+33
View File
@@ -0,0 +1,33 @@
# Monitoring
## Vultr API metrics
The `vultr-exporter` service reads its API token from:
```text
/zfs/storage/secrets/services/vultr-exporter
```
Create the file on Jeeves as root with the following contents:
```text
API_KEY=<Vultr API token>
```
The token needs read access to the Vultr Account and Billing APIs. Unrelated
resource collectors are disabled in the packaged exporter.
Restrict the file to root and ensure the public egress IP used by Jeeves is
allowed for the token in the Vultr API settings:
```console
sudo chown root:root /zfs/storage/secrets/services/vultr-exporter
sudo chmod 600 /zfs/storage/secrets/services/vultr-exporter
```
The exporter listens on `127.0.0.1:9188`; it is scraped by the local
`prometheus-main` service every five minutes and is not exposed through the
host firewall.
Portal-1 exposes its node exporter only through `tailscale0` on port `9100`.
Jeeves reaches it using the Portal-1 Tailscale hostname.
@@ -0,0 +1,798 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"mappings": [
{
"options": {
"0": {
"color": "red",
"index": 1,
"text": "Offline"
},
"1": {
"color": "green",
"index": 0,
"text": "Online"
}
},
"type": "value"
}
]
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 4,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "up{job=\"node\",instance=\"portal-1\"}",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Node Exporter",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 4,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - avg(rate(node_cpu_seconds_total{job=\"node\",instance=\"portal-1\",mode=\"idle\"}[5m])))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "CPU Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 9,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_memory_MemAvailable_bytes{job=\"node\",instance=\"portal-1\"} / node_memory_MemTotal_bytes{job=\"node\",instance=\"portal-1\"}))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "RAM Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 14,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_filesystem_avail_bytes{job=\"node\",instance=\"portal-1\",mountpoint=\"/\",fstype!=\"\"} / node_filesystem_size_bytes{job=\"node\",instance=\"portal-1\",mountpoint=\"/\",fstype!=\"\"}))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Root Filesystem Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 19,
"y": 0
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "node_load1{job=\"node\",instance=\"portal-1\"}",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Load (1m)",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - avg(rate(node_cpu_seconds_total{job=\"node\",instance=\"portal-1\",mode=\"idle\"}[5m])))",
"instant": false,
"legendFormat": "CPU used",
"range": true,
"refId": "A"
}
],
"title": "CPU Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"id": 7,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_memory_MemAvailable_bytes{job=\"node\",instance=\"portal-1\"} / node_memory_MemTotal_bytes{job=\"node\",instance=\"portal-1\"}))",
"instant": false,
"legendFormat": "RAM used",
"range": true,
"refId": "A"
}
],
"title": "RAM Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 13
},
"id": 8,
"options": {
"cellHeight": "sm",
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Value"
}
]
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_filesystem_avail_bytes{job=\"node\",instance=\"portal-1\",fstype!~\"tmpfs|devtmpfs|overlay|squashfs\",mountpoint!~\"/run.*\"} / node_filesystem_size_bytes{job=\"node\",instance=\"portal-1\",fstype!~\"tmpfs|devtmpfs|overlay|squashfs\",mountpoint!~\"/run.*\"}))",
"format": "table",
"instant": true,
"legendFormat": "{{mountpoint}}",
"range": false,
"refId": "A"
}
],
"title": "Filesystem Usage",
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "Bps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 13
},
"id": 9,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_read_bytes_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "read",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_written_bytes_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "write",
"range": true,
"refId": "B"
}
],
"title": "Disk Throughput",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "iops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 21
},
"id": 10,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_reads_completed_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "reads",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_writes_completed_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "writes",
"range": true,
"refId": "B"
}
],
"title": "Disk Operations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "Bps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 21
},
"id": 11,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_network_receive_bytes_total{job=\"node\",instance=\"portal-1\",device!=\"lo\"}[5m]))",
"instant": false,
"legendFormat": "received",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_network_transmit_bytes_total{job=\"node\",instance=\"portal-1\",device!=\"lo\"}[5m]))",
"instant": false,
"legendFormat": "sent",
"range": true,
"refId": "B"
}
],
"title": "Network Traffic",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"monitoring",
"portal-1",
"vultr"
],
"templating": {
"list": []
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Portal-1 Host",
"uid": "portal-1-host",
"version": 1,
"weekStart": ""
}
File diff suppressed because it is too large Load Diff
+21 -10
View File
@@ -41,29 +41,36 @@ let
{
job_name = "node";
static_configs = [
(mkTarget "jeeves" "192.168.90.40:9100")
(mkTarget "bob" "192.168.90.25:9100")
(mkTarget "jeeves" "jeeves.taile39c31.ts.net:9100")
(mkTarget "bob" "bob.taile39c31.ts.net:9100")
(mkTarget "portal-1" "portal-1.taile39c31.ts.net:9100")
];
}
{
job_name = "vultr";
scrape_interval = "5m";
scrape_timeout = "45s";
static_configs = [ (mkTarget "vultr" "127.0.0.1:9188") ];
}
{
job_name = "process_grouped";
static_configs = [
(mkTarget "jeeves" "192.168.90.40:9256")
(mkTarget "bob" "192.168.90.25:9256")
(mkTarget "jeeves" "jeeves.taile39c31.ts.net:9256")
(mkTarget "bob" "bob.taile39c31.ts.net:9256")
];
}
{
job_name = "smartctl";
static_configs = [
(mkTarget "jeeves" "192.168.90.40:9633")
(mkTarget "bob" "192.168.90.25:9633")
(mkTarget "jeeves" "jeeves.taile39c31.ts.net:9633")
(mkTarget "bob" "bob.taile39c31.ts.net:9633")
];
}
{
job_name = "zfs";
static_configs = [
(mkTarget "jeeves" "192.168.90.40:9134")
(mkTarget "bob" "192.168.90.25:9134")
(mkTarget "jeeves" "jeeves.taile39c31.ts.net:9134")
(mkTarget "bob" "bob.taile39c31.ts.net:9134")
];
}
];
@@ -79,8 +86,8 @@ let
{
job_name = "process_pid";
static_configs = [
(mkTarget "jeeves" "192.168.90.40:9257")
(mkTarget "bob" "192.168.90.25:9257")
(mkTarget "jeeves" "jeeves.taile39c31.ts.net:9257")
(mkTarget "bob" "bob.taile39c31.ts.net:9257")
];
}
];
@@ -97,7 +104,9 @@ let
after = [
"zfs-media-database-prometheus.mount"
"network.target"
"tailscaled-autoconnect.service"
];
wants = [ "tailscaled-autoconnect.service" ];
requires = [ "zfs-media-database-prometheus.mount" ];
wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = [ dataDir ];
@@ -151,6 +160,8 @@ let
};
in
{
imports = [ ./vultr-exporter.nix ];
users = {
groups.prometheus = { };
users.prometheus = {
@@ -0,0 +1,96 @@
{
lib,
pkgs,
...
}:
let
vars = import ../vars.nix;
vultrExporter = pkgs.buildGoModule rec {
pname = "vultr-exporter";
version = "0-unstable-2025-10-14";
src = pkgs.fetchFromGitHub {
owner = "DazWilkin";
repo = "vultr-exporter";
rev = "d8b5d7107c2eda31566e6e895c8a67db32fdaf68";
hash = "sha256-3LpXh+mFMBRJfiY2ftELpM3AIE0LiZ9c6NFok0OeH5I=";
};
vendorHash = "sha256-qCHWmg6GxLIjqlf5UqRcuDRd1xqRFI3sl4ZduLajx+Y=";
subPackages = [ "cmd/server" ];
postPatch = ''
substituteInPlace cmd/server/main.go \
--replace-fail 'registry.MustRegister(collector.NewBlockStorageCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewKubernetesCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewLoadBalancerCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewReservedIPsCollector(s, client, log))' ""
'';
ldflags = [
"-s"
"-w"
"-X main.GitCommit=${src.rev}"
"-X main.OSVersion=NixOS"
];
postInstall = ''
mv $out/bin/server $out/bin/vultr-exporter
'';
meta = {
description = "Prometheus exporter for Vultr account, billing, and bandwidth metrics";
homepage = "https://github.com/DazWilkin/vultr-exporter";
license = lib.licenses.asl20;
mainProgram = "vultr-exporter";
};
};
in
{
systemd.services.vultr-exporter = {
description = "Vultr Prometheus exporter";
documentation = [ "https://github.com/DazWilkin/vultr-exporter" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe vultrExporter} --endpoint=127.0.0.1:9188 --path=/metrics";
EnvironmentFile = "${vars.secrets}/services/vultr-exporter";
Restart = "on-failure";
RestartSec = "30s";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
}
+16
View File
@@ -0,0 +1,16 @@
{
imports = [
./firewall.nix
./interfaces.nix
];
networking = {
hostName = "jeeves";
hostId = "0e15ce35";
useNetworkd = true;
# Without this, unconfigured NICs get a catch-all DHCP network and
# join the LAN uninvited (how the dead atlantic card ended up
# ARP-poisoning 192.168.99.14). Every interface must be configured
# explicitly below.
useDHCP = false;
};
}
+47
View File
@@ -0,0 +1,47 @@
let
ports = {
audiobookshelf = 8000;
ebook_searcher = 8070;
gems = 8002;
gitea_http = 6443;
gitea_ssh = 2223;
grafana = 3000;
jellyfin_http = 8096;
nix_binary_cache = 5000;
nornsight = 8001;
ollama = 11434;
open_webui = 8080;
postgresql = 5432;
share = 8091;
signal_cli = 8989;
syncthing = 8384;
};
in
{
networking.firewall = {
enable = true;
interfaces.br-nix-builder = {
allowedTCPPorts = [ ];
allowedUDPPorts = [ ];
};
interfaces.tailscale0.allowedTCPPorts = [
ports.audiobookshelf
ports.gems
ports.jellyfin_http
ports.nix_binary_cache
ports.nornsight
ports.share
];
allowedTCPPorts = [
ports.ebook_searcher
ports.gitea_http
ports.gitea_ssh
ports.grafana
ports.ollama
ports.open_webui
ports.postgresql
ports.signal_cli
ports.syncthing
];
};
}
@@ -8,27 +8,6 @@
"net.bridge.bridge-nf-call-iptables" = 0;
};
networking = {
hostName = "jeeves";
hostId = "0e15ce35";
firewall = {
enable = true;
interfaces.br-nix-builder = {
allowedTCPPorts = [ ];
allowedUDPPorts = [ ];
};
allowedTCPPorts = [
8070
];
};
useNetworkd = true;
# Without this, unconfigured NICs get a catch-all DHCP network and
# join the LAN uninvited (how the dead atlantic card ended up
# ARP-poisoning 192.168.99.14). Every interface must be configured
# explicitly below.
useDHCP = false;
};
systemd.network = {
enable = true;
wait-online = {
+1
View File
@@ -83,6 +83,7 @@ in
useHostResolvConf = false;
};
nix.settings = {
system-features = lib.mkAfter [ "gccarch-x86-64-v3" ];
trusted-substituters = [
"https://cache.nixos.org"
"https://cache.tmmworkshop.com"
@@ -6,6 +6,7 @@ in
services.audiobookshelf = {
enable = true;
port = 8000;
host = "0.0.0.0";
};
systemd.services.audiobookshelf.serviceConfig.WorkingDirectory =
lib.mkForce "${vars.docker_configs}/audiobookshelf";
+1 -1
View File
@@ -36,7 +36,7 @@ in
Type = "simple";
User = "gems";
Group = "gems";
ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 127.0.0.1 --port 8002";
ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 0.0.0.0 --port 8002";
Restart = "on-failure";
RestartSec = "5s";
StandardOutput = "journal";
+5 -6
View File
@@ -2,11 +2,6 @@ let
vars = import ../vars.nix;
in
{
networking.firewall.allowedTCPPorts = [
6443
2223
];
services.gitea = {
enable = true;
appName = "TMM Workshop";
@@ -27,10 +22,14 @@ in
};
service.DISABLE_REGISTRATION = true;
server = {
DOMAIN = "tmmworkshop.com";
DOMAIN = "gitea.tmmworkshop.com";
ROOT_URL = "https://gitea.tmmworkshop.com/";
HTTP_PORT = 6443;
BUILTIN_SSH_SERVER_USER = "gitea";
SSH_USER = "gitea";
SSH_DOMAIN = "gitea.tmmworkshop.com";
SSH_PORT = 2223;
SSH_LISTEN_HOST = "0.0.0.0";
SSH_LISTEN_PORT = 2223;
START_SSH_SERVER = true;
PUBLIC_URL_DETECTION = "auto";
+2 -4
View File
@@ -6,8 +6,6 @@ let
grafanaDataDir = "${vars.services}/grafana";
in
{
networking.firewall.allowedTCPPorts = [ 3000 ];
services.grafana = {
enable = true;
dataDir = grafanaDataDir;
@@ -19,9 +17,9 @@ in
secret_key = "$__file{${vars.secrets}/services/grafana/secret_key}";
};
server = {
http_addr = "192.168.90.40";
http_addr = "0.0.0.0";
http_port = 3000;
root_url = "http://192.168.90.40:3000/";
root_url = "http://jeeves:3000/";
};
};
provision = {
-1
View File
@@ -4,7 +4,6 @@ in
{
services.jellyfin = {
enable = true;
openFirewall = true;
dataDir = "${vars.services}/jellyfin";
cacheDir = "${vars.services}/jellyfin/cache";
};
-1
View File
@@ -38,7 +38,6 @@ in
"qwen3.5:35b"
];
modelsDir = vars.ollama;
openFirewall = true;
};
systemd.services = {
ollama.serviceConfig = {
-1
View File
@@ -7,6 +7,5 @@ in
package = pkgs.nix-serve-ng;
enable = true;
secretKeyFile = "${vars.secrets}/services/nix-cache/cache-priv-key.pem";
openFirewall = true;
};
}
-1
View File
@@ -2,7 +2,6 @@
services.open-webui = {
enable = true;
host = "0.0.0.0";
openFirewall = true;
environment = {
ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True";
+3 -5
View File
@@ -3,8 +3,6 @@ let
vars = import ../vars.nix;
in
{
networking.firewall.allowedTCPPorts = [ 5432 ];
# Symlink pg_wal to a ZFS dataset on the special (metadata) vdev for fast WAL writes
# this is required for systemd sandboxing
systemd.services.postgresql.serviceConfig.ReadWritePaths = [ "/zfs/media/database/postgres-wal" ];
@@ -27,7 +25,7 @@ in
local all richie trust
host all richie 127.0.0.1/32 trust
host all richie ::1/128 trust
host all richie 192.168.90.1/24 trust
host all richie 100.64.0.0/10 trust
host all richie 192.168.99.1/24 trust
host all richie 172.16.0.0/12 trust
@@ -42,12 +40,12 @@ in
local postgres math trust
host postgres math 127.0.0.1/32 trust
host postgres math ::1/128 trust
host postgres math 192.168.90.1/24 trust
host postgres math 100.64.0.0/10 trust
local data_science_dev math trust
host data_science_dev math 127.0.0.1/32 trust
host data_science_dev math ::1/128 trust
host data_science_dev math 192.168.90.1/24 trust
host data_science_dev math 100.64.0.0/10 trust
'';
identMap = ''
+2 -2
View File
@@ -21,11 +21,11 @@ in
message-level = 3;
peer-port = 51413;
rpc-bind-address = "0.0.0.0";
rpc-host-whitelist = "127.0.0.1,192.168.90.40";
rpc-host-whitelist = "127.0.0.1,jeeves,jeeves.taile39c31.ts.net";
rpc-host-whitelist-enabled = true;
rpc-port = 9091;
rpc-whitelist-enabled = true;
rpc-whitelist = "127.0.0.1,192.168.90.49";
rpc-whitelist = "127.0.0.1,100.*.*.*";
seed-queue-enabled = false;
};
};
@@ -1,7 +1,6 @@
zpool = ["root_pool", "storage", "media"]
services = [
"audiobookshelf",
"haproxy",
"docker",
"jellyfin",
]
+1 -3
View File
@@ -2,11 +2,9 @@ let
vars = import ./vars.nix;
in
{
networking.firewall.allowedTCPPorts = [ 8384 ];
services.syncthing = {
overrideFolders = false;
guiAddress = "192.168.90.40:8384";
guiAddress = "0.0.0.0:8384";
settings = {
devices.davids-server.id = "7GXTDGR-AOXFW2O-K6J7NM3-XYZNRRW-AKHAFWM-GBOWUPQ-OA6JIWD-ER7RDQL"; # cspell:disable-line
folders = {
-75
View File
@@ -1,75 +0,0 @@
let
domains = [
"audiobookshelf"
"cache"
"gitea"
"gems"
"jellyfin"
"share"
];
extraDomains = [ "www.norn-sight.com" ];
makeCert = name: {
name = "${name}.tmmworkshop.com";
value = {
webroot = "/var/lib/acme/.challenges";
group = "acme";
reloadServices = [ "haproxy.service" ];
};
};
makeExtraCert = name: {
inherit name;
value = {
webroot = "/var/lib/acme/.challenges";
group = "acme";
reloadServices = [ "haproxy.service" ];
};
};
acmeServices =
map (domain: "acme-${domain}.tmmworkshop.com.service") domains
++ map (domain: "acme-${domain}.service") extraDomains;
in
{
users.users.haproxy.extraGroups = [ "acme" ];
security.acme = {
acceptTerms = true;
defaults.email = "Richie@tmmworkshop.com";
certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains));
};
# Minimal nginx to serve ACME HTTP-01 challenge files for HAProxy
services.nginx = {
enable = true;
virtualHosts."acme-challenge" = {
listen = [
{
addr = "127.0.0.1";
port = 8402;
}
];
locations."/.well-known/acme-challenge/" = {
root = "/var/lib/acme/.challenges";
};
};
};
# Ensure the challenge directory exists with correct permissions
systemd.tmpfiles.rules = [
"d /var/lib/acme/.challenges 0750 acme acme - -"
"d /var/lib/acme/.challenges/.well-known 0750 acme acme - -"
"d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -"
];
users.users.nginx.extraGroups = [ "acme" ];
# HAProxy needs certs to exist before it can bind :443.
# NixOS's acme module generates self-signed placeholders on first boot
# via acme-<domain>.service — just make HAProxy wait for them.
systemd.services.haproxy = {
after = acmeServices;
wants = acmeServices;
};
}
-9
View File
@@ -1,9 +0,0 @@
{ lib, ... }:
{
imports =
let
files = builtins.attrNames (builtins.readDir ./.);
nixFiles = builtins.filter (name: lib.hasSuffix ".nix" name && name != "default.nix") files;
in
map (file: ./. + "/${file}") nixFiles;
}
-14
View File
@@ -1,14 +0,0 @@
{
networking.firewall.allowedTCPPorts = [
80
443
];
# Global robots.txt served by HAProxy for every vhost (see haproxy.cfg).
environment.etc."haproxy/robots.txt".source = ./robots.txt;
services.haproxy = {
enable = true;
config = builtins.readFile ./haproxy.cfg;
};
}
+83
View File
@@ -0,0 +1,83 @@
# portal_1
Minimal NixOS target for a Vultr VM, installed with nixos-anywhere. The Nix
flake target is `portal_1`; the machine hostname is `portal-1` because DNS
hostnames cannot contain underscores.
## Before deploying
1. Confirm the VM's system disk is `/dev/vda`. If it is not, update both
references in `disk-config.nix`.
2. Confirm the SSH public key in `default.nix` is the key that should have
administrator access.
3. Boot the VM into a NixOS installer or another nixos-anywhere-compatible
Linux rescue environment with root SSH access. Keep this environment
running while completing the SOPS bootstrap below.
## Bootstrap SOPS
Use the rescue environment's SSH host key as the permanent portal identity.
Replace `VM_IP` below:
```console
ssh root@VM_IP 'cat /etc/ssh/ssh_host_ed25519_key.pub' | \
nix shell nixpkgs#ssh-to-age --command ssh-to-age
```
This prints an `age1...` recipient; it does not copy the private key. Add the
recipient to `.sops.yaml`:
```yaml
- &system_portal_1 age1...
```
Then add `*system_portal_1` to the age recipients for
`users/secrets.yaml`. Re-encrypt the existing file for the new recipient and
add the Tailscale key:
```console
nix shell nixpkgs#sops --command sops updatekeys users/secrets.yaml
nix shell nixpkgs#sops --command sops users/secrets.yaml
```
Add the OAuth client secret from the `Auth Keys: Write` credential in the SOPS
editor and save it:
```yaml
tailscale_auth_key: tskey-client-...
```
## Deploy
From the repository root, replace `VM_IP` with the VM's public IP:
```console
nix run github:nix-community/nixos-anywhere -- \
--copy-host-keys --flake .#portal_1 root@VM_IP
```
This repartitions `/dev/vda`, so anything already on that disk is erased. The
layout reserves 8 GiB for swap and assigns the remaining space to the root
filesystem.
`--copy-host-keys` preserves the same private SSH host key at
`/etc/ssh/ssh_host_ed25519_key` on the installed system. SOPS-Nix converts that
key to an age identity during activation. After the reboot, connect as
`richie` and verify that automatic Tailscale enrollment succeeded:
```console
ssh -p 278 richie@VM_IP
sudo tailscale status
```
The installed OpenSSH service listens on port 278. Port 22 is served by
Endlessh and will not provide an SSH login.
HAProxy uses the same frontend, routing, and rate-limiting configuration as
Jeeves. Portal manages the ACME certificates for the existing public domains;
their DNS records must resolve to Portal for HTTP-01 issuance and renewal.
The application backends still use Jeeves' original `127.0.0.1` addresses.
Replace them with the corresponding Tailscale addresses before directing
application traffic through Portal. Ports 80 and 443 are allowed through the
firewall.
+57
View File
@@ -0,0 +1,57 @@
{
inputs,
lib,
pkgs,
...
}:
{
imports = [
inputs.disko.nixosModules.disko
"${inputs.self}/users/richie"
"${inputs.self}/common/global"
"${inputs.self}/common/optional/tailscale.nix"
./disk-config.nix
./haproxy
./monitoring.nix
];
nixpkgs.hostPlatform = "x86_64-linux";
boot = {
# Avoid consuming the VM's limited memory for /tmp.
tmp.useTmpfs = false;
# The Vultr system disk and NIC are exposed as virtio devices.
initrd.availableKernelModules = [
"virtio_pci"
"virtio_blk"
"virtio_scsi"
"sd_mod"
];
};
networking = {
hostName = "portal-1";
useDHCP = lib.mkDefault true;
firewall = {
enable = true;
allowedTCPPorts = [ 278 ];
};
};
services.openssh.ports = [ 278 ];
environment.systemPackages = with pkgs; [
curl
htop
vim
];
# The VM uses its regular swap instead of compressed RAM swap.
zramSwap.enable = false;
time.timeZone = "Etc/UTC";
system.stateVersion = "24.05";
}
+59
View File
@@ -0,0 +1,59 @@
{ ... }:
{
# Vultr's first virtio disk is normally /dev/vda. Change this before
# deployment if the selected image exposes its system disk differently.
disko.devices.disk.main = {
type = "disk";
device = "/dev/vda";
content = {
type = "gpt";
partitions = {
bios = {
size = "1M";
type = "EF02";
};
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [
"fmask=0077"
"dmask=0077"
];
};
};
swap = {
size = "8G";
content = {
type = "swap";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
# Install GRUB for both legacy BIOS and EFI-capable Vultr plans.
boot.loader = {
grub = {
enable = true;
devices = [ "/dev/vda" ];
efiSupport = true;
efiInstallAsRemovable = true;
};
efi.canTouchEfiVariables = false;
};
}
+89
View File
@@ -0,0 +1,89 @@
let
domains = [
"audiobookshelf"
"cache"
"gitea"
"gems"
"jellyfin"
"share"
];
extraDomains = [ "www.norn-sight.com" ];
makeCert = name: {
name = "${name}.tmmworkshop.com";
value = {
webroot = "/var/lib/acme/.challenges";
group = "acme";
reloadServices = [ "haproxy.service" ];
};
};
makeExtraCert = name: {
inherit name;
value = {
webroot = "/var/lib/acme/.challenges";
group = "acme";
reloadServices = [ "haproxy.service" ];
};
};
acmeServices =
map (domain: "acme-${domain}.tmmworkshop.com.service") domains
++ map (domain: "acme-${domain}.service") extraDomains;
in
{
networking.firewall.allowedTCPPorts = [
80
443
2223
];
# Global robots.txt served by HAProxy for every vhost (see haproxy.cfg).
environment.etc."haproxy/robots.txt".source = ./robots.txt;
services = {
haproxy = {
enable = true;
config = builtins.readFile ./haproxy.cfg;
};
nginx = {
enable = true;
virtualHosts."acme-challenge" = {
listen = [
{
addr = "127.0.0.1";
port = 8402;
}
];
locations."/.well-known/acme-challenge/" = {
root = "/var/lib/acme/.challenges";
};
};
};
};
security.acme = {
acceptTerms = true;
defaults.email = "Richie@tmmworkshop.com";
certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains));
};
systemd = {
services.haproxy = {
# HAProxy needs the initial certificates and Tailscale connectivity before
# it can serve requests to its backends.
after = acmeServices ++ [ "tailscaled-autoconnect.service" ];
wants = acmeServices ++ [ "tailscaled-autoconnect.service" ];
};
tmpfiles.rules = [
"d /var/lib/acme/.challenges 0750 acme acme - -"
"d /var/lib/acme/.challenges/.well-known 0750 acme acme - -"
"d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -"
];
};
users.users = {
haproxy.extraGroups = [ "acme" ];
nginx.extraGroups = [ "acme" ];
};
}
@@ -22,8 +22,8 @@ defaults
#Application Setup
frontend ContentSwitching
bind *:80 v4v6
bind *:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem
bind [::]:80 v4v6
bind [::]:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem
mode http
# ACME challenge routing (must be first)
@@ -110,6 +110,14 @@ frontend ContentSwitching
use_backend gems if host_gems
use_backend norn_sight if host_norn_sight
# Proxy Gitea's built-in SSH server over Tailscale to Jeeves. Port 22 remains
# available to Endlessh on Portal.
frontend GiteaSSH
bind [::]:2223 v4v6
mode tcp
option tcplog
default_backend gitea_ssh
# Stick-table only (no servers): tracks per-IP request rate to Gitea's compare
# endpoint over a 5-minute window so the frontend can cap it at 1 per 5 min.
backend st_compare
@@ -124,31 +132,35 @@ backend acme_challenge
backend audiobookshelf_nodes
mode http
server server 127.0.0.1:8000
server server jeeves:8000
backend cache_nodes
mode http
server server 127.0.0.1:5000
server server jeeves:5000
backend jellyfin
mode http
option forwardfor
server jellyfin 127.0.0.1:8096
server jellyfin jeeves:8096
backend share_nodes
mode http
server server 127.0.0.1:8091
server server jeeves:8091
backend gitea
mode http
server server 127.0.0.1:6443
server server jeeves:6443
backend gitea_ssh
mode tcp
server gitea jeeves:2223
backend gems
mode http
option forwardfor
timeout server 1h
server gems 127.0.0.1:8002
server gems jeeves:8002
backend norn_sight
mode http
server server 127.0.0.1:8001
server server jeeves:8001
+15
View File
@@ -0,0 +1,15 @@
{ config, ... }:
{
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [
config.services.prometheus.exporters.node.port
];
services.prometheus.exporters.node = {
enable = true;
enabledCollectors = [
"pressure"
"processes"
"systemd"
];
};
}
+2 -1
View File
@@ -8,8 +8,9 @@
"${inputs.self}/common/optional/steam.nix"
"${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/yubikey.nix"
"${inputs.self}/common/optional/zerotier.nix"
"${inputs.self}/common/optional/zfs"
./hardware.nix
./open_webui.nix
./programs.nix
+1 -1
View File
@@ -51,7 +51,7 @@ def test_config_defaults_enable_reranking(mocker: MockerFixture) -> None:
config = load_rerank_config()
assert config.enabled is True
assert config.base_url == "http://192.168.90.25:8001"
assert config.base_url == "http://bob:8001"
assert config.model == "qwen3-reranker-06b"
assert config.candidates == 24
assert config.timeout_seconds == 30
+1 -16
View File
@@ -1,22 +1,7 @@
{ config, ... }:
{
imports = [
./cli
./minimal.nix
./programs.nix
./ssh_config.nix
];
programs = {
home-manager.enable = true;
git.enable = true;
};
home = {
username = "richie";
homeDirectory = "/home/${config.home.username}";
stateVersion = "24.05";
sessionVariables = {
FLAKE = "$HOME/dotfiles";
};
};
}
+1 -1
View File
@@ -35,7 +35,7 @@
"sqltools.connections": [
{
"previewLimit": 50,
"server": "192.168.90.40",
"server": "jeeves.taile39c31.ts.net",
"port": 5432,
"askForPassword": true,
"driver": "PostgreSQL",
+27
View File
@@ -0,0 +1,27 @@
{ config, pkgs, ... }:
{
imports = [
./cli
];
programs = {
home-manager.enable = true;
};
home = {
username = "richie";
homeDirectory = "/home/${config.home.username}";
stateVersion = "24.05";
sessionVariables.FLAKE = "$HOME/dotfiles";
packages = with pkgs; [
# cli
btop
eza
ripgrep
starship
tmux
# networking
wget
];
};
}
-6
View File
@@ -3,8 +3,6 @@
home.packages = with pkgs; [
# cli
bat
btop
eza
fd
ffmpegthumbnailer
fzf
@@ -18,9 +16,6 @@
p7zip
poppler
rar
ripgrep
starship
tmux
unzip
yazi
zoxide
@@ -37,7 +32,6 @@
# networking
iperf3
nmap
wget
# python
ruff
uv
+16 -4
View File
@@ -4,7 +4,7 @@
enableDefaultConfig = false;
settings = {
jeeves = {
HostName = "192.168.90.40";
HostName = "jeeves";
User = "richie";
IdentityFile = "~/.ssh/id_ed25519";
Port = 629;
@@ -18,7 +18,7 @@
Port = 2222;
};
brain = {
HostName = "192.168.90.35";
HostName = "brain";
User = "richie";
IdentityFile = "~/.ssh/id_ed25519";
Port = 129;
@@ -31,18 +31,30 @@
Port = 2222;
};
bob = {
HostName = "192.168.90.25";
HostName = "bob";
User = "richie";
IdentityFile = "~/.ssh/id_ed25519";
Port = 262;
DynamicForward = [ { port = 9050; } ];
};
rhapsody-in-green = {
HostName = "192.168.90.221";
HostName = "rhapsody-in-green";
User = "richie";
IdentityFile = "~/.ssh/id_ed25519";
Port = 922;
};
portal-1 = {
HostName = "portal-1";
User = "richie";
IdentityFile = "~/.ssh/id_ed25519";
Port = 278;
};
"gitea.tmmworkshop.com" = {
HostName = "gitea.tmmworkshop.com";
User = "gitea";
IdentityFile = "~/.ssh/id_ed25519";
Port = 2223;
};
};
};
}
+5
View File
@@ -0,0 +1,5 @@
{
imports = [
../home/minimal.nix
];
}
+39 -37
View File
@@ -1,62 +1,64 @@
richie_password: ENC[AES256_GCM,data:DMi3M8aqrQ60APIofr8wJMh+VZ14hLRxz6jWZgzswr0pV/QVSX53ShBFr90ruO3mucOLYv0l+bI31covfqMAhXWBJp9wUgtC2Q==,iv:qgtn30hZfIL4dBnQSLkjbo7zPJA4m9TR0f52sTFc0v4=,tag:ydLbcGyXjv0fE+4b5ECX5w==,type:str]
math_password: ENC[AES256_GCM,data:ykiSr3iBHrShJarEQSJ/zuXbCPcbW2oUpaAjblu1V15ufFKVSMZM94LlpMiCYtN9cYBLs98hcMeajJbvgbwT5emPHthy9+TJDw==,iv:1TJEUo0ishqFAZiUE1473yR3RT6Gbtqt4zM+C1a1KEk=,tag:pR6jyIj+bu3XaSx5yIHSmA==,type:str]
tailscale_auth_key: ENC[AES256_GCM,data:SZEfpVnqA84AQLA/geGsPNZfsWBktlM3Zrm7OpuSS/hCWWEKtSIbRYyovH68h8tQJBQFpv18n4oXJe9eAr6Mog==,iv:x1m6XPS/h818qNRcO+uqYjgTfd/FkxHHaSHHJrLyrp4=,tag:eOpcJIhzUWFzXTuRdS9S6w==,type:str]
tailscale_oauth_client_id: ENC[AES256_GCM,data:+cWxKlLIcJtsLhqmZUNLVDs=,iv:qrTjhLfu82pzbZnU6BrK1n+Ncbm2Cl3MMCSTKCJ6H8U=,tag:s/N5XpLwNH1gZ+LzbqRr2g==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBObHhkaFlnaG4zaTZtbkIw
TkxSQnMxbDNwUVo4R1VYRDNKVFRDUE9kb0ZjCnpWWElJUVNuNFBsMzZod1ZQY0Fa
VXgxNjZ0TUJ3cEt2OTFWL2dGY0txVHMKLS0tIHNQU3Rsanh4dkc3ZElsai9YMFdH
cXUzVmFxTUVIOWZVR2Fpa2crdWsrdlkKwdGLfbKWc25qfBKyd/cawiUWv9iepKHN
EOp/LdH2GbCfnQSVbxi28ukLHxWqOLdqMm8xSni/Of2PXvMnpdyCyQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjQvSzZSY08vRlI0dzBP
YytpVlpPSGFHaTI5b2xzUkFFdEY0bVhQdGgwClM3UmM1OElpcEZTcTB0NHpSZ2d6
aVBpcjVIaDk3Tko1cm4xYjdkYVQ1L1EKLS0tIE1aQW95U0JIVGc4MGloSXRXc3ZS
a3EvU1dJdmxOeVBIS28yT3dHd2ZEYWcKv5TIqRRQs3AXER1WHEx8wotyXr2Ktxsu
dwldwEHyDNqwf7UgGOZu7JueeZ7y2KC8KZ7ixIdrhVaoxuLEvCg7VA==
-----END AGE ENCRYPTED FILE-----
recipient: age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6R0djMTArVmFySE5DMnFr
WFdBUERFbE1HRVVFd0oyaXJ2eU5HUStBUFN3CnR3ckZ2bkpGZFFScHQwTlBZYTMv
cTBXbGVoTXlWczhId05QVjZDRzFqRjQKLS0tIDNIcVBydHYwTGRnVU1mWWk4WTlR
eVlwQWgxSG5SdmFrWTlOcFo5eXZONWMKgx4huoSnbkRq0wQbsYgsWUKDTxDGNvYR
anVMQg+c7PwDlk1V4JQZ4WrYLx63Ep5qDjGlN/Ssf2Vo6rAuuKetcA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByaE5jb3p3VzZ1dE05Wmlx
c1ZoWGhTcTRHRE9oMWtkaCtBY1l1a2QyWWlvCk9MM2FmcEZubXR1REpOcHZ6aHVE
UEkzbllZdkxPYzlTMHNod3VWNnhoaTgKLS0tIGVCdEpqUzhFM0swRjVjb0Z6ckFN
UzlYb1U4UGJqWE9oczdlNVh1S1lqL3cKSKxaJFhun2ee45I1jC55YKNdfYB8MsS+
dgFOFCvn/4uEcUyKZbUf0STjbIZ5eMHl8NVRTupI4tftGiE559M5Wg==
-----END AGE ENCRYPTED FILE-----
recipient: age1q47vup0tjhulkg7d6xwmdsgrw64h4ax3la3evzqpxyy4adsmk9fs56qz3y
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2QjZYejFYbCswQjdmaDA0
L2ZqUUhtYU12YlpISmxueHQzRG5YL0tQNXh3CndGamMwRzYvUzkvaE9DVnMwTkNC
cFMxczZuOUorS3dnVUpjaVdCUjhLZ2MKLS0tIGlDeGRGa3dRbWgzT3NVYjBaYUJ4
VW5yeFlvWUZ5MVpNZHA5M1VXR1hxU1kKqii08/MB2aabgP4RQs1ry8AxmFqB8Mn+
m7B0u64aziKXLSl0u471wqgD+YGRwNcajXT2pHCy8QWLznzvIMSrxA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2U01jTVZQT1RRRU1nQmVv
Z0lnOGJLN1c0UkNLbnJjV29na3Rub0xzbXhNCjY0aFNuUnFDUnZheDRHT3R5L3Fv
ZkJ3QkF2RENKRVdUYjRzRXMrNklvN2cKLS0tIFJzK2o1MmVTWHZMWmVDWlgzeGY5
VDg4OGl0NGFYY2VNZGY5UkR1cVdWY0UK1qa3swdfgUw1tnnThRf1sl9f2zTP/d/d
iwcenOEryXatVMJjZVWrLC5C7MwtKz+pIBXa98yaSpCDLqjAMwOCPg==
-----END AGE ENCRYPTED FILE-----
recipient: age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSUc3VnllVDFZTm1jRnlP
ZDBqelhkeHliZ1VlcjVnblQyeFlWclZTWkZjCjhJQk5EWkVoQjdoMHg4Zko4OU1C
NWFnVTIwV3RYbTZSeWttaVRPbjNrMG8KLS0tIDZWUHJSbVVlR0lKUDRtc3VqOTc1
cGJ3NDBLem9FNUpnbStYRTlqQStHV2sKwxPe4nTULsU0mVeUh8mhr2KX9U0iT5dL
zvHldoQG6mZHgtHK6XI5AQJYf+zUW66OKqNSxAnn+BM20QkAQVZNVw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTREI2UEZwS0Y0SmEvK1F4
Y1hZZjRVWWxMT29mc3ZXcHFDclRUYi96LzA4CkIwZ2h4T2JQRW1OL2hmRTFHWkFY
RHZqQUFqU3hvNlRBZHRiSThPVUE3TG8KLS0tIHh0Z2tlNXREczJsa1g2ekZyZjBt
dW41aVA3VEVqa1NDMHM0T3FKZFNTN0kKGQPRUMSRtibFVFRZR/igsjgxgDo4ogga
dUHBRU65r41er+Wk3lKdtbXJX1g83K8ay7540hNXheOhxFi57trrzA==
-----END AGE ENCRYPTED FILE-----
recipient: age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0NU9ac2FuRHI1dkQycmc0
YlhGK29UeTdiZEZXcWtPUW4rMis4Z2NWYWpJCkVldEdMc3ZTaDFidHpaZk5mM283
bTd0RlN5SHVabGhjeXFnSThydnVoem8KLS0tIElWclYvTmtkb0wyNDlKT3ZEckwx
L2NObzZadlJ5d3MyeGRqKy95L3BOMFEKtoswi6r2TmCZzngUkiGQV5TTsuzisMFS
5QI0aQZwhexqUMvbPuajYKvcPj+D6a2xaxbL3TBRLjOrFmcp5J7/YA==
-----END AGE ENCRYPTED FILE-----
recipient: age1l272y8udvg60z7edgje42fu49uwt4x2gxn5zvywssnv9h2krms8s094m4k
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByUlVjM2xpc281bHZzOGVo
L3VEclhJZDd5SS9mazFiTk9DcmxMaWxPT213ClNPWERKQU03OWk0OEVIY05ib2VG
WXhiZDhuMDZ5ZmFPWHB5RTFKYmpkVzAKLS0tIGppUndCb25wb2dyV01YbENrWjdU
TmMvWVpobnl0eXBIOGQwMW5BSlhJTUkKzua1artJWbZlKfzv27xfZJeBpntBYwUf
c8i1gNlvRwkhFAlrWcKR65vgyxsO3rbkLJRkcwG/q4hHj9zBeC/K2A==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrV1Z2MGNPK28wTStRSUZW
dmFMaUt5MENHeUpVcDVxQnZhd1o5RmJwOWwwCittaGNrcW1hV25VaGJaTEhZRFBJ
Y2toWEpFWFZYT0plb2lkTlJCS2Mwb3MKLS0tIHlkVjVESWFsbWlVQ0xVQ0llclF2
L1FMbC91YjJsR3BGRjJ0QVQ1MDdBbEUKNnOI9+H1LJcvVcwl9uQMmFp35y5RB+Zl
Mo3z3unweQtS+jiUn7RyVs5daG1E2tfihu2CQDWaktuCuunMpt3Wew==
-----END AGE ENCRYPTED FILE-----
recipient: age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4
lastmodified: "2026-06-29T20:19:44Z"
mac: ENC[AES256_GCM,data:GIvQxWt4tZGn0fyiXVtxGFQQoNcFUgilF+/PSz50exVrmzsS0XQUk/TIDFHaQR9jlJI50jqlyc1rBHgjnqC2oPHhPWaaVhgF18vQI55rGKdymNFjsHnaCkblFVdR1RJm0FSB2Ri6y5k8tfN3ywiwromJRz4NYzr1hbmr36azfg4=,iv:V3jspeYt/d2wy13gUrQmPGARm0hxwvSL/mocJAUofdw=,tag:vARoUzBWTJKkONDGoQdzNQ==,type:str]
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqR0hUT3hxTzRwTVhuTm9E
TUFtNFFCYWNUNUNVbHNaNXQ0aEVBa04vSEU0CkRPVzhuYUsrM0wxZjRDdkxvTVA2
UW9CSjlIQW5yenhaUi9mbEdLYlhWYk0KLS0tIGRXMm9Wa3l4UWxqcGl6ZlBZV3E0
Z3YrbUYrSmV0NHNTeTdhMkJEdytwSmMKGLpe5+p4FGJNHy6SVgnsDbEsiP0J8lW/
0+3X3LQoL0f1S2L5DjexF/H0NGvWjAPaGzDNB6V50VxGDabEIVqjvQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a
lastmodified: "2026-08-25T15:35:53Z"
mac: ENC[AES256_GCM,data:4/WuSmUb2ONlB56dgbwwekZ8h7E1vvPzPxYCvNM8N68nt1nb9JxoMeAX9PktmljAarYLQJd1O5+49ZEph3Gzx9LrwvWnm9asq0UeeJi08x7wdQDcawNFlBFzXF2qlgxU1JssNHZZOB+5nYH4t8vFzPTq/9BqoifPR2+pLBYhUQI=,iv:N/gVgeWT2/1bfHuR6TanyDBijWmXAqyACXpnwfwDuVA=,tag:pQ4ct4G7SARz2wyjOc7AKQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
version: 3.13.3