This commit is contained in:
Bryan Ramos 2026-04-15 20:58:07 -04:00
commit 864c69fe61
147 changed files with 11233 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

0
.gitattributes vendored Normal file
View file

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.qcow2
result
.direnv
.claude

19
.gitmodules vendored Normal file
View file

@ -0,0 +1,19 @@
[submodule "nvim"]
path = user/modules/neovim/nvim
url = https://github.com/itme-brain/nvim.git
branch = master
[submodule "vim"]
path = user/modules/vim/vim
url = https://github.com/itme-brain/vim.git
branch = master
[submodule "git"]
path = user/modules/git/git
url = https://github.com/itme-brain/git.git
branch = master
[submodule "bash"]
path = user/modules/bash/bash
url = https://github.com/itme-brain/bash.git
branch = master

30
.sops.yaml Normal file
View file

@ -0,0 +1,30 @@
# sops-nix configuration
# Per-machine age keys - add new machines here to grant access
keys:
# Machines
- &desktop age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl
- &server age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp
creation_rules:
# Desktop secrets
- path_regex: secrets/system/wifi\.yaml$ # Home WIFI Credentials
key_groups:
- age:
- *desktop
# Shared secrets (desktop + server)
- path_regex: secrets/system/llama\.yaml$ # llama.cpp API key
key_groups:
- age:
- *desktop
- *server
# Server secrets (cameras)
- path_regex: secrets/system/cameras\.yaml$ # RTSP Feed
key_groups:
- age:
- *server
# Server secrets (searxng)
- path_regex: secrets/system/searxng\.yaml$ # searxng token
key_groups:
- age:
- *server

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

122
README.md Normal file
View file

@ -0,0 +1,122 @@
# NixOS Configuration
Modular NixOS flake configuration with home-manager integration.
## Requirements
- [Nix with Flakes](https://nixos.wiki/wiki/Flakes#Enable_flakes_permanently_in_NixOS)
- [NixOS](https://www.nixos.org/) for system configurations
- [Home-Manager](https://nix-community.github.io/home-manager/) for user configurations
## Flake Outputs
| Configuration | Description |
|---------------|-------------|
| `desktop` | Primary workstation |
| `server` | Home server |
| `wsl` | Windows Subsystem for Linux |
## Fresh Install
From the NixOS live installer:
```bash
# Enable flakes
echo "experimental-features = nix-command flakes" | sudo tee -a /etc/nix/nix.conf
# Clone repo
nix run nixpkgs#git -- clone --recurse-submodules https://github.com/itme-brain/nixos.git
cd nixos
# Enter dev shell and install
nix develop
just install desktop
```
## Getting Started
```bash
git clone --recurse-submodules git@github.com:itme-brain/nixos.git
cd nixos
nix develop
just
```
**Note:** Replace `hardware.nix` in `system/machines/<machine>` with output from `nixos-generate-config` for your hardware.
## Directory Structure
```
.
├── flake.nix
├── flake.lock
├── justfile
├── system/
│ ├── keys/ # Machine SSH keys
│ │ └── desktop/
│ └── machines/
│ ├── desktop/
│ │ ├── default.nix # Machine entry point
│ │ ├── hardware.nix # Hardware config
│ │ ├── system.nix # System settings
│ │ └── modules/
│ │ ├── disko/ # Disk partitioning
│ │ └── home-manager/ # Home-manager integration
│ ├── server/ # Server (same structure)
│ └── wsl/ # WSL (same structure)
└── user/
├── default.nix # User options (name, email, keys)
├── home.nix # Shared home-manager defaults
├── bookmarks/
├── keys/
│ ├── age/
│ ├── pgp/
│ └── ssh/
└── modules/
├── bash/bash/ # Shell (submodule)
├── git/git/ # Git (submodule)
├── neovim/nvim/ # Neovim (submodule)
├── vim/vim/ # Vim (submodule)
├── tmux/
├── dev/ # CLI dev tools
├── security/
│ ├── gpg/
│ └── yubikey/
├── utils/
│ ├── dev/ # Dev tools (claude-code, direnv, etc.)
│ ├── email/
│ ├── irc/
│ └── writing/
└── gui/
├── default.nix # Browser-focused mimeApps
├── wm/
│ ├── hyprland/
│ └── sway/
├── browsers/
├── alacritty/
├── dev/
│ ├── pcb/ # Arduino, KiCad
│ └── design/ # Penpot
├── corn/
├── fun/
└── utils/
```
## Architecture
**flake.nix** defines NixOS configurations that reference machines under `system/machines/`.
Each machine imports its hardware, system settings, and home-manager config.
**user/home.nix** provides shared defaults for all users:
- Essential packages
- Default modules
**Machine home.nix** imports user defaults and enables machine-specific modules.
## Resources
- [nixpkgs Packages](https://search.nixos.org/packages)
- [nixpkgs Options](https://search.nixos.org/options)
- [Home-Manager Options](https://home-manager-options.extranix.com)

6
external/README.md vendored Normal file
View file

@ -0,0 +1,6 @@
# External Automation
This directory contains automation for systems that are not managed as NixOS
hosts inside this repository.
- `rigby/`: Ubuntu-based AI rig recovery and service automation.

64
external/rigby/README.md vendored Normal file
View file

@ -0,0 +1,64 @@
# Rigby Recovery
This directory contains disaster-recovery automation for `rigby`, the Ubuntu
AI rig at `192.168.0.23`.
## Scope
This automation manages the host state after a manual Ubuntu install.
It is intended to restore the working state we validated for:
- AMD ROCm `7.2.1`
- `amdgpu-dkms`
- `amdgpu.cwsr_enable=0`
- pinned ComfyUI checkout
- `uv`-managed Python `3.13` venv
- ROCm PyTorch
- ComfyUI service layout
- output sharing over Samba
- required groups and permissions
## Manual Prerequisites
These are intentionally documented, not automated:
- Install Ubuntu `24.04.4`
- Update BIOS to the known-good version for the board
- Verify BIOS settings:
- `Above 4G Decoding = Enabled`
- `SVM = Enabled`
- UEFI boot
- sane PCIe slot configuration
- Ensure host SSH is reachable as `bryan`
- Ensure passwordless sudo works for `bryan`
- Ensure the initial DHCP lease is known so recovery can begin
## Recovery Flow
1. Install Ubuntu manually.
2. Clone this repository onto the operator machine.
3. From the repo root, run `just rigby-check HOST=<rigby-ip>`.
4. Run `just rigby-recover HOST=<rigby-ip>`.
5. Reboot `rigby`.
6. Validate:
- `rocminfo`
- `rocm-smi`
- ComfyUI startup
## Notes
- The AMD repo and package installs are automated here, but BIOS and physical
host setup remain manual.
- ComfyUI itself is deployed as an application under `/home/comfy/ComfyUI`.
- The `comfyui.service` unit is installed but left disabled so the service is
started on demand.
- Models, LoRAs, VAEs, outputs, and other AI assets are not restored by this
automation. `rigby` is the source of truth for that data, so disaster
recovery for models requires a separate backup strategy.
- The `just` entrypoints accept `HOST=<ip>` so recovery does not depend on a
fixed DHCP lease.
- Recovery installs the configured SSH key for `bryan`.
- Static IP configuration is applied at the end of the playbook via netplan.
The SSH session used for recovery may be interrupted once the new address is
applied, and subsequent access should use the final static IP.

9
external/rigby/ansible.cfg vendored Normal file
View file

@ -0,0 +1,9 @@
[defaults]
inventory = inventory.ini
host_key_checking = False
stdout_callback = yaml
retry_files_enabled = False
interpreter_python = auto_silent
[ssh_connection]
pipelining = True

2
external/rigby/inventory.ini vendored Normal file
View file

@ -0,0 +1,2 @@
[ai_rig]
rigby ansible_host=192.168.0.23 ansible_user=bryan

442
external/rigby/playbooks/recover.yml vendored Normal file
View file

@ -0,0 +1,442 @@
---
- name: Recover rigby AI rig
hosts: ai_rig
become: true
vars:
rigby_user: bryan
rigby_recovery_ssh_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDl4895aB9P5p/lp8Hq5rHun4clvhyTSHFi3U2d6OOBoW5Fm+VcQnW/xbjmCBsXk5BdiowsBxQhwnzdfz/KJL7J5RobomUEaVRwb9UwT88eJveLp14BG8j2J3SjfyhrCX+4jkPx0bPQk1HGcuYY+tPEXf1q/ps88Dhu0CARBIzYQOTYY6b1qWzxpDoFZGHjKG8g5iY6FIu65yKKvvVy1f8IgZ3l3IpwBWVamxgkTcYY0QYSrmzo1n7TXxwrWbvenAqBsQ0cBPs+gVa3uIr+1TJl0Az5SElBVGu3LvUdlk58trtPUj6TQR3YUkg7Vjll7WHOdqhux5ZQNhjkOsHerf0Tw86e6cEzgeTuIbQHIb0LcsUunwKcuh2+au7RO599cvHn0+xZE5MZBxloDDaJ3JsiliM8kyPP/U3ERj03cWLW7BqbT+sfjAOl21RCzk0iQxk1wt/8VmtCr9Adv7IyrtaYvf/bwRP+g+9ldmzKGt8Mdb605uVzZ70H/LLm17f40Te+QHaex5by/6p6cuwEEZtgIg53Wpglu0rA6UxrBfQEHKl/Jt3FLeE0mnEyYkkR2MnHNtyWRIXtuqYZMAm2Ub1pFHH7jQV1gGiDVTw6a2eIwK21a/hXtRjFUpFd1nB1n+KNfJBE4zT3wm3Ud7mKw/6rWnoRyhYZvGXkFdp+iEs49Q== itme-brain@github/78120816
rigby_static_network_enabled: true
rigby_interface: eno1
rigby_static_ip: 192.168.0.23/24
rigby_gateway: 192.168.0.1
rigby_dns:
- 192.168.0.1
- 1.1.1.1
comfy_user: comfy
comfy_group: comfy
comfy_home: /home/comfy
comfy_root: /home/comfy/ComfyUI
comfy_venv: /home/comfy/comfy-venv
comfy_python_version: "3.13"
comfy_port: 8188
comfy_output_dir: /home/comfy/ComfyUI/output
comfy_repo_url: https://github.com/comfy-org/ComfyUI
comfy_repo_version: a1344238901efc5ea199d8094cb16fca36ceb28b
comfy_manager_version: "4.1"
comfy_torch_index_url: https://download.pytorch.org/whl/rocm7.2
grub_cmdline_linux_default: "amdgpu.cwsr_enable=0"
amd_driver_deb: amdgpu-install_7.2.1.70201-1_all.deb
amd_driver_url: https://repo.radeon.com/amdgpu-install/7.2.1/ubuntu/noble/amdgpu-install_7.2.1.70201-1_all.deb
rigby_packages:
- curl
- git
- rsync
- software-properties-common
- python-is-python3
- python3.13
- python3.13-venv
- python3.13-dev
- build-essential
- linux-headers-{{ ansible_kernel }}
- linux-modules-extra-{{ ansible_kernel }}
- samba
- just
- python3.12
- python3.12-venv
- docker.io
vllm_user: vllm
vllm_home: /home/vllm
vllm_venv: /home/vllm/vllm-venv
vllm_models: /home/vllm/models
vllm_port: 8000
vllm_gpu_memory_utilization: "0.95"
vllm_rocm_wheels_url: https://wheels.vllm.ai/rocm/0.19.0/rocm721
vllm_models_list:
- name: Qwen2.5-Coder-14B
recipe: coder
dir: Qwen2.5-Coder-14B-Instruct
max_model_len: 4096
- name: Qwen2.5-7B-Instruct
recipe: qwen7b
dir: Qwen2.5-7B-Instruct
max_model_len: 8192
tool_call_parser: hermes
librechat_root: /home/bryan/LibreChat
librechat_repo_url: https://github.com/danny-avila/LibreChat
tasks:
- name: Ensure deadsnakes PPA is configured
ansible.builtin.apt_repository:
repo: ppa:deadsnakes/ppa
state: present
update_cache: true
- name: Install required Ubuntu packages
ansible.builtin.apt:
name: "{{ rigby_packages }}"
state: present
update_cache: true
- name: Ensure AMD installer package is present
ansible.builtin.get_url:
url: "{{ amd_driver_url }}"
dest: "/tmp/{{ amd_driver_deb }}"
mode: "0644"
- name: Install AMD installer package
ansible.builtin.apt:
deb: "/tmp/{{ amd_driver_deb }}"
state: present
- name: Install AMD GPU DKMS driver
ansible.builtin.apt:
name: amdgpu-dkms
state: present
update_cache: true
- name: Install ROCm stack
ansible.builtin.apt:
name: rocm
state: present
- name: Ensure required groups exist
ansible.builtin.group:
name: "{{ item }}"
state: present
loop:
- render
- video
- "{{ comfy_group }}"
- name: Ensure comfy user exists
ansible.builtin.user:
name: "{{ comfy_user }}"
group: "{{ comfy_group }}"
groups:
- render
- video
append: true
create_home: true
shell: /bin/bash
- name: Ensure bryan is in required groups
ansible.builtin.user:
name: "{{ rigby_user }}"
groups:
- render
- video
- "{{ comfy_group }}"
append: true
- name: Ensure recovery SSH keys are present for bryan
ansible.posix.authorized_key:
user: "{{ rigby_user }}"
state: present
key: "{{ item }}"
loop: "{{ rigby_recovery_ssh_keys }}"
- name: Configure GRUB default kernel args
ansible.builtin.lineinfile:
path: /etc/default/grub
regexp: '^GRUB_CMDLINE_LINUX_DEFAULT='
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ grub_cmdline_linux_default }}"'
- name: Ensure GRUB menu is shown
ansible.builtin.lineinfile:
path: /etc/default/grub
regexp: '^{{ item.key }}='
line: "{{ item.key }}={{ item.value }}"
loop:
- { key: GRUB_TIMEOUT_STYLE, value: "menu" }
- { key: GRUB_TIMEOUT, value: "5" }
- name: Regenerate grub config
ansible.builtin.command: update-grub
changed_when: true
- name: Ensure Comfy directories exist
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ comfy_user }}"
group: "{{ comfy_group }}"
mode: "{{ item.mode }}"
loop:
- { path: "{{ comfy_home }}", mode: "0775" }
- { path: "{{ comfy_root }}", mode: "0775" }
- { path: "{{ comfy_output_dir }}", mode: "2775" }
- { path: "{{ comfy_home }}/.local/bin", mode: "0775" }
- { path: "{{ comfy_home }}/piptmp", mode: "0775" }
- name: Ensure uv is installed for comfy
ansible.builtin.shell: |
set -euo pipefail
curl -LsSf https://astral.sh/uv/install.sh | sh
args:
creates: "{{ comfy_home }}/.local/bin/uv"
become_user: "{{ comfy_user }}"
- name: Ensure ComfyUI repo is present at pinned revision
ansible.builtin.git:
repo: "{{ comfy_repo_url }}"
dest: "{{ comfy_root }}"
version: "{{ comfy_repo_version }}"
update: true
become_user: "{{ comfy_user }}"
- name: Ensure ComfyUI venv exists
ansible.builtin.command:
argv:
- "{{ comfy_home }}/.local/bin/uv"
- venv
- --python
- "{{ comfy_python_version }}"
- "{{ comfy_venv }}"
args:
creates: "{{ comfy_venv }}/bin/python"
become_user: "{{ comfy_user }}"
- name: Install base Python packaging tools in Comfy venv
ansible.builtin.command:
argv:
- "{{ comfy_home }}/.local/bin/uv"
- pip
- install
- --python
- "{{ comfy_venv }}/bin/python"
- --upgrade
- pip
- setuptools
- wheel
become_user: "{{ comfy_user }}"
- name: Install ROCm PyTorch in Comfy venv
ansible.builtin.command:
argv:
- "{{ comfy_home }}/.local/bin/uv"
- pip
- install
- --python
- "{{ comfy_venv }}/bin/python"
- --index-url
- "{{ comfy_torch_index_url }}"
- torch
- torchvision
- torchaudio
environment:
TMPDIR: "{{ comfy_home }}/piptmp"
become_user: "{{ comfy_user }}"
- name: Install ComfyUI requirements in Comfy venv
ansible.builtin.command:
argv:
- "{{ comfy_home }}/.local/bin/uv"
- pip
- install
- --python
- "{{ comfy_venv }}/bin/python"
- -r
- "{{ comfy_root }}/requirements.txt"
environment:
TMPDIR: "{{ comfy_home }}/piptmp"
become_user: "{{ comfy_user }}"
- name: Install ComfyUI-Manager in Comfy venv
ansible.builtin.command:
argv:
- "{{ comfy_home }}/.local/bin/uv"
- pip
- install
- --python
- "{{ comfy_venv }}/bin/python"
- "comfyui-manager=={{ comfy_manager_version }}"
environment:
TMPDIR: "{{ comfy_home }}/piptmp"
become_user: "{{ comfy_user }}"
- name: Ensure output directories have group inheritance
ansible.builtin.shell: |
set -euo pipefail
find "{{ comfy_output_dir }}" -type d -exec chown {{ comfy_user }}:{{ comfy_group }} {} +
find "{{ comfy_output_dir }}" -type d -exec chmod 2775 {} +
changed_when: true
- name: Ensure output files are group writable
ansible.builtin.shell: |
set -euo pipefail
find "{{ comfy_output_dir }}" -type f -exec chown {{ comfy_user }}:{{ comfy_group }} {} +
find "{{ comfy_output_dir }}" -type f -exec chmod 0664 {} +
changed_when: true
- name: Install ComfyUI systemd unit
ansible.builtin.template:
src: ../templates/comfyui.service.j2
dest: /etc/systemd/system/comfyui.service
owner: root
group: root
mode: "0644"
- name: Ensure Samba include directory exists
ansible.builtin.file:
path: /etc/samba/smb.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Install Samba share config for Comfy outputs
ansible.builtin.template:
src: ../templates/comfy-output.conf.j2
dest: /etc/samba/smb.conf.d/comfy-output.conf
owner: root
group: root
mode: "0644"
- name: Ensure Samba includes conf.d snippets
ansible.builtin.blockinfile:
path: /etc/samba/smb.conf
marker: "; {mark} ANSIBLE MANAGED COMFY OUTPUT INCLUDE"
block: |
include = /etc/samba/smb.conf.d/comfy-output.conf
- name: Reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
- name: Ensure ComfyUI service is installed but disabled
ansible.builtin.systemd_service:
name: comfyui.service
enabled: false
- name: Ensure Samba service is enabled and running
ansible.builtin.systemd_service:
name: smbd.service
enabled: true
state: started
- name: Install netplan static IP config for rigby
ansible.builtin.template:
src: ../templates/99-rigby-static.yaml.j2
dest: /etc/netplan/99-rigby-static.yaml
owner: root
group: root
mode: "0644"
when: rigby_static_network_enabled | bool
- name: Apply static netplan configuration as final step
ansible.builtin.command: netplan apply
when: rigby_static_network_enabled | bool
changed_when: true
# --- vLLM ---
- name: Ensure vllm user exists
ansible.builtin.user:
name: "{{ vllm_user }}"
groups:
- render
- video
append: true
create_home: true
shell: /bin/bash
- name: Ensure vllm models directory exists
ansible.builtin.file:
path: "{{ vllm_models }}"
state: directory
owner: "{{ vllm_user }}"
group: "{{ vllm_user }}"
mode: "0755"
- name: Ensure uv is installed for vllm user
ansible.builtin.shell: |
set -euo pipefail
curl -LsSf https://astral.sh/uv/install.sh | sh
args:
creates: "{{ vllm_home }}/.local/bin/uv"
become_user: "{{ vllm_user }}"
- name: Ensure vllm venv exists
ansible.builtin.command:
argv:
- "{{ vllm_home }}/.local/bin/uv"
- venv
- --python
- "3.12"
- "{{ vllm_venv }}"
args:
creates: "{{ vllm_venv }}/bin/python"
become_user: "{{ vllm_user }}"
- name: Install vLLM in venv
ansible.builtin.command:
argv:
- "{{ vllm_home }}/.local/bin/uv"
- pip
- install
- --python
- "{{ vllm_venv }}/bin/python"
- vllm
- --extra-index-url
- "{{ vllm_rocm_wheels_url }}"
args:
creates: "{{ vllm_venv }}/bin/vllm"
become_user: "{{ vllm_user }}"
- name: Install vllm justfile
ansible.builtin.template:
src: ../templates/vllm-justfile.j2
dest: "{{ vllm_home }}/justfile"
owner: "{{ vllm_user }}"
group: "{{ vllm_user }}"
mode: "0644"
- name: Ensure vllm bashrc sources api key from file
ansible.builtin.lineinfile:
path: "{{ vllm_home }}/.bashrc"
line: "export VLLM_API_KEY=$(cat {{ vllm_home }}/.api_key)"
state: present
# --- LibreChat ---
- name: Ensure Docker service is enabled and running
ansible.builtin.systemd_service:
name: docker
enabled: true
state: started
- name: Ensure bryan is in docker group
ansible.builtin.user:
name: "{{ rigby_user }}"
groups:
- docker
append: true
- name: Ensure LibreChat repo is present
ansible.builtin.git:
repo: "{{ librechat_repo_url }}"
dest: "{{ librechat_root }}"
update: false
become_user: "{{ rigby_user }}"
- name: Install librechat.yaml config
ansible.builtin.template:
src: ../templates/librechat.yaml.j2
dest: "{{ librechat_root }}/librechat.yaml"
owner: "{{ rigby_user }}"
group: "{{ rigby_user }}"
mode: "0644"
- name: Install librechat systemd unit
ansible.builtin.template:
src: ../templates/librechat.service.j2
dest: /etc/systemd/system/librechat.service
owner: root
group: root
mode: "0644"
- name: Reload systemd and enable librechat service
ansible.builtin.systemd_service:
name: librechat.service
daemon_reload: true
enabled: true

View file

@ -0,0 +1,16 @@
network:
version: 2
renderer: networkd
ethernets:
{{ rigby_interface }}:
dhcp4: false
addresses:
- {{ rigby_static_ip }}
routes:
- to: default
via: {{ rigby_gateway }}
nameservers:
addresses:
{% for dns in rigby_dns %}
- {{ dns }}
{% endfor %}

View file

@ -0,0 +1,9 @@
[comfy-output]
path = {{ comfy_output_dir }}
browseable = yes
read only = no
guest ok = yes
force user = {{ comfy_user }}
force group = {{ comfy_group }}
create mask = 0664
directory mask = 2775

View file

@ -0,0 +1,22 @@
[Unit]
Description=ComfyUI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ comfy_user }}
Group={{ comfy_group }}
UMask=0002
WorkingDirectory={{ comfy_root }}
Environment=HOME={{ comfy_home }}
Environment=COMFYUI_PATH={{ comfy_root }}
Environment=PATH={{ comfy_home }}/.local/bin:{{ comfy_venv }}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart={{ comfy_venv }}/bin/python {{ comfy_root }}/main.py --highvram --enable-manager --listen 0.0.0.0 --port {{ comfy_port }} --disable-auto-launch
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,18 @@
[Unit]
Description=LibreChat
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User={{ rigby_user }}
Group=docker
WorkingDirectory={{ librechat_root }}
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,43 @@
version: 1.3.5
cache: true
interface:
webSearch: false
runCode: false
mcpServers:
use: true
create: true
share: false
public: false
mcpServers:
searxng:
command: npx
args:
- -y
- mcp-searxng
env:
SEARXNG_URL: http://searxng:8080
timeout: 60000
fetch:
command: uvx
args:
- mcp-server-fetch
- --ignore-robots-txt
timeout: 60000
endpoints:
custom:
- name: "rigby-vllm"
apiKey: "${VLLM_API_KEY}"
baseURL: "http://host.docker.internal:{{ vllm_port }}/v1"
models:
default: []
fetch: true
titleConvo: true
titleModel: "current_model"
titleMessageRole: "user"
summarize: false
summaryModel: "current_model"
modelDisplayLabel: "Rigby vLLM"

View file

@ -0,0 +1,58 @@
{% raw %}# List available recipes
[private]
default:
@just --list
# Show currently running vLLM server
status:
@pgrep -a -f "vllm serve" || echo "No vLLM server running"
# Tail the vLLM log
logs:
@tail -f {% endraw %}{{ vllm_home }}/vllm.log{% raw %}
# Stop any running vLLM server and wait for VRAM to free
stop:
#!/usr/bin/env bash
set -euo pipefail
if pgrep -f "vllm serve" > /dev/null; then
echo "Stopping vLLM..."
pkill -TERM -f "vllm serve" || true
sleep 2
pkill -KILL -f "vllm serve" 2>/dev/null || true
fi
echo "Waiting for VRAM to release..."
for i in $(seq 1 30); do
used=$(rocm-smi --showmeminfo vram 2>/dev/null | awk '/VRAM Total Used Memory/ {print $NF}')
total=$(rocm-smi --showmeminfo vram 2>/dev/null | awk '/VRAM Total Memory \(B\)/ {print $NF}')
if [ -n "$used" ] && [ -n "$total" ] && [ "$total" -gt 0 ]; then
pct=$(( used * 100 / total ))
echo " VRAM: ${pct}%"
if [ "$pct" -lt 10 ]; then
echo "VRAM free."
exit 0
fi
fi
sleep 2
done
echo "Warning: VRAM did not fully release after 60s"
{% endraw %}
{% for model in vllm_models_list %}
# Serve {{ model.name }}
{{ model.recipe }}: stop
#!/usr/bin/env bash
source {{ vllm_home }}/.bashrc
nohup {{ vllm_home }}/vllm-venv/bin/vllm serve {{ vllm_models }}/{{ model.dir }} \
--served-model-name {{ model.name }} \
--host 0.0.0.0 \
--port {{ vllm_port }} \
--api-key ${VLLM_API_KEY} \
--dtype auto \
--max-model-len {{ model.max_model_len }} \
--gpu-memory-utilization {{ vllm_gpu_memory_utilization }}{% if model.tool_call_parser is defined %} \
--enable-auto-tool-choice \
--tool-call-parser {{ model.tool_call_parser }}{% endif %} \
> {{ vllm_home }}/vllm.log 2>&1 &
echo "Started {{ model.name }} (pid $!). Run 'just logs' to follow."
{% endfor %}

204
flake.lock generated Normal file
View file

@ -0,0 +1,204 @@
{
"nodes": {
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773025010,
"narHash": "sha256-khlHllTsovXgT2GZ0WxT4+RvuMjNeR5OW0UYeEHPYQo=",
"owner": "nix-community",
"repo": "disko",
"rev": "7b9f7f88ab3b339f8142dc246445abb3c370d3d3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1772985280,
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1739577062,
"narHash": "sha256-u/trdPzJO8UotNq48RbG7m6Pe8761IEMCOY0QidNjY4=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "0b2b8b31f69f24e9a75b4b18a32c771a48612d5e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "2411.6.0",
"repo": "NixOS-WSL",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1773375660,
"narHash": "sha256-SEzUWw2Rf5Ki3bcM26nSKgbeoqi2uYy8IHVBqOKjX3w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e20095fe3c6cbb1ddcef89b26969a69a1570776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nur": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1773108757,
"narHash": "sha256-3BAoe2R6YA6Xjdsgx3urZ4Ns3LeTy0E/w5d1wPny910=",
"owner": "nix-community",
"repo": "NUR",
"rev": "9f2c583704f122828e6f9893416ca3b007464ee6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NUR",
"type": "github"
}
},
"root": {
"inputs": {
"disko": "disko",
"home-manager": "home-manager",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable",
"nur": "nur",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1773550941,
"narHash": "sha256-wa/++bL2QeMUreNFBZEWluQfOYB0MnQIeGNMuaX9sfs=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "c469b6885f0dcd5c7c56bd935a0f08dbcd9e79e1",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

89
flake.nix Normal file
View file

@ -0,0 +1,89 @@
{
description = "My Nix Configs";
inputs =
{
self.submodules = true;
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
nur = {
url = "github:nix-community/NUR";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-wsl = {
url = "github:nix-community/NixOS-WSL/2411.6.0";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, nixpkgs-unstable, nur, ... }@inputs:
let
mkPkgs = system: import nixpkgs {
inherit system;
config = {
allowUnfree = true;
nvidia.acceptLicense = true;
};
overlays = [
nur.overlays.default
# Make unstable packages available as pkgs.unstable.*
(final: prev: {
unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
};
})
];
};
mkSystem = { path, system ? "x86_64-linux" }:
let pkgs = mkPkgs system;
in nixpkgs.lib.nixosSystem {
inherit system pkgs;
specialArgs = { inherit inputs; };
modules = [
inputs.sops-nix.nixosModules.sops
path
];
};
in
{
nixosConfigurations = {
desktop = mkSystem { path = ./system/machines/desktop; };
server = mkSystem { path = ./system/machines/server; };
wsl = mkSystem { path = ./system/machines/wsl; };
};
devShells.x86_64-linux.default = with mkPkgs "x86_64-linux"; mkShell {
name = "devShell";
packages = [
just
rclone
ansible
age
sops
ssh-to-age
git
git-crypt
gnupg
yubikey-manager
age-plugin-yubikey
];
};
};
}

383
justfile Normal file
View file

@ -0,0 +1,383 @@
SYSTEM := "$(echo $HOSTNAME)"
VALID_SYSTEMS := "desktop server wsl"
RIGBY_DIR := "external/rigby"
RIGBY_HOST := "192.168.0.23"
# Print this list
default:
@just --list
# Verify SSH connectivity and Ansible access to the Ubuntu AI rig.
[group('rigby')]
rigby-check HOST=RIGBY_HOST:
@cd {{RIGBY_DIR}} && ansible -i "{{HOST}}," all -u bryan -m ping
# Apply the disaster-recovery playbook for the Ubuntu AI rig.
[group('rigby')]
rigby-recover HOST=RIGBY_HOST:
@cd {{RIGBY_DIR}} && ansible-playbook -i "{{HOST}}," -u bryan playbooks/recover.yml
# Preview rig recovery changes without modifying the target host.
[group('rigby')]
rigby-recover-dry-run HOST=RIGBY_HOST:
@cd {{RIGBY_DIR}} && ansible-playbook -i "{{HOST}}," -u bryan playbooks/recover.yml --check --diff
# Validate system argument
[private]
_validate SYSTEM:
#!/usr/bin/env bash
case "{{SYSTEM}}" in
desktop|server|wsl) ;;
*) echo "Error: Unknown system '{{SYSTEM}}'. Use one of: {{VALID_SYSTEMS}}"; exit 1 ;;
esac
# Helper to parse submodules from .gitmodules
[private]
_subs_init := '''
declare -A SUBS
while read -r key path; do
name="${key#submodule.}"; name="${name%.path}"
SUBS[$name]="$path"
done < <(git config -f .gitmodules --get-regexp 'submodule\..*\.path')
'''
# Clean up build artifacts
[group('nix')]
clean:
#!/usr/bin/env bash
set -euo pipefail
echo "Cleaning build artifacts"
rm -f result
rm -f ./*.qcow2
echo "Done"
# Output what derivations will be built
[group('nix')]
out SYSTEM="desktop": (_validate SYSTEM)
@echo "Outputting derivations to be built for {{SYSTEM}}..."
@nix build --dry-run .#nixosConfigurations."{{SYSTEM}}".config.system.build.toplevel -L
# Test switch into the next generation
[group('nixos')]
test SYSTEM=SYSTEM: (_validate SYSTEM)
@echo "Testing switching to next NixOS generation for {{SYSTEM}}..."
@sudo nixos-rebuild test --flake .#{{SYSTEM}}
# Build the nix expression and hydrate the results directory
[group('nix')]
build SYSTEM="desktop": (_validate SYSTEM)
@echo "Building NixOS configuration for {{SYSTEM}}..."
@nix build .#nixosConfigurations."{{SYSTEM}}".config.system.build.toplevel -L
@echo -e "\033[32mBuild success - result directory hydrated\033[0m"
# Deploy a vm of the defined system
[group('nixos')]
vm SYSTEM: (_validate SYSTEM)
#!/usr/bin/env bash
set -euo pipefail
echo "Building VM for {{SYSTEM}}..."
nixos-rebuild build-vm --flake .#{{SYSTEM}}
if [[ -f result/bin/run-{{SYSTEM}}-vm ]]; then
result/bin/run-{{SYSTEM}}-vm
else
echo "Error: VM build failed!"
exit 1
fi
# grep nixpkgs for PKG
[group('nix')]
search PKG:
nix search nixpkgs {{PKG}}
# Open nixos packages in the browser
[group('nix')]
pkgs:
@xdg-open https://search.nixos.org/packages
# Open nixos options in the browser
[group('nix')]
options:
@xdg-open https://search.nixos.org/options
# NixOS-rebuild switch for the current system
[group('nixos')]
switch:
@echo -e "\033[32m->> Switching to next generation ->>\033[0m"
@sudo nixos-rebuild switch --flake .#{{SYSTEM}}
# Rollback to previous generation
[group('nixos')]
rollback:
@sudo nixos-rebuild switch --rollback
# NixOS-rebuild boot for the current system
[group('nixos')]
boot:
@echo -e "\033[34m->> Reboot to new generation ->>\033[0m"
@sudo nixos-rebuild boot --flake .#{{SYSTEM}}
# Partition disk only (interactive disk selection)
[group('nixos')]
partition SYSTEM:
#!/usr/bin/env bash
set -euo pipefail
DISKO_CONFIG="./system/machines/{{SYSTEM}}/modules/disko/default.nix"
if [[ ! -f "$DISKO_CONFIG" ]]; then
echo "Error: No disko config for '{{SYSTEM}}'"
exit 1
fi
# Build array of disk options with readable info
declare -a DISK_IDS
declare -a DISK_OPTIONS
for id in /dev/disk/by-id/*; do
name=$(basename "$id")
[[ "$name" =~ part ]] && continue
[[ ! "$name" =~ ^(ata|nvme|scsi)- ]] && continue
dev=$(readlink -f "$id")
dev_name=$(basename "$dev")
size=$(lsblk -dn -o SIZE "$dev" 2>/dev/null) || continue
model=$(lsblk -dn -o MODEL "$dev" 2>/dev/null | xargs) || model=""
DISK_IDS+=("$id")
DISK_OPTIONS+=("$dev_name $size $model")
done
if [[ ${#DISK_IDS[@]} -eq 0 ]]; then
echo "No disks found!"
exit 1
fi
echo "Select a disk:"
select opt in "${DISK_OPTIONS[@]}"; do
if [[ -n "$opt" ]]; then
idx=$((REPLY - 1))
DISK="${DISK_IDS[$idx]}"
break
else
echo "Invalid selection"
fi
done
echo ""
echo -e "\033[31m!! WARNING: This will DESTROY all data on $DISK !!\033[0m"
read -p "Continue? [y/N]: " confirm
case "${confirm,,}" in
y|yes) ;;
*) echo "Aborted."; exit 1 ;;
esac
echo "Writing disk '$DISK' to disko config..."
sed -i "s|device = \"/dev/disk/by-id/[^\"]*\";|device = \"$DISK\";|" "$DISKO_CONFIG"
echo "Partitioning $DISK..."
sudo nix \
--extra-experimental-features "nix-command flakes" \
run github:nix-community/disko -- \
--mode destroy,format,mount \
"$DISKO_CONFIG"
echo -e "\033[32mPartitioning complete. Disk mounted at /mnt.\033[0m"
# Install NixOS (partition + install in one shot)
[group('nixos')]
install SYSTEM:
#!/usr/bin/env bash
set -euo pipefail
DISKO_CONFIG="./system/machines/{{SYSTEM}}/modules/disko/default.nix"
if [[ ! -f "$DISKO_CONFIG" ]]; then
echo "Error: No disko config for '{{SYSTEM}}'"
exit 1
fi
# Build array of disk options with readable info
declare -a DISK_IDS
declare -a DISK_OPTIONS
for id in /dev/disk/by-id/*; do
name=$(basename "$id")
[[ "$name" =~ part ]] && continue
[[ ! "$name" =~ ^(ata|nvme|scsi)- ]] && continue
dev=$(readlink -f "$id")
dev_name=$(basename "$dev")
size=$(lsblk -dn -o SIZE "$dev" 2>/dev/null) || continue
model=$(lsblk -dn -o MODEL "$dev" 2>/dev/null | xargs) || model=""
DISK_IDS+=("$id")
DISK_OPTIONS+=("$dev_name $size $model")
done
if [[ ${#DISK_IDS[@]} -eq 0 ]]; then
echo "No disks found!"
exit 1
fi
echo "Select a disk:"
select opt in "${DISK_OPTIONS[@]}"; do
if [[ -n "$opt" ]]; then
idx=$((REPLY - 1))
DISK="${DISK_IDS[$idx]}"
break
else
echo "Invalid selection"
fi
done
echo ""
echo -e "\033[31m!! WARNING: This will DESTROY all data on $DISK !!\033[0m"
read -p "Continue? [y/N]: " confirm
case "${confirm,,}" in
y|yes) ;;
*) echo "Aborted."; exit 1 ;;
esac
echo "Writing disk '$DISK' to disko config..."
sed -i "s|device = \"/dev/disk/by-id/[^\"]*\";|device = \"$DISK\";|" "$DISKO_CONFIG"
echo "Partitioning and installing NixOS..."
sudo nix \
--extra-experimental-features "nix-command flakes" \
run github:nix-community/disko/latest#disko-install -- \
--flake .#{{SYSTEM}} \
--disk main "$DISK"
echo -e "\033[32mDone! Reboot to start NixOS.\033[0m"
# Commit all changes and push to upstream
[group('git')]
gh COMMIT_MESSAGE:
#!/usr/bin/env bash
set -euo pipefail
git add -A
git commit -m "{{COMMIT_MESSAGE}}"
git push
# Show status of submodules with changes
[group('submodule')]
sstatus:
#!/usr/bin/env bash
{{_subs_init}}
for name in "${!SUBS[@]}"; do
status=$(git -C "${SUBS[$name]}" status -s)
[[ -n "$status" ]] && echo -e "\033[34m$name:\033[0m" && echo "$status"
done
# Pull all submodules and parent
[group('submodule')]
spull:
#!/usr/bin/env bash
set -euo pipefail
{{_subs_init}}
git pull
for name in "${!SUBS[@]}"; do
echo -e "\033[34m$name:\033[0m"
git -C "${SUBS[$name]}" pull
done
# Push submodules and parent
[group('submodule')]
spush NAME="":
#!/usr/bin/env bash
set -euo pipefail
{{_subs_init}}
if [[ -n "{{NAME}}" ]]; then
path="${SUBS[{{NAME}}]:-}"
[[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1
git -C "$path" push
else
for path in "${SUBS[@]}"; do git -C "$path" push; done
fi
git push
# Commit submodule changes and update parent
[group('submodule')]
scommit NAME="":
#!/usr/bin/env bash
set -euo pipefail
{{_subs_init}}
MSGS=()
commit_sub() {
local name="$1" path="$2"
[[ -z "$(git -C "$path" status -s)" ]] && return 0
echo -e "\033[34m$name:\033[0m"
git -C "$path" status -s
read -p "Commit message: " MSG
[[ -z "$MSG" ]] && return 0
git -C "$path" add -A && git -C "$path" commit -m "$MSG"
git add "$path"
MSGS+=("$name: $MSG")
}
if [[ -n "{{NAME}}" ]]; then
path="${SUBS[{{NAME}}]:-}"
[[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1
commit_sub "{{NAME}}" "$path"
else
for name in "${!SUBS[@]}"; do commit_sub "$name" "${SUBS[$name]}"; done
fi
if ! git diff --cached --quiet; then
COMMIT_MSG="updated submodules"$'\n'
for m in "${MSGS[@]}"; do COMMIT_MSG+="- $m"$'\n'; done
git commit -m "$COMMIT_MSG"
fi
# Commit and push submodules + parent
[group('submodule')]
ssync NAME="":
#!/usr/bin/env bash
set -euo pipefail
{{_subs_init}}
MSGS=()
sync_sub() {
local name="$1" path="$2"
[[ -z "$(git -C "$path" status -s)" ]] && return 0
echo -e "\033[34m$name:\033[0m"
git -C "$path" status -s
read -p "Commit message: " MSG
[[ -z "$MSG" ]] && return 0
git -C "$path" add -A && git -C "$path" commit -m "$MSG"
git -C "$path" push
git add "$path"
MSGS+=("$name: $MSG")
}
if [[ -n "{{NAME}}" ]]; then
path="${SUBS[{{NAME}}]:-}"
[[ -z "$path" ]] && echo "Unknown: {{NAME}}. Available: ${!SUBS[*]}" && exit 1
sync_sub "{{NAME}}" "$path"
else
for name in "${!SUBS[@]}"; do sync_sub "$name" "${SUBS[$name]}"; done
fi
if ! git diff --cached --quiet; then
COMMIT_MSG="updated submodules"$'\n'
for m in "${MSGS[@]}"; do COMMIT_MSG+="- $m"$'\n'; done
git commit -m "$COMMIT_MSG"
fi
git push
# Fetch resources and compute sha256 hash
[group('nix')]
hash URL:
#!/usr/bin/env bash
set -euo pipefail
if [[ "{{URL}}" =~ \.(tar(\.gz)?|tgz|gz|zip)$ ]]; then
CONTENTS=$(nix-prefetch-url --unpack {{URL}})
else
CONTENTS=$(nix-prefetch-url {{URL}})
fi
HASH=$(nix hash convert --hash-algo sha256 "$CONTENTS")
echo -e "\033[32m$HASH\033[0m"

68
secrets/README.md Normal file
View file

@ -0,0 +1,68 @@
# Secrets Management
```
secrets/
├── system/ # System-level secrets (WiFi, VPN, etc.)
└── user/ # User-level secrets (password-store, API keys, etc.)
```
## Prerequisites
Age identity files are stored in `src/user/config/keys/age/` and deployed automatically.
```bash
# For testing with a local key:
age-keygen > src/user/config/keys/age/local
# For Yubikey (see "Migrating to Yubikey" below):
age-plugin-yubikey --identity > src/user/config/keys/age/yubikey
# Add the public key to .sops.yaml in repo root
```
After rebuild, the identity is written to `~/.config/sops/age/keys.txt`.
## Adding Secrets
1. Create or edit a YAML file:
```bash
vim secrets/system/example.yaml
```
2. Encrypt in place:
```bash
sops -e -i secrets/system/example.yaml
```
3. Reference in NixOS config:
```nix
sops.secrets."SECRET_NAME" = {
sopsFile = path/to/example.yaml;
};
```
## Editing Secrets
```bash
# Opens decrypted in $EDITOR, re-encrypts on save
sops secrets/system/wifi.yaml
```
## Viewing Secrets
```bash
# Decrypt to stdout
sops -d secrets/system/wifi.yaml
```
## Removing Secrets
1. Remove from NixOS config
2. Delete the encrypted file or remove the key from it via `sops`
## Re-keying (after adding/removing age keys)
```bash
# Update .sops.yaml with new keys, then:
sops updatekeys secrets/system/wifi.yaml
```

View file

@ -0,0 +1,17 @@
RTSP_USER: ENC[AES256_GCM,data:yketGXU=,iv:KQVYzBjzkkDepiD+hjGWLjvyC3iySK6JMZ9Fyrdo1Eo=,tag:7sHqOYROk6qNd56xWex1Bw==,type:str]
RTSP_PASS: ENC[AES256_GCM,data:QGfg7bZVdGAjuw==,iv:uS/6XpHlMgpZ812tVxGFjwMeyqX5YvfBNJUVuc0C+z8=,tag:5SIdu/yGVxzhYclyOUrOCg==,type:str]
sops:
age:
- recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwSys1ZzdwRmRybkR2TGFn
RG1wVGI4aTNkYTZpOUtUSlBJQTVnU1JsdmpzCklLdUY0K1ZjSzhId3NVNXcvUWl0
eE95cmVHWGNsZVNYWHQvSXlNZjl5WWMKLS0tIFBpek81aGlhUXUxWm91ZjV1RFk0
SzZFalY2NXJOMFNSVFVxbDZPb1Q1amsKaDZqJvFfqxhqVcd5ldRHC+3XC/lBb9N7
VUQ/hQZM5a1WUk321Y2bBXTN6cE/06UYrl6HXwZgxTVydou4eHywww==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-15T06:01:25Z"
mac: ENC[AES256_GCM,data:EwPCYlEKUgjcrZ0c75UH7n9FjkbF+WEMQzJ7Xb1+fXkD0zIIVgjudgCNtwwJTbSVupyuCVcJfCKN9n4kBpG+HyIqDZQl1MTy5YzcvvMoj3rkPLIRMfkLXFs4FRe/cFKFdxARbQrlEJqfgQME8/M07Bl+VcZRIq0mz7HlrxZFbgg=,iv:WDGCSNFT8l+MEOQCWSDDtYTj9gdDoCk+kl8UdQg+9mw=,tag:4b9vRle/waBqQX284cIiNA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.1

25
secrets/system/llama.yaml Normal file
View file

@ -0,0 +1,25 @@
LLAMA_API_KEY: ENC[AES256_GCM,data:ZVDpwGAxnHbHxt+JW3mYGyyBU5JfFAbjc/byq6Ok9wTlpQZBx969Z0wV74F5pR4axmpdGs7XlZDh1rJaQTn7lg==,iv:oAG9G25x+1FRkRNBRzLW2UJmbSxgx5Cu64Qo/6VzAyw=,tag:nkO/SdzjjLxH4fkgIdwUYQ==,type:str]
sops:
age:
- recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzUmV6Q2dCMWU3TUFkZ0I0
dHA3dXd2U0RSRzNtL3YvdG8rYWdnOTZoTkMwCkNnYnVlVmMyRDNnS1FmWktlNU9N
UW1OMlJYODVzSHNIZWZMRkpPY05Ed3cKLS0tIDg0b0VkT0NrS3NIWE9EdWtWYXc1
NjNESHpYbVptcnVRYWFKb3RlYkJ6OWMK3JsRXPDvJdKv2UyYIH8kr/WKbXgUDXbc
fYOD0Huo73BA0vr8PlrsF4STVgJr/arKCMdI1C0bDdcwjExKnR1tIw==
-----END AGE ENCRYPTED FILE-----
- recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFTGNKOWczaityaXowWi9I
dmh0MjJoelV3bVlzeGpLZmVTVzJjckwwQUFzCk81ZHlTcm5oWHRQNklreUR4bWNS
OVdQelQ4YXkzeWZqOWZoNWlOVkZpWUkKLS0tIDZKQUU3LzV0UUhnRHVHQkFadkxm
djRyUEYyZ2srMlVxR0JtQlFqSWV1QWcKMIF9Sq4TUUmpVZAukjTjFbIrMxcE3+el
QSrHIm1HXLXwCKLDQ2N6b8Q9iUo/XMV0wsD3TLxdnUfegpQpfsDhag==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-14T05:45:37Z"
mac: ENC[AES256_GCM,data:G+o6OhNF5AFBDKQEU3f1MZ+GOkxQj/m7NNk4Ti8PxPPOHdByoCrauvgB78SdQf5ubcfupElcNB0yF5QsG3/m7eGaSA+8J0cDL6jB3NEE5EUbW1Fuzzg2Ez1JnFu4BstkLiDRD/TribXMNFAjykmNrHt4zee6fhU3H0MOn7+Acok=,iv:IqBLSBq1kOMRHQn1IvU8OgmWGn6EFJcef/rNr38txmY=,tag:/mSWgbPbhUNoIm3x+6zyRA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.1

View file

@ -0,0 +1,16 @@
SEARXNG_TOKEN: ENC[AES256_GCM,data:6hI9+Gk9D7OjgcNV7WHUkcT8Kzta+QbJ8bq5uDv1AU/n1lpD/41RSWAZ91v5f0VSAldKvDMIuRdjxmKaE0ITOA==,iv:LURC0t6YwectCMllBBx8TIGxM80vXS84pkvczmWtO6U=,tag:ZSRbU7B+LKsLw8R7Yi9uzg==,type:str]
sops:
age:
- recipient: age198jg29ryg3c0qj3yg6y9ha4ce2ue4hjdaa9kalf49fxju74dhchsquvjzp
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWkRjc0xWR3o5ZFlnUXRY
bnNKUWhNL3dFcnJ0a0tLZHNuRU1obEwzam44CjczSEEvdVUyUXN5MEpsYnA5Y3NQ
M2N6VXZpM3ZxMHd0ZWV1MG5qT3ZnZzgKLS0tIGN3UG01eW4xZ1A4bkF3TzNKVkdv
eE9uRmpId2R5VTJSeDhRVUkvSWt4RHMK/oXVHDAWN5SY/4hPCm0QsTo2ubBD+uBf
fOZr/4HNDOyq8AIfbRVbilC7l/Ozg8snu8chRo1keCjqHp+Pt+Yzhw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-04-13T19:21:23Z"
mac: ENC[AES256_GCM,data:qxBRF1wSTXeFEvjs+5HiRmk6wqt1Rtx1kNFigpqicfd/IJsZTJY/6g3SmZXrJNkpkYwyOBNlblGfLAfKMWm6/eg1KYBJHlSAAqkH/xECdDqKY0rTkMj3rfPTZzLGmi4kVp6v8jg9OO5SwK8sLGtbaK2S/VjdTI0NXlMMnsB30Sg=,iv:GacmqywEsxCyKQKmCPu42uyqy6Q0JhR7STDAMFvW7kQ=,tag:G1Lbu6DKuO5yLWs6kXjwHA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.1

19
secrets/system/wifi.yaml Normal file
View file

@ -0,0 +1,19 @@
WIFI_HOME_SSID: ENC[AES256_GCM,data:xZl6DE4=,iv:koEKZTW3O+bctlwoSzZCBLRT4iG380RmP/olukUd8Xc=,tag:4HM6d+FslbM1hRYcn3JTqA==,type:str]
WIFI_HOME_PSK: ENC[AES256_GCM,data:jyC4VXzhpIE=,iv:fN33x0y4kmRrPQe7ydWGdeTQaR5a3ekBaUKHX9FpHk0=,tag:tQUUj5LU6kidYTTI2RWf8w==,type:str]
WIFI_CAMS_SSID: ENC[AES256_GCM,data:yJ/oUCfSbaw=,iv:foswCMqFLOUyPQP9KL08Mhix0j2+Jt4sHHaPV49RFe4=,tag:rBG9IyQDmbNsUp4E+tnmZg==,type:str]
WIFI_CAMS_PSK: ENC[AES256_GCM,data:VlVxqxbHof6rmqSRJrXEQsT15BNl1lrghg==,iv:B6si07a0Z5ZJfMkK0HN9fa5zvQDzf7lvIQt1ZBpBZdk=,tag:21622mki8lITWA5fh7bKrg==,type:str]
sops:
age:
- recipient: age17ejyzyk52unr6eyaa9rpunxpmf7u9726v6sx7me3ww3mdu5xzgjqsgj9gl
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuaFRWRnNGVm9TTkhJVFAv
RWQ1Q2Q3RStBa0E4V2hFYUV2ZHFPZnJGdkFJClY0WThYbWk2Nmx6V0g4UU9WSGRZ
bFpNalZJRlZyWjFTMU1JK1dpWndPS3cKLS0tIHI0M3ZUVlI3TTV6c2h1WmdrdW1l
VWtxaFNVUUFHT20xVTZpSjVWRHozTzQKAAsNbFf6bU6eelqOX7Ei+Zrtw0aw0WgQ
5zOWrxd92MaG/AvVpL0jC1LuWtZeK3MK7Qpgtm8t0rgugUas16KYpA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-15T06:25:12Z"
mac: ENC[AES256_GCM,data:+Lhmcr2Jg1htfcMMMPu8AxrDhvlm4yLVIunxAcs4adX8NeJccD+/UVvZO+qtzF6iQmXCdTvRDo3shqmJKHvs6ZUJVe3jokTKMJoQdIbSIS0fSwULUV8evK5Incf8qzpnHd2J1Kg4qCL8oWeN9t4TBJTPVrNJzd/sOF1Kp2g9IBE=,iv:/ORst/Lnj3h16fJQWxAaJ5vMWKMN2lGhGoIQjNxNpGQ=,tag:NURI5mwbfECaWTgbSs6clA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.1

43
system/keys/default.nix Normal file
View file

@ -0,0 +1,43 @@
{ lib, ... }:
with lib;
with builtins;
let
extractName = filename:
let
noKey = removeSuffix ".key" filename;
noMarkers = replaceStrings
[ ".pub" ".priv" ".public" ".private" ]
[ "" "" "" "" ]
noKey;
in noMarkers;
constructKeys = dir: (
listToAttrs (
map (subdir: {
name = subdir;
value = listToAttrs (
map (file: {
name = extractName file;
value = readFile "${dir}/${subdir}/${file}";
}) (filter (file:
(readDir "${dir}/${subdir}").${file} == "regular" &&
hasSuffix ".key" file
) (attrNames (readDir "${dir}/${subdir}")))
);
}) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir)))
)
);
in
{
options = {
machines = mkOption {
description = "Machine Configurations";
type = types.attrs;
default = {
keys = constructKeys ./.;
};
};
};
}

View file

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYXfu4Jc/HtdyhOfAdCXYzhqCubIq3Bz6Kl9NDUov76 bryan@desktop

View file

@ -0,0 +1,19 @@
## Hardware
| Component | Model |
|-------------|------------------------------------|
| Motherboard | MSI B760 GAMING PLUS WIFI |
| CPU | Intel Core i7-12700KF (12th Gen) |
| GPU | NVIDIA GeForce GTX 1650 |
| Storage | 2x 2TB Crucial MX500 SSD |
## Memory
| Slot | Size | Manufacturer | Part Number | Speed |
|---------|------|----------------|-------------|------------|
| DIMM A1 | - | - | - | - |
| DIMM A2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s |
| DIMM B1 | - | - | - | - |
| DIMM B2 | 16GB | Team Group Inc | UD5-6000 | 4800 MT/s |
**Total: 32GB DDR5**

View file

@ -0,0 +1,17 @@
{ inputs, ... }:
{
imports = [
inputs.disko.nixosModules.disko
(import ./modules/disko)
inputs.home-manager.nixosModules.home-manager
{ home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; }
(import ./modules/home-manager)
../../../user
../../keys
../../modules/sops
../../modules/docker
./hardware.nix
./system.nix
];
}

View file

@ -0,0 +1,89 @@
{ config, lib, pkgs, modulesPath, ... }:
with lib;
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
options.monitors = mkOption {
type = types.listOf (types.submodule {
options = {
name = mkOption { type = types.str; example = "HDMI-A-1"; };
width = mkOption { type = types.int; };
height = mkOption { type = types.int; };
x = mkOption { type = types.int; };
y = mkOption { type = types.int; };
scale = mkOption { type = types.float; };
refreshRate = mkOption { type = types.int; };
};
});
default = [];
description = "System monitor configuration";
};
config = {
monitors = [
{ name = "HDMI-A-1"; width = 1920; height = 1080; x = 0; y = 0; scale = 1.0; refreshRate = 60; }
{ name = "DP-1"; width = 1920; height = 1080; x = 1920; y = 0; scale = 1.0; refreshRate = 60; }
];
boot = {
initrd = {
availableKernelModules = [ "vmd" "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" ];
kernelModules = [ "dm-snapshot" ];
};
extraModulePackages = [ ];
kernelPackages = pkgs.linuxPackages_zen;
kernelParams = [ "intel_iommu=on" ];
kernelModules = [ "kvm-intel" "virtio" "vfio-pci" "coretemp" ];
};
environment.systemPackages = with pkgs; [
linuxHeaders
vulkan-headers
vulkan-loader
vulkan-tools
vulkan-extension-layer
mesa
mesa-demos
cudaPackages.cudatoolkit
cudaPackages.cudnn
nvidia-vaapi-driver
];
hardware = {
cpu = {
intel = {
updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
};
};
nvidia = {
open = true;
modesetting.enable = true;
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
nvidia-container-toolkit.enable = true;
graphics = {
enable = true;
enable32Bit = true;
};
};
# Despite confusing name, this configures userspace nvidia libraries
services.xserver.videoDrivers = [ "nvidia" ];
virtualisation.libvirtd = {
enable = true;
qemu = {
runAsRoot = true;
};
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "performance";
};
}

View file

@ -0,0 +1,57 @@
{
disko.devices = {
disk = {
main = {
type = "disk";
device = "/dev/disk/by-id/ata-CT2000MX500SSD1_2137E5D2D47D";
content = {
type = "gpt";
partitions = {
boot = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
primary = {
size = "100%";
content = {
type = "lvm_pv";
vg = "nix";
};
};
};
};
};
};
lvm_vg = {
nix = {
type = "lvm_vg";
lvs = {
root = {
size = "5%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [ "defaults" ];
};
};
home = {
size = "100%FREE";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/home";
};
};
};
};
};
};
}

View file

@ -0,0 +1,5 @@
{
imports = [
./home.nix
];
}

View file

@ -0,0 +1,94 @@
{ config, pkgs, ... }:
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = {
monitors = config.monitors;
};
home-manager.users.${config.user.name} = {
imports = [
../../../../../user
../../../../../user/home.nix
../../../../../user/modules
];
home.stateVersion = "23.11";
home.packages = [ pkgs.sshfs ];
systemd.user.services.nvr-mount = {
Unit = {
Description = "Mount Frigate recordings via SSHFS";
After = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
RemainAfterExit = true;
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p %h/Media/nvr";
ExecStart = "${pkgs.sshfs}/bin/sshfs -o reconnect,ServerAliveInterval=15 server:/var/lib/frigate/recordings %h/Media/nvr";
ExecStop = "${pkgs.fuse}/bin/fusermount -u %h/Media/nvr";
};
Install = {
WantedBy = [ "default.target" ];
};
};
systemd.user.services.comfy-mount = {
Unit = {
Description = "Mount ComfyUI outputs via SSHFS";
After = [ "network-online.target" ];
};
Service = {
Type = "oneshot";
RemainAfterExit = true;
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p %h/Media/Comfy";
ExecStart = "${pkgs.sshfs}/bin/sshfs -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 rigby:/home/comfy/ComfyUI/output %h/Media/Comfy";
ExecStop = "${pkgs.fuse}/bin/fusermount -u %h/Media/Comfy";
};
Install = {
WantedBy = [ "default.target" ];
};
};
programs.ssh = {
enable = true;
enableDefaultConfig = false;
matchBlocks = {
"*" = {
serverAliveInterval = 60;
serverAliveCountMax = 3;
};
"server" = {
hostname = "192.168.0.154";
user = "bryan";
};
"rigby" = {
hostname = "192.168.0.23";
user = "bryan";
};
};
};
# Machine-specific modules
modules.user = {
vim.enable = false;
security.yubikey.enable = true;
utils = {
dev.enable = true;
irc.enable = true;
writing.enable = true;
};
gui = {
wm.hyprland.enable = true;
browser.firefox.enable = true;
alacritty.enable = true;
corn.enable = true;
fun.enable = true;
utils.enable = true;
};
};
};
}

View file

@ -0,0 +1,212 @@
{ pkgs, lib, config, ... }:
let
gpgEnabled = lib.any
(user: user.modules.user.security.gpg.enable or false)
(lib.attrValues config.home-manager.users);
devEnabled = lib.any
(user: user.modules.user.utils.dev.enable or false)
(lib.attrValues config.home-manager.users);
sysModules = config.modules.system;
in
{ system.stateVersion = "23.11";
modules.system.sops.enable = true;
modules.system.docker.enable = true;
# WiFi secrets
sops.secrets = let wifi = { sopsFile = ../../../secrets/system/wifi.yaml; }; in {
"WIFI_HOME_SSID" = wifi;
"WIFI_HOME_PSK" = wifi;
"WIFI_CAMS_SSID" = wifi;
"WIFI_CAMS_PSK" = wifi;
} // lib.optionalAttrs devEnabled {
"LLAMA_API_KEY" = {
sopsFile = ../../../secrets/system/llama.yaml;
owner = config.user.name;
};
};
sops.templates."wifi-env".content = ''
WIFI_HOME_SSID=${config.sops.placeholder."WIFI_HOME_SSID"}
WIFI_HOME_PSK=${config.sops.placeholder."WIFI_HOME_PSK"}
WIFI_CAMS_SSID=${config.sops.placeholder."WIFI_CAMS_SSID"}
WIFI_CAMS_PSK=${config.sops.placeholder."WIFI_CAMS_PSK"}
'';
users.users = {
${config.user.name} = {
isNormalUser = true;
extraGroups = config.user.groups
++ [ "video" "audio" "kvm" "libvirtd" "dialout" ];
openssh.authorizedKeys.keys = [ "${config.user.keys.ssh.graphone}" ];
};
};
nix = {
channel.enable = false;
package = pkgs.nixVersions.stable;
extraOptions = ''
experimental-features = nix-command flakes
keep-going = true
'';
settings = {
auto-optimise-store = true;
trusted-users = [ "${config.user.name}" ];
substitute = true;
max-jobs = "auto";
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
};
boot.loader = {
systemd-boot = {
enable = true;
configurationLimit = 5;
#memtest86.enable = true;
};
efi = {
canTouchEfiVariables = true;
};
#timeout = null;
};
environment = {
systemPackages = with pkgs; [
ansible
vim
git
usbutils
];
pathsToLink = [
"/share/applications"
"/share/xdg-desktop-portal"
];
};
fonts.packages = with pkgs; [
nerd-fonts.terminess-ttf
];
security = {
sudo = {
wheelNeedsPassword = false;
execWheelOnly = true;
};
polkit.enable = true;
};
time = {
timeZone = "America/New_York";
hardwareClockInLocalTime = true;
};
i18n.defaultLocale = "en_US.UTF-8";
console = {
font = "Lat2-Terminus16";
useXkbConfig = true;
};
networking = {
hostName = "desktop";
useDHCP = lib.mkDefault true;
networkmanager = {
enable = true;
ensureProfiles = {
environmentFiles = [ config.sops.templates."wifi-env".path ];
profiles.wifi = {
connection = {
id = "$WIFI_HOME_SSID";
type = "wifi";
interface-name = "wlo1";
autoconnect = false; # Manual connection via nmcli
};
wifi = {
ssid = "$WIFI_HOME_SSID";
mode = "infrastructure";
};
wifi-security = {
key-mgmt = "wpa-psk";
psk = "$WIFI_HOME_PSK";
};
ipv4.method = "auto";
ipv6.method = "auto";
};
profiles.cams = {
connection = {
id = "$WIFI_CAMS_SSID";
type = "wifi";
interface-name = "wlo1";
autoconnect = false;
};
wifi = {
ssid = "$WIFI_CAMS_SSID";
mode = "infrastructure";
};
wifi-security = {
key-mgmt = "wpa-psk";
psk = "$WIFI_CAMS_PSK";
};
ipv4.method = "auto";
ipv6.method = "auto";
};
};
};
firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];
};
};
services.dnsmasq = {
enable = true;
settings = {
# Explicit subdomains -> local server
address = [
"/*.ramos.codes/192.168.0.154"
];
server = [ "192.168.0.1" ];
};
};
services = {
pcscd.enable = gpgEnabled;
timesyncd = lib.mkDefault {
enable = true;
servers = [
"0.pool.ntp.org"
"1.pool.ntp.org"
"2.pool.ntp.org"
"3.pool.ntp.org"
];
};
pipewire = {
enable = true;
audio.enable = true;
wireplumber.enable = true;
pulse.enable = true;
jack.enable = true;
alsa.enable = true;
alsa.support32Bit = true;
};
openssh = {
enable = true;
startWhenNeeded = false;
settings = {
X11Forwarding = false;
PasswordAuthentication = false;
};
};
};
}

View file

@ -0,0 +1,20 @@
## Hardware
| Component | Model |
|-----------|--------------------------------|
| System | HP Z230 SFF Workstation |
| CPU | Intel Core i7-4770 @ 3.40GHz |
| GPU | Integrated |
| Storage | 6TB Seagate ST6000NM0024 |
| Network | Intel (onboard) |
## Memory
| Slot | Size | Manufacturer | Part Number | Speed |
|-------|------|---------------|-------------------|-----------|
| DIMM1 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s |
| DIMM2 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s |
| DIMM3 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s |
| DIMM4 | 4GB | Hynix/Hyundai | HMT451U6AFR8C-PB | 1600 MT/s |
**Total: 16GB DDR3**

View file

@ -0,0 +1,16 @@
{ inputs, ... }:
{
imports = [
inputs.disko.nixosModules.disko
(import ./modules/disko)
inputs.home-manager.nixosModules.home-manager
{ home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; }
(import ./modules/home-manager)
../../../user
../../keys
../../modules/sops
./hardware.nix
./system.nix
];
}

View file

@ -0,0 +1,27 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "ehci_pci" "ahci" "usbhid" "sd_mod" "sr_mod" ];
kernelModules = [ ];
};
kernelModules = [ "kvm-intel" ];
extraModulePackages = [ ];
};
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
# Enable VAAPI for hardware video acceleration
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-vaapi-driver # i965 driver for Haswell
];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View file

@ -0,0 +1,103 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.backup;
recipientArgs = concatMapStrings (r: "-r '${lib.strings.trim r}' ") cfg.recipients;
# Convert absolute paths to relative for tar, preserving structure
# e.g., /var/lib/forgejo -> var/lib/forgejo
tarPaths = map (p: removePrefix "/" p) cfg.paths;
excludeArgs = concatMapStrings (e: "--exclude='${e}' ") cfg.exclude;
backupScript = pkgs.writeShellScript "backup" ''
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_NAME="backup-$TIMESTAMP.tar.gz.age"
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
echo "Starting backup: $BACKUP_NAME"
echo "Paths: ${concatStringsSep " " cfg.paths}"
export PATH="${pkgs.gzip}/bin:${pkgs.age-plugin-yubikey}/bin:$PATH"
${pkgs.gnutar}/bin/tar -C / ${excludeArgs}-czf - ${concatStringsSep " " tarPaths} | \
${pkgs.age}/bin/age ${recipientArgs} -o "$TEMP_DIR/$BACKUP_NAME"
${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf copy "$TEMP_DIR/$BACKUP_NAME" "${cfg.destination}"
# Prune old backups
${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf lsf "${cfg.destination}" | \
sort -r | \
tail -n +$((${toString cfg.keepLast} + 1)) | \
while read -r old; do
${pkgs.rclone}/bin/rclone --config /root/.config/rclone/rclone.conf delete "${cfg.destination}/$old"
done
echo "Backup complete"
'';
in
{
options.modules.system.backup = {
enable = mkEnableOption "Encrypted backups";
paths = mkOption {
type = types.listOf types.str;
default = [];
description = "Absolute paths to include in backup (structure preserved)";
};
exclude = mkOption {
type = types.listOf types.str;
default = [];
description = "Patterns to exclude (passed to tar --exclude)";
};
recipients = mkOption {
type = types.listOf types.str;
default = [];
description = "Age public keys for encryption";
};
destination = mkOption {
type = types.str;
default = "";
description = "Rclone destination";
};
schedule = mkOption {
type = types.str;
default = "daily";
description = "Systemd calendar expression";
};
keepLast = mkOption {
type = types.int;
default = 3;
description = "Number of backups to keep";
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.rclone ];
systemd.services.backup = {
description = "Encrypted backup";
serviceConfig = {
Type = "oneshot";
ExecStart = backupScript;
};
};
systemd.timers.backup = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.schedule;
Persistent = true;
};
};
};
}

View file

@ -0,0 +1,20 @@
server=1
rpccookiefile=/var/lib/bitcoin/.cookie
rpccookieperms=group
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
dnsseed=0
onlynet=onion
bind=127.0.0.1
proxy=127.0.0.1:9050
listen=1
listenonion=1
torcontrol=127.0.0.1:9051
txindex=1
dbcache=1024

View file

@ -0,0 +1,80 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.bitcoin;
nginx = config.modules.system.nginx;
home = "/var/lib/bitcoin";
bitcoinConf = pkgs.writeTextFile {
name = "bitcoin.conf";
text = builtins.readFile ./config/bitcoin.conf;
};
in
{ options.modules.system.bitcoin = { enable = mkEnableOption "Bitcoin Server"; };
config = mkIf cfg.enable {
modules.system.tor.enable = true;
environment.systemPackages = with pkgs; [
bitcoind
];
users = {
users = {
"btc" = {
inherit home;
description = "Bitcoin Core system user";
isSystemUser = true;
group = "bitcoin";
extraGroups = [ "tor" ];
createHome = true;
};
"nginx" = {
extraGroups = mkIf nginx.enable [
"bitcoin"
];
};
};
groups = {
"bitcoin" = {
members = [
"btc"
config.user.name
];
};
};
};
programs.bash.shellAliases = {
btc = "bitcoin-cli";
};
services.bitcoind = {
"mainnet" = {
enable = true;
user = "btc";
group = "bitcoin";
configFile = bitcoinConf;
dataDir = home;
pidFile = "${home}/bitcoind.pid";
};
};
# Make data dir group-accessible so electrs/clightning can read cookie
systemd.tmpfiles.rules = [
"d ${home} 0750 btc bitcoin -"
];
systemd.services.bitcoind-mainnet = {
wants = [ "tor.service" ];
after = [ "tor.service" ];
serviceConfig.ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/tor";
};
modules.system.backup.paths = [
"${home}/wallets"
];
};
}

View file

@ -0,0 +1,31 @@
alias=OrdSux
network=bitcoin
bitcoin-datadir=/var/lib/bitcoin
bitcoin-rpcconnect=127.0.0.1
bitcoin-rpcport=8332
lightning-dir=/var/lib/clightning
plugin-dir=/var/lib/clightning/plugins
log-file=/var/lib/clightning/lightningd.log
log-level=info
rpc-file-mode=0660
# Bind RPC locally only
bind-addr=127.0.0.1:9736
# Auto-create Tor hidden service for peer connections
addr=autotor:127.0.0.1:9051
# Route outbound through Tor
proxy=127.0.0.1:9050
always-use-proxy=true
large-channels
fee-base=1000
fee-per-satoshi=10
min-capacity-sat=10000
htlc-minimum-msat=0
funding-confirms=3
max-concurrent-htlcs=30

View file

@ -0,0 +1,115 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.modules.system.bitcoin.clightning;
btc = config.modules.system.bitcoin;
nginx = config.modules.system.nginx;
home = "/var/lib/clightning";
domain = "ramos.codes";
clnrest = pkgs.callPackage ./plugins/clnrest.nix { };
clnConfig = pkgs.writeTextFile {
name = "lightning.conf";
text = ''
${builtins.readFile ./config/lightning.conf}
bitcoin-cli=${pkgs.bitcoind}/bin/bitcoin-cli
# CLNRest configuration
clnrest-port=3010
clnrest-host=127.0.0.1
clnrest-protocol=https
'';
};
in
{ options.modules.system.bitcoin.clightning = { enable = mkEnableOption "Core Lightning Server"; };
config = mkIf (cfg.enable && btc.enable) {
environment.systemPackages = with pkgs; [
clightning
];
users = {
users = {
"clightning" = {
inherit home;
description = "Core Lightning system user";
isSystemUser = true;
group = "bitcoin";
extraGroups = [ "tor" ];
createHome = true;
};
};
groups = {
"bitcoin" = {
members = mkAfter [
"clightning"
];
};
};
};
programs.bash.shellAliases = {
cln = "lightning-cli";
};
systemd.services.lightningd = {
description = "Core Lightning Daemon";
wantedBy = [ "multi-user.target" ];
wants = [ "bitcoind-mainnet.service" "tor.service" ];
after = [
"bitcoind-mainnet.service"
"tor.service"
"network.target"
];
serviceConfig = {
ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/bitcoin /var/lib/tor ${home} ${home}/bitcoin";
ExecStart = "${pkgs.clightning}/bin/lightningd --conf=${clnConfig}";
User = "clightning";
Group = "bitcoin";
WorkingDirectory = home;
Type = "simple";
KillMode = "process";
TimeoutSec = 60;
Restart = "always";
RestartSec = 60;
};
};
# Bind mount from /data
fileSystems.${home} = {
device = "/data/clightning";
fsType = "none";
options = [ "bind" ];
};
# Ensure data directory exists with correct permissions
systemd.tmpfiles.rules = mkAfter [
"d /data/clightning 0750 clightning bitcoin -"
"d /data/clightning/bitcoin 0750 clightning bitcoin -"
"d /data/clightning/plugins 0750 clightning bitcoin -"
"L+ /home/${config.user.name}/.lightning - - - - ${home}"
"L+ ${home}/plugins/clnrest - - - - ${clnrest}/libexec/c-lightning/plugins/clnrest"
];
modules.system.backup.paths = [
"${home}/bitcoin/hsm_secret"
"${home}/bitcoin/emergency.recover"
];
services.nginx.virtualHosts."ln.${domain}" = mkIf nginx.enable {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "https://127.0.0.1:3010";
extraConfig = ''
proxy_ssl_verify off;
'';
};
};
};
}

View file

@ -0,0 +1,54 @@
{
lib,
rustPlatform,
fetchFromGitHub,
pkg-config,
openssl,
protobuf,
}:
rustPlatform.buildRustPackage rec {
pname = "clnrest";
version = "25.02.2";
src = fetchFromGitHub {
owner = "ElementsProject";
repo = "lightning";
rev = "v${version}";
hash = "sha256-SiPYB463l9279+zawsxmql1Ui/dTdah5KgJgmrWsR2A=";
};
cargoLock = {
lockFile = "${src}/Cargo.lock";
};
cargoBuildFlags = [
"-p"
"clnrest"
];
cargoTestFlags = [
"-p"
"clnrest"
];
nativeBuildInputs = [
pkg-config
protobuf
];
buildInputs = [ openssl ];
postInstall = ''
mkdir -p $out/libexec/c-lightning/plugins
mv $out/bin/clnrest $out/libexec/c-lightning/plugins/
rmdir $out/bin
'';
meta = {
description = "Transforms RPC calls into REST APIs";
homepage = "https://docs.corelightning.org/docs/rest";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "clnrest";
};
}

View file

@ -0,0 +1,13 @@
network = "bitcoin"
electrum_rpc_addr = "127.0.0.1:50001"
cookie_file = "/var/lib/bitcoin/.cookie"
db_dir = "/var/lib/electrs"
log_filters = "INFO"
daemon_rpc_addr = "127.0.0.1:8332"
daemon_p2p_addr = "127.0.0.1:8333"
daemon_dir = "/var/lib/bitcoin"

View file

@ -0,0 +1,121 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.modules.system.bitcoin.electrum;
nginx = config.modules.system.nginx;
home = "/var/lib/electrs";
btc = config.modules.system.bitcoin;
domain = "ramos.codes";
electrsConfig = pkgs.writeTextFile {
name = "config.toml";
text = builtins.readFile ./config/config.toml;
};
in
{ options.modules.system.bitcoin.electrum = { enable = mkEnableOption "Electrs Server"; };
config = mkIf (cfg.enable && btc.enable) {
#TODO: Fix the failing overlay due to `cargoHash/cargoSha256`
#nixpkgs.overlays = [
# (final: prev: {
# electrs = prev.electrs.overrideAttrs (old: rec {
# pname = "electrs";
# version = "0.10.8";
# src = pkgs.fetchFromGitHub {
# owner = "romanz";
# repo = pname;
# rev = "v${version}";
# hash = "sha256-L26jzAn8vwnw9kFd6ciyYS/OLEFTbN8doNKy3P8qKRE=";
# };
# #cargoDeps = old.cargoDeps.overrideAttrs (const {
# # name = "electrs-${version}.tar.gz";
# # inherit src;
# # sha256 = "";
# #});
# cargoHash = "sha256-lBRcq73ri0HR3duo6Z8PdSjnC8okqmG5yWeHxH/LmcU=";
# });
# })
#];
environment.systemPackages = with pkgs; [
electrs
];
users = {
users = {
"electrs" = {
inherit home;
description = "Electrs system user";
isSystemUser = true;
group = "bitcoin";
createHome = true;
};
};
groups = {
"bitcoin" = {
members = mkAfter [
"electrs"
];
};
};
};
systemd.services.electrs = {
description = "Electrs Bitcoin Indexer";
wantedBy = [ "multi-user.target" ];
wants = [ "bitcoind-mainnet.service" ];
after = [
"bitcoind-mainnet.service"
"network.target"
];
serviceConfig = {
ExecStartPre = "+${pkgs.coreutils}/bin/chmod 750 /var/lib/bitcoin";
ExecStart = "${pkgs.electrs}/bin/electrs --conf=${electrsConfig}";
User = "electrs";
Group = "bitcoin";
WorkingDirectory = home;
Type = "simple";
KillMode = "process";
TimeoutSec = 60;
Restart = "always";
RestartSec = 60;
};
};
# Bind mount from /data
fileSystems.${home} = {
device = "/data/electrs";
fsType = "none";
options = [ "bind" ];
};
# Ensure db directory exists with correct permissions
systemd.tmpfiles.rules = [
"d /data/electrs 0750 electrs bitcoin -"
];
# Nginx SSL proxy for Electrum protocol (TCP)
networking.firewall.allowedTCPPorts = mkIf nginx.enable [ 50002 ];
services.nginx.streamConfig = mkIf nginx.enable ''
map $ssl_server_name $electrs_backend {
electrum.${domain} 127.0.0.1:50001;
default "";
}
server {
listen 50002 ssl;
proxy_pass $electrs_backend;
ssl_certificate /var/lib/acme/${domain}/fullchain.pem;
ssl_certificate_key /var/lib/acme/${domain}/key.pem;
}
'';
};
}

View file

@ -0,0 +1,35 @@
let
mkModules = dir: isRoot:
let
entries = builtins.readDir dir;
names = builtins.attrNames entries;
isModuleDir = path:
builtins.pathExists path &&
builtins.readFileType path == "directory" &&
builtins.baseNameOf path != "config" &&
builtins.baseNameOf path != "plugins" &&
builtins.baseNameOf path != "home-manager" &&
builtins.baseNameOf path != "disko";
isModule = file: file == "default.nix";
isNix = file: builtins.match ".*\\.nix" file != null && file != "default.nix";
in
builtins.concatMap (name:
let
path = "${dir}/${name}";
in
if isModuleDir path then
mkModules path false
else if isModule name && !isRoot then
[dir]
else if isNix name then
[path]
else
[]
) names;
in
{
imports = mkModules ./. true;
}

View file

@ -0,0 +1,75 @@
{ lib, ... }:
{
disko.devices = {
disk = {
main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
lvm = {
size = "100%";
content = {
type = "lvm_pv";
vg = "vg0";
};
};
};
};
};
};
lvm_vg = {
vg0 = {
type = "lvm_vg";
lvs = {
root = {
size = "200G";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
data = {
size = "1T";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/data";
};
};
bitcoin = {
size = "1T";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/var/lib/bitcoin";
};
};
frigate = {
size = "3T";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/var/lib/frigate";
};
};
# ~300GB left unallocated for future growth
};
};
};
};
}

View file

@ -0,0 +1,106 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.forgejo;
nginx = config.modules.system.nginx;
domain = "ramos.codes";
socketPath = "/run/forgejo/forgejo.sock";
in
{
options.modules.system.forgejo = {
enable = mkEnableOption "Forgejo Server";
};
config = mkIf cfg.enable {
users.groups.git = {};
users.users.git = {
isSystemUser = true;
group = "git";
home = "/var/lib/forgejo";
shell = "${pkgs.bash}/bin/bash";
};
users.users.nginx = mkIf nginx.enable {
extraGroups = [ "git" ];
};
# Bind mount from /data
fileSystems."/var/lib/forgejo" = {
device = "/data/forgejo";
fsType = "none";
options = [ "bind" ];
};
systemd.tmpfiles.rules = [
"d /data/forgejo 0750 git git -"
"d /data/forgejo/.ssh 0700 git git -"
"d /data/forgejo/custom 0750 git git -"
"d /data/forgejo/data 0750 git git -"
];
services.forgejo = {
enable = true;
user = "git";
group = "git";
stateDir = "/var/lib/forgejo";
settings = {
DEFAULT = {
APP_NAME = "Git Server";
APP_SLOGAN = "";
};
service.REQUIRE_SIGNIN_VIEW = false;
server = {
DOMAIN = "git.${domain}";
ROOT_URL = "https://git.${domain}/";
PROTOCOL = "http+unix";
HTTP_ADDR = socketPath;
SSH_DOMAIN = "git.${domain}";
SSH_PORT = 22;
START_SSH_SERVER = false;
LANDING_PAGE = "explore";
LFS_MAX_FILE_SIZE = 0;
};
"repository.upload" = {
FILE_MAX_SIZE = 0;
};
service = {
REGISTER_MANUAL_CONFIRM = true;
DISABLE_REGISTRATION = false;
DEFAULT_ALLOW_CREATE_ORGANIZATION = false;
};
admin = {
DISABLE_REGULAR_ORG_CREATION = true;
};
auth = {
ENABLE_BASIC_AUTHENTICATION = true;
};
};
database = {
type = "sqlite3";
path = "/var/lib/forgejo/data/forgejo.db";
};
};
modules.system.backup.paths = [
"/var/lib/forgejo"
];
services.nginx.virtualHosts."git.${domain}" = mkIf nginx.enable {
useACMEHost = domain;
forceSSL = true;
extraConfig = "client_max_body_size 0;";
locations."/" = {
proxyPass = "http://unix:${socketPath}";
};
};
};
}

View file

@ -0,0 +1,162 @@
# Frigate Camera Setup
## Camera Models
| Camera | Model | MAC | IP |
|--------|-------|-----|-----|
| cam4 | W461ASC | 00:1f:54:c2:d1:b1 | 192.168.1.194 |
| cam1 | B463AJ | 00:1f:54:a9:81:d1 | 192.168.1.167 |
| cam2 | W463AQ (ch1) | 00:1f:54:b2:9b:1d | 192.168.1.147 |
| cam3 | W463AQ (ch2) | 00:1f:54:b2:9b:1d | 192.168.1.147 |
| cam5 | SL300 | | | |
## Network Architecture
- Camera network: 192.168.1.0/24 (isolated, no internet)
- Server NIC: enp2s0f1 @ 192.168.1.1
- WiFi AP: TP-Link RE315 @ 192.168.1.254
- DHCP range: 192.168.1.100-200
## RTSP URL Format
```
rtsp://admin:ocu?u3Su@<IP>/cam/realmonitor?channel=<CH>&subtype=0
```
- channel=1 for single-camera devices
- channel=1,2 for dual-camera devices (W463AQ)
- subtype=0 for main stream, subtype=1 for sub stream
## Camera Reset Procedures
### W461ASC (cam4)
1. Keep camera powered on
2. Reset button is on the back of the camera
3. Press and hold reset button for 30-60 seconds until chime sounds
### B463AJ (cam1)
1. Remove doorbell from mount
2. Locate reset button on the back
3. Press and hold until you hear chime reset sound
4. Reconnect via Lorex app as new device
### W463AQ (cam2/cam3)
1. Keep camera powered on
2. Rotate the lens upwards to reveal hidden reset button
3. Press and hold reset button until you hear audio prompt
4. Flashing green Smart Security Lighting confirms reset
5. Solid green = not fully reset, repeat if needed
### SL300 (cam5)
1. Keep camera powered on
2. Tilt camera lens upwards to reveal reset/microSD card cover
3. Remove the cover
4. Press and hold reset button until audio prompt
5. Replace cover quickly
6. Wait for green LED flash + audio confirmation
## Initial Setup
1. Temporarily enable internet for camera network:
```bash
sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE
sudo sysctl -w net.ipv4.ip_forward=1
```
2. Connect camera to "cams" WiFi network
3. Use Lorex app to configure camera (requires cloud - CCP middleman)
4. Get camera MAC from DHCP leases:
```bash
cat /var/lib/dnsmasq/dnsmasq.leases
```
5. Add DHCP reservation in `system.nix`:
```nix
dhcp-host = [
"aa:bb:cc:dd:ee:ff,192.168.1.XXX,camera_name"
];
```
6. Add MAC to firewall block list in `system.nix`:
```nix
iptables -A FORWARD -m mac --mac-source aa:bb:cc:dd:ee:ff -j DROP
```
7. Update camera IP in `frigate/default.nix` and enable
8. Deploy and disable internet:
```bash
nixos-rebuild switch --flake .#server --target-host server
sudo iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -o enp2s0f0 -j MASQUERADE
sudo sysctl -w net.ipv4.ip_forward=0
```
## Storage
| Path | Bind Mount | Contents |
|------|------------|----------|
| /var/lib/frigate | /data/frigate/lib | Database, recordings, clips |
## Notes
- Lorex cameras are cloud-only for configuration (no local web UI responds)
- RTSP works locally without internet
- Cameras phone home aggressively when internet is available - keep isolated
- Haswell CPU cannot hardware decode HEVC - using CPU decode
- Consider T400 GPU for hardware acceleration if scaling to more cameras
## Port Scan Results (W461ASC)
- 80/tcp - HTTP (non-responsive, proprietary)
- 554/tcp - RTSP (working)
- 8086/tcp - Proprietary
- 35000/tcp - Proprietary
## Planned Upgrades
Replace Lorex cameras with proper RTSP/ONVIF cameras for reliable Frigate integration.
| Current | Replacement | Price | Notes |
|---------|-------------|-------|-------|
| cam1 (B861AJ) | Reolink Video Doorbell WiFi | ~$120 | 5MP, wired power + WiFi, always-on |
| cam4 (W461ASC) | TP-Link Tapo C110 | ~$30 | 3MP, compact, window-friendly |
| cam2 + cam3 (W463AQ) | Reolink E1 Pro | ~$45 | 4MP, 355° pan |
| cam5 (SL300) | **Remove** | - | Obstructed, overlaps with cam4 |
**Total: ~$195**
### Reolink Video Doorbell WiFi
- URL: https://reolink.com/us/product/reolink-video-doorbell-wifi
- Model: SKU 2267808
- Resolution: 5MP (2560x1920 @ 20fps)
- Dimensions: Standard doorbell form factor
- Power: Hardwired 12-24VAC or DC 24V (always-on, no battery)
- Network: 2.4GHz/5GHz WiFi
- Protocols: RTSP, ONVIF, RTMP, HTTPS
- FOV: 180° diagonal (135° H, 100° V)
### TP-Link Tapo C110
- URL: https://www.tp-link.com/us/home-networking/cloud-camera/tapo-c110/
- Resolution: 3MP (2304x1296 @ 15fps)
- Dimensions: 2.66" x 2.15" x 3.89" (compact cube, similar to Lorex W461ASC)
- Power: 9V DC adapter
- Network: 2.4GHz WiFi
- Protocols: RTSP, ONVIF (officially supported NVR mode)
- RTSP URL: `rtsp://user:pass@IP:554/stream1` (main), `stream2` (sub)
- Frigate: Confirmed working - https://www.simonam.dev/tapo-c110-frigate-config/
### Reolink E1 Pro
- URL: https://reolink.com/us/product/e1-pro/
- Resolution: 4MP (2560x1440)
- Dimensions: ~4" dome with pan/tilt
- Power: 5V DC adapter
- Network: 2.4GHz/5GHz WiFi
- Protocols: RTSP, ONVIF
- Features: Pan 355°, Tilt 50°, person/pet detection
**Why replace Lorex:** Cloud-dependent config, no ONVIF, doorbell sleeps on battery, aggressive phone-home behavior requires network isolation.

View file

@ -0,0 +1,295 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.frigate;
nginx = config.modules.system.nginx;
domain = "ramos.codes";
user = config.sops.placeholder."RTSP_USER";
pass = config.sops.placeholder."RTSP_PASS";
privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") nginx.privateAllowCidrs + "\ndeny all;";
in
{
options.modules.system.frigate = {
enable = mkEnableOption "Enable Frigate NVR";
};
config = mkIf cfg.enable {
# Allow user to access frigate recordings via SSHFS
users.users.${config.user.name}.extraGroups = [ "frigate" ];
# go2rtc config with credentials from sops
sops.templates."go2rtc.yaml" = {
content = ''
rtsp:
listen: ":8554"
webrtc:
listen: ":8555"
streams:
cam1: "rtsp://${user}:${pass}@192.168.1.167/cam/realmonitor?channel=1&subtype=0#backchannel=1"
cam1_sub: "rtsp://${user}:${pass}@192.168.1.167/cam/realmonitor?channel=1&subtype=1"
cam2: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=1&subtype=0#backchannel=1"
cam2_sub: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=1&subtype=1"
cam3: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=2&subtype=0#backchannel=1"
cam3_sub: "rtsp://${user}:${pass}@192.168.1.147/cam/realmonitor?channel=2&subtype=1"
cam4: "rtsp://${user}:${pass}@192.168.1.194/cam/realmonitor?channel=1&subtype=0"
cam4_sub: "rtsp://${user}:${pass}@192.168.1.194/cam/realmonitor?channel=1&subtype=1"
'';
mode = "0444"; # go2rtc runs as dynamic user, needs read access
};
# go2rtc service using sops-templated config
services.go2rtc.enable = true;
systemd.services.go2rtc = {
serviceConfig.ExecStart = mkForce "${pkgs.go2rtc}/bin/go2rtc -config ${config.sops.templates."go2rtc.yaml".path}";
after = [ "sops-nix.service" ];
wants = [ "sops-nix.service" ];
};
services.frigate = {
enable = true;
package = pkgs.unstable.frigate;
hostname = "frigate.${domain}";
vaapiDriver = "i965"; # Haswell iGPU for H.264 decode
settings = {
mqtt.enabled = false;
ffmpeg = {
hwaccel_args = "preset-vaapi"; # VAAPI for H.264 substream detection
input_args = "preset-rtsp-restream"; # TCP transport for go2rtc
};
birdseye = {
mode = "continuous";
width = 1280;
height = 720;
quality = 8; # 8 - 31
};
motion = {
enabled = true;
};
detect = {
enabled = true;
min_initialized = 3;
max_disappeared = 25;
width = 1280;
height = 720;
};
audio = {
enabled = true;
max_not_heard = 30;
min_volume = 600;
listen = [
"glass"
"shatter"
"fire_alarm"
"boom"
"thump"
"siren"
"alarm"
"explosion"
"burst"
];
};
audio_transcription.enabled = false;
record = {
enabled = true;
continuous.days = 3; # Full 24/7 footage
motion.days = 7; # Motion segments after continuous expires
detections.retain = {
days = 14; # Any tracked object (person, car, etc.)
mode = "motion";
};
alerts.retain = {
days = 30; # Zone violations, loitering - important stuff
mode = "all";
};
};
snapshots = {
enabled = true;
retain = {
default = 3;
};
quality = 80;
};
cameras = {
cam1 = {
enabled = true;
ffmpeg.inputs = [
{
path = "rtsp://127.0.0.1:8554/cam1";
roles = [ "record" ];
}
{
path = "rtsp://127.0.0.1:8554/cam1_sub";
roles = [ "detect" "audio" ];
}
];
};
cam2 = {
enabled = true;
motion.enabled = false;
detect.enabled = false;
objects.mask = [ "0.969,0.078,0.846,0.075,0.845,0.034,0.97,0.037" ];
ffmpeg.inputs = [
{
path = "rtsp://127.0.0.1:8554/cam2";
roles = [ "record" ];
}
{
path = "rtsp://127.0.0.1:8554/cam2_sub";
roles = [ "detect" "audio" ];
}
];
};
cam3 = {
enabled = true;
motion.enabled = false;
detect.enabled = false;
ffmpeg.inputs = [
{
path = "rtsp://127.0.0.1:8554/cam3";
roles = [ "record" ];
}
{
path = "rtsp://127.0.0.1:8554/cam3_sub";
roles = [ "detect" "audio" ];
}
];
};
cam4 = {
enabled = true;
audio.enabled = false;
motion.mask = [ "0.811,0.109,0.954,0.111,0.959,0.065,0.811,0.055" ];
zones.zone1 = {
friendly_name = "lot";
coordinates = "0.299,0.438,0.191,0.951,0.453,0.964,0.453,0.437";
loitering_time = 10;
};
ffmpeg.inputs = [
{
path = "rtsp://127.0.0.1:8554/cam4";
roles = [ "record" ];
}
{
path = "rtsp://127.0.0.1:8554/cam4_sub";
roles = [ "detect" ];
}
];
};
};
classification = {
custom = {
"door" = {
enabled = true;
name = "door";
threshold = 0.8;
state_config = {
cameras = {
cam2.crop = [
0.8595647692717828
0.39901413156128707
0.9903488513256276
0.6315191663236775
];
cam3.crop = [
0.0008617338314475493
0.3909394833748086
0.12040036569190293
0.6034526066822848
];
};
motion = true;
};
};
"lot" = {
enabled = true;
name = "lot";
threshold = 0.8;
state_config = {
cameras = {
cam4.crop = [
0.2757899560295573
0.5156825410706086
0.4445399560295573
0.8156825410706086
];
};
motion = true;
};
};
};
};
};
};
# Add SSL to frigate's nginx virtualHost
services.nginx.virtualHosts."frigate.${domain}" = mkIf nginx.enable {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
extraConfig = privateAccessRules;
};
locations."/go2rtc/" = {
proxyPass = "http://127.0.0.1:1984/";
proxyWebsockets = true;
extraConfig = privateAccessRules;
};
};
# Frigate segment cache in RAM (reduces disk writes)
fileSystems."/var/cache/frigate" = {
device = "tmpfs";
fsType = "tmpfs";
options = [ "size=512M" "mode=0755" ];
};
systemd.tmpfiles.rules = [
# Set ownership after tmpfs mount
"d /var/cache/frigate 0750 frigate frigate -"
# Create log directories for Frigate API (NixOS uses journald, but API expects these)
"d /dev/shm/logs 0755 frigate frigate -"
"d /dev/shm/logs/frigate 0755 frigate frigate -"
"d /dev/shm/logs/nginx 0755 frigate frigate -"
"d /dev/shm/logs/go2rtc 0755 frigate frigate -"
];
# Pipe journald logs to files for Frigate GUI
systemd.services.frigate-log-pipe = {
description = "Pipe logs to /dev/shm for Frigate GUI";
wantedBy = [ "multi-user.target" ];
after = [ "frigate.service" "go2rtc.service" "nginx.service" ];
serviceConfig = {
Type = "simple";
Restart = "always";
ExecStart = pkgs.writeShellScript "frigate-log-pipe" ''
while true; do
${pkgs.systemd}/bin/journalctl -u frigate -n 500 -o cat > /dev/shm/logs/frigate/current 2>/dev/null
${pkgs.systemd}/bin/journalctl -u go2rtc -n 500 -o cat > /dev/shm/logs/go2rtc/current 2>/dev/null
${pkgs.systemd}/bin/journalctl -u nginx -n 500 -o cat > /dev/shm/logs/nginx/current 2>/dev/null
chown frigate:frigate /dev/shm/logs/*/current
sleep 5
done
'';
};
};
# Backup recordings/database
modules.system.backup = {
paths = [ "/var/lib/frigate" ];
};
};
}

View file

@ -0,0 +1,23 @@
{ config, ... }:
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.${config.user.name} = {
imports = [
../../../../../user
../../../../../user/home.nix
../../../../../user/modules
];
home.stateVersion = "25.11";
# Machine-specific modules
modules.user = {
neovim.enable = false;
vim.enable = true;
tmux.enable = false;
utils.dev.enable = true;
};
};
}

View file

@ -0,0 +1,59 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.immich;
nginx = config.modules.system.nginx;
domain = "ramos.codes";
port = 2283;
privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") nginx.privateAllowCidrs + "\ndeny all;";
in
{
options.modules.system.immich = {
enable = mkEnableOption "Immich Photo Server";
};
config = mkIf cfg.enable {
# Bind mount from /data
systemd.tmpfiles.rules = [
"d /data/immich 0750 immich immich -"
"d /data/postgresql 0750 postgres postgres -"
];
fileSystems."/var/lib/immich" = {
device = "/data/immich";
fsType = "none";
options = [ "bind" ];
};
fileSystems."/var/lib/postgresql" = {
device = "/data/postgresql";
fsType = "none";
options = [ "bind" ];
};
services.immich = {
enable = true;
port = port;
host = "127.0.0.1";
mediaLocation = "/var/lib/immich";
machine-learning.enable = false;
};
modules.system.backup.paths = [
"/var/lib/immich"
"/var/lib/postgresql"
];
services.nginx.virtualHosts."photos.${domain}" = mkIf nginx.enable {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString port}";
proxyWebsockets = true;
extraConfig = privateAccessRules;
};
};
};
}

View file

@ -0,0 +1,163 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.nginx;
domain = "ramos.codes";
privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") cfg.privateAllowCidrs + "\ndeny all;";
in
{
options.modules.system.nginx = {
enable = mkEnableOption "Nginx Reverse Proxy";
privateAllowCidrs = mkOption {
type = types.listOf types.str;
default = [
"192.168.0.0/24"
"10.8.0.0/24"
];
description = ''
CIDR ranges allowed to access private vhosts (LAN + WireGuard).
'';
};
};
config = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ 80 443 ];
systemd.services.nginx.serviceConfig.LimitNOFILE = 65536;
environment.etc."fail2ban/filter.d/nginx-404.conf".text = ''
[Definition]
failregex = ^<HOST> - .+ "(GET|POST|HEAD|PUT|DELETE|PATCH) .+ HTTP/[0-9.]+" 404
ignoreregex =
'';
environment.etc."fail2ban/filter.d/nginx-401.conf".text = ''
[Definition]
failregex = ^<HOST> - .+ "(GET|POST|HEAD|PUT|DELETE|PATCH) .+ HTTP/[0-9.]+" 401
ignoreregex =
'';
services.fail2ban.jails.nginx-404 = ''
enabled = true
filter = nginx-404
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 10m
bantime = 24h
'';
services.fail2ban.jails.nginx-401 = ''
enabled = true
filter = nginx-401
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 10m
bantime = 24h
'';
security.acme = {
acceptTerms = true;
defaults.email = config.user.email;
certs."${domain}" = {
domain = "*.${domain}";
dnsProvider = "namecheap";
environmentFile = "/var/lib/acme/namecheap.env";
group = "nginx";
};
};
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
eventsConfig = "worker_connections 4096;";
# Catch-all default - friendly error for unknown subdomains
virtualHosts."_" = {
default = true;
useACMEHost = domain;
forceSSL = true;
locations."/" = {
return = "404 'Not Found: This subdomain does not exist.'";
extraConfig = ''
add_header Content-Type text/plain;
'';
};
};
virtualHosts."wg.${domain}" = {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString config.modules.system.wstunnel.listenPort}";
proxyWebsockets = true;
extraConfig = ''
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
'';
};
};
virtualHosts."ai.${domain}" = let
apiKeyAuth = ''
set $api_key "";
if ($http_authorization ~* "^Bearer (.+)$") {
set $api_key $1;
}
if ($api_key = "") {
return 401 '{"error": "Missing Authorization header"}';
}
include ${config.sops.templates."nginx-ai-auth.conf".path};
'';
in {
useACMEHost = domain;
forceSSL = true;
# Web UI + llama.cpp API (browser, /v1/* calls from the UI)
# Auth handled by llama.cpp itself (--api-key flag)
locations."/" = {
proxyPass = "http://192.168.0.23:8000";
proxyWebsockets = true;
};
# Llama Stack API (opencode, programmatic clients)
# Clients use baseURL: https://ai.ramos.codes/stack/v1
locations."/stack/v1/" = {
proxyPass = "http://192.168.0.23:8321/v1/";
proxyWebsockets = true;
extraConfig = apiKeyAuth + ''
proxy_read_timeout 300s;
proxy_send_timeout 300s;
'';
};
# MCP servers (namespaced, for llama.cpp web UI + direct access)
locations."/mcp/web_search/" = {
proxyPass = "http://192.168.0.23:8002/";
proxyWebsockets = true;
extraConfig = ''
include ${config.sops.templates."nginx-mcp-auth.conf".path};
proxy_read_timeout 300s;
proxy_send_timeout 300s;
'';
};
};
virtualHosts."comfy.${domain}" = {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "http://192.168.0.23:8188";
proxyWebsockets = true;
extraConfig = privateAccessRules;
};
};
};
};
}

View file

@ -0,0 +1,136 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.sandpack;
domain = "ramos.codes";
privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") config.modules.system.nginx.privateAllowCidrs + "\ndeny all;";
staticBrowserServer = pkgs.stdenvNoCC.mkDerivation (finalAttrs: let
pnpm = pkgs.pnpm_10;
in {
pname = "static-browser-server";
version = "1.0.6";
src = pkgs.fetchFromGitHub {
owner = "LibreChat-AI";
repo = "static-browser-server";
rev = "30de7ae4ebf5433acc0fb640649fb77426a79e04";
hash = "sha256-OVAGnoh7KRmTPY2bXE0kvCMiPx3tXAooDa8n8ujugYM=";
};
patches = [ ./pnpm-lock.patch ];
pnpmDeps = pkgs.fetchPnpmDeps {
inherit (finalAttrs) pname version src patches;
pnpm = pnpm;
fetcherVersion = 3;
hash = "sha256-+Gz8tQy4rkoi365To9GI6sShPTjuKEmZxtV5mEB2UYk=";
};
nativeBuildInputs = [
pkgs.makeWrapper
pkgs.nodejs
pkgs.pnpmConfigHook
pnpm
];
buildPhase = ''
runHook preBuild
pnpm build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/libexec/static-browser-server $out/bin
cp -r out $out/libexec/
pnpm exec esbuild \
./servers/demo-server.ts \
--bundle \
--platform=node \
--format=cjs \
--outfile=$out/libexec/static-browser-server/demo-server.js
makeWrapper ${pkgs.nodejs}/bin/node $out/bin/static-browser-server \
--add-flags $out/libexec/static-browser-server/demo-server.js
runHook postInstall
'';
});
in
{
options.modules.system.sandpack = {
enable = mkEnableOption "Sandpack services";
};
config = mkIf cfg.enable {
virtualisation.oci-containers = {
backend = "podman";
containers.sandpack-bundler = {
image = "ghcr.io/librechat-ai/codesandbox-client/bundler:latest";
ports = [ "127.0.0.1:4333:80" ];
};
};
systemd.services.sandpack-preview = {
description = "Sandpack static preview server";
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${staticBrowserServer}/bin/static-browser-server";
WorkingDirectory = "${staticBrowserServer}/libexec/static-browser-server";
Restart = "always";
RestartSec = 5;
DynamicUser = true;
Environment = [
"HOST=127.0.0.1"
"PORT=4324"
];
};
};
services.nginx.virtualHosts."bundler.${domain}" = {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:4333";
extraConfig = ''
${privateAccessRules}
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "3600" always;
if ($request_method = OPTIONS) {
return 204;
}
'';
};
};
services.nginx.virtualHosts."preview.${domain}" = {
useACMEHost = domain;
forceSSL = true;
serverAliases = [ "~^.+-preview\\.ramos\\.codes$" ];
locations."/" = {
proxyPass = "http://127.0.0.1:4324";
extraConfig = ''
${privateAccessRules}
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "3600" always;
if ($request_method = OPTIONS) {
return 204;
}
'';
};
};
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.tor;
in
{
options.modules.system.tor = {
enable = mkEnableOption "Tor";
};
config = mkIf cfg.enable {
services.tor = {
enable = true;
client = {
enable = true;
# SOCKS proxy on 127.0.0.1:9050
};
settings = {
ControlPort = 9051;
CookieAuthentication = true;
CookieAuthFileGroupReadable = true;
DataDirectoryGroupReadable = true;
};
};
};
}

View file

@ -0,0 +1,72 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.webdav;
domain = "ramos.codes";
privateAccessRules = concatMapStringsSep "\n" (cidr: "allow ${cidr};") config.modules.system.nginx.privateAllowCidrs + "\ndeny all;";
in
{
options.modules.system.webdav = {
enable = mkEnableOption "WebDAV server for phone backups";
directory = mkOption {
type = types.path;
default = "/var/lib/seedvault";
description = "Directory to store backups";
};
};
config = mkIf cfg.enable {
# Create backup directory
systemd.tmpfiles.rules = [
"d ${cfg.directory} 0750 webdav webdav -"
];
services.webdav = {
enable = true;
# Credentials in /var/lib/webdav/env:
# WEBDAV_USERNAME=seedvault
# WEBDAV_PASSWORD=your-secure-password
environmentFile = "/var/lib/webdav/env";
settings = {
address = "127.0.0.1";
port = 8090;
directory = cfg.directory;
behindProxy = true;
permissions = "CRUD"; # Create, Read, Update, Delete
users = [
{
username = "{env}WEBDAV_USERNAME";
password = "{env}WEBDAV_PASSWORD";
}
];
};
};
services.nginx.virtualHosts."backup.${domain}" = {
useACMEHost = domain;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8090";
extraConfig = ''
${privateAccessRules}
proxy_set_header Host $host;
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;
# WebDAV needs these
proxy_pass_request_headers on;
proxy_set_header Destination $http_destination;
# Large file uploads for backups
client_max_body_size 0;
proxy_request_buffering off;
'';
};
};
};
}

View file

@ -0,0 +1,107 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.wireguard;
in
{
options.modules.system.wireguard = {
enable = mkEnableOption "WireGuard VPN";
address = mkOption {
type = types.str;
default = "10.8.0.1/24";
description = "WireGuard interface address with CIDR";
};
subnet = mkOption {
type = types.str;
default = "10.8.0.0/24";
description = "WireGuard subnet used for peer allocations";
};
listenPort = mkOption {
type = types.port;
default = 51820;
description = "WireGuard UDP listen port";
};
privateKeyFile = mkOption {
type = types.str;
default = "/var/lib/wireguard/server.key";
description = "Path to WireGuard server private key";
};
peers = mkOption {
type = types.listOf (types.submodule ({ ... }: {
options = {
publicKey = mkOption {
type = types.str;
description = "Peer public key";
};
allowedIPs = mkOption {
type = types.listOf types.str;
description = "Allowed IPs for peer, usually a single /32";
};
presharedKeyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional preshared key file";
};
persistentKeepalive = mkOption {
type = types.nullOr types.int;
default = 25;
description = "Persistent keepalive interval seconds";
};
};
}));
default = [ ];
description = "WireGuard peers";
};
};
config = mkIf cfg.enable {
networking.firewall.allowedUDPPorts = [ cfg.listenPort ];
networking.firewall.trustedInterfaces = [ "wg0" ];
networking.nat.internalInterfaces = mkAfter [ "wg0" ];
systemd.tmpfiles.rules = [
"d /var/lib/wireguard 0700 root root -"
];
systemd.services.wireguard-generate-key = {
description = "Generate WireGuard server key if missing";
before = [ "wireguard-wg0.service" ];
wantedBy = [ "wireguard-wg0.service" ];
serviceConfig = {
Type = "oneshot";
};
path = with pkgs; [ wireguard-tools coreutils ];
script = ''
set -euo pipefail
if [ ! -s "${cfg.privateKeyFile}" ]; then
umask 077
wg genkey | tee "${cfg.privateKeyFile}" | wg pubkey > /var/lib/wireguard/server.pub
elif [ ! -s /var/lib/wireguard/server.pub ]; then
umask 077
wg pubkey < "${cfg.privateKeyFile}" > /var/lib/wireguard/server.pub
fi
'';
};
networking.wireguard.interfaces.wg0 = {
ips = [ cfg.address ];
listenPort = cfg.listenPort;
privateKeyFile = cfg.privateKeyFile;
peers = map (peer: {
inherit (peer) publicKey allowedIPs;
presharedKeyFile = peer.presharedKeyFile;
persistentKeepalive = peer.persistentKeepalive;
}) cfg.peers;
};
};
}

View file

@ -0,0 +1,37 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.wstunnel;
in
{
options.modules.system.wstunnel = {
enable = mkEnableOption "wstunnel WebSocket transport for WireGuard";
listenPort = mkOption {
type = types.port;
default = 8080;
description = "Local port wstunnel server listens on (nginx proxies to this)";
};
wireguardPort = mkOption {
type = types.port;
default = 51820;
description = "Local WireGuard port to forward traffic to";
};
};
config = mkIf cfg.enable {
systemd.services.wstunnel = {
description = "wstunnel WebSocket server for WireGuard transport";
after = [ "network.target" "wireguard-wg0.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.wstunnel}/bin/wstunnel server ws://127.0.0.1:${toString cfg.listenPort} --restrict-to 127.0.0.1:${toString cfg.wireguardPort}";
Restart = "on-failure";
RestartSec = 5;
DynamicUser = true;
};
};
};
}

View file

@ -0,0 +1,253 @@
{ pkgs, lib, config, ... }:
{ system.stateVersion = "25.11";
imports = [ ./modules ];
modules.system.sops.enable = true;
# Camera RTSP credentials (used by frigate/go2rtc)
sops.secrets = let
cameras = { sopsFile = ../../../secrets/system/cameras.yaml; };
llama = { sopsFile = ../../../secrets/system/llama.yaml; };
in {
"RTSP_USER" = cameras;
"RTSP_PASS" = cameras;
"LLAMA_API_KEY" = llama // { owner = config.user.name; };
};
# API key auth for ai.ramos.codes — nginx validates Bearer token against sops secret
sops.templates."nginx-ai-auth.conf" = {
content = ''
if ($api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") {
return 401 '{"error": "Invalid API key"}';
}
'';
owner = "nginx";
};
# MCP endpoint auth — validates X-API-Key header
sops.templates."nginx-mcp-auth.conf" = {
content = ''
if ($http_x_api_key != "${config.sops.placeholder."LLAMA_API_KEY"}") {
return 401 '{"error": "Unauthorized"}';
}
'';
owner = "nginx";
};
modules.system = {
nginx = {
enable = true;
};
sandpack.enable = true;
forgejo.enable = true;
frigate.enable = true;
immich.enable = true;
webdav.enable = false;
wstunnel.enable = true;
wireguard = {
enable = true;
peers = [
{
publicKey = "HRFsVXn3jeqKQLQIl0cB6KC/qia7M1gQf2lqG5HDxF8=";
allowedIPs = [ "10.8.0.2/32" ];
}
{
publicKey = "eY2JTwuvzLLVnyhUTop0I+7qO2swFSjo12So4Yzkamk=";
allowedIPs = [ "10.8.0.3/32" ];
}
];
};
# bitcoin = {
# enable = true;
# electrum.enable = true;
# clightning.enable = true;
# };
backup = {
enable = true;
recipients = [
"${config.user.keys.age.yubikey}"
"${config.machines.keys.desktop.ssh}"
];
paths = [ "/root/.config/rclone" ];
destination = "gdrive:backups/server";
schedule = "daily";
keepLast = 2;
};
};
users.users = {
${config.user.name} = {
isNormalUser = true;
extraGroups = config.user.groups;
openssh.authorizedKeys.keys = [
"${config.machines.keys.desktop.ssh}"
];
};
};
nix = {
channel.enable = false;
package = pkgs.nixVersions.stable;
extraOptions = "experimental-features = nix-command flakes";
settings = {
auto-optimise-store = true;
trusted-users = [ "${config.user.name}" ];
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
};
boot.loader = {
timeout = 3;
grub = {
enable = true;
devices = [ "nodev" ];
efiSupport = true;
efiInstallAsRemovable = true; # HP Z230 UEFI ignores custom boot entries
configurationLimit = 5;
splashImage = null;
};
efi = {
canTouchEfiVariables = false; # Not needed with efiInstallAsRemovable
efiSysMountPoint = "/boot";
};
};
environment.systemPackages = with pkgs; [
wget
git
vim
htop
dmidecode
];
security.sudo = {
wheelNeedsPassword = false;
execWheelOnly = true;
};
time = {
timeZone = "America/New_York";
hardwareClockInLocalTime = true;
};
services.timesyncd = lib.mkDefault {
enable = true;
servers = [
"0.pool.ntp.org"
"1.pool.ntp.org"
"2.pool.ntp.org"
"3.pool.ntp.org"
];
};
i18n.defaultLocale = "en_US.UTF-8";
console.font = "Lat2-Terminus16";
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
networking = {
hostName = "server";
useDHCP = false;
nat = {
enable = true;
internalInterfaces = [ "enp2s0f1" ];
externalInterface = "enp2s0f0";
};
interfaces.enp2s0f0 = {
ipv4.addresses = [{
address = "192.168.0.154";
prefixLength = 24;
}];
};
# Camera network - isolated, no gateway
interfaces.enp2s0f1 = {
ipv4.addresses = [{
address = "192.168.1.1";
prefixLength = 24;
}];
};
defaultGateway = "192.168.0.1";
nameservers = [ "1.1.1.1" "8.8.8.8" ];
firewall = {
enable = true;
allowedTCPPorts = [ 22 ];
allowedUDPPorts = [ 53 67 ]; # DNS + DHCP
# extraCommands = ''
# # Block camera MACs from forwarding (instant DROP, no timeouts)
# iptables -A FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP # cam4
# iptables -A FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP # cam2/cam3
# iptables -A FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP # cam1
# '';
# extraStopCommands = ''
# iptables -D FORWARD -m mac --mac-source 00:1f:54:c2:d1:b1 -j DROP || true
# iptables -D FORWARD -m mac --mac-source 00:1f:54:b2:9b:1d -j DROP || true
# iptables -D FORWARD -m mac --mac-source 00:1f:54:a9:81:d1 -j DROP || true
# '';
};
};
services.dnsmasq = {
enable = true;
settings = {
# All *.ramos.codes subdomains -> local server
address = "/.ramos.codes/192.168.0.154";
# Except www, http, https and bare domain -> forward to upstream
server = [
"/www.ramos.codes/1.1.1.1"
"/http.ramos.codes/1.1.1.1"
"/https.ramos.codes/1.1.1.1"
"/ramos.codes/1.1.1.1"
"1.1.1.1"
"8.8.8.8"
];
cache-size = 1000;
# Camera network DHCP (isolated - no gateway = no internet)
interface = [ "enp2s0f1" "wg0" ];
bind-interfaces = true;
dhcp-range = "192.168.1.100,192.168.1.200,24h";
# Static DHCP reservations for cameras
dhcp-host = [
"00:1f:54:c2:d1:b1,192.168.1.194,cam4"
"00:1f:54:b2:9b:1d,192.168.1.147,cam2"
"00:1f:54:a9:81:d1,192.168.1.167,cam1"
];
};
};
systemd.services.dnsmasq = {
after = [ "wireguard-wg0.service" ];
wants = [ "wireguard-wg0.service" ];
};
services.fail2ban = {
enable = true;
maxretry = 5;
bantime = "1h";
ignoreIP = [
"127.0.0.1/8"
"192.168.0.0/24"
"10.8.0.0/24"
];
};
services.openssh = {
enable = true;
startWhenNeeded = true;
settings = {
X11Forwarding = false;
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
}

View file

@ -0,0 +1,15 @@
{ inputs, ... }:
{
imports = [
inputs.nixos-wsl.nixosModules.wsl
(import ./modules/wsl)
inputs.home-manager.nixosModules.home-manager
{ home-manager.sharedModules = [ inputs.sops-nix.homeManagerModules.sops ]; }
(import ./modules/home-manager)
../../../user
../../keys
../../modules/sops
./system.nix
];
}

View file

@ -0,0 +1,5 @@
{
imports = [
./home.nix
];
}

View file

@ -0,0 +1,24 @@
{ config, ... }:
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.${config.user.name} = {
imports = [
../../../../../user
../../../../../user/home.nix
../../../../../user/modules
];
home.stateVersion = "23.11";
# Machine-specific modules
modules.user = {
utils = {
dev.enable = true;
email.enable = true;
irc.enable = true;
};
};
};
}

View file

@ -0,0 +1,5 @@
{
imports = [
./wsl.nix
];
}

View file

@ -0,0 +1,20 @@
{ config, lib, ... }:
{
imports = [ ../../../../../user ];
wsl = rec {
enable = true;
defaultUser = lib.mkDefault config.user.name;
useWindowsDriver = true;
wslConf = {
user.default = lib.mkDefault defaultUser;
boot.command = "cd";
network = {
hostname = "${config.networking.hostName}";
generateHosts = true;
};
};
};
}

View file

@ -0,0 +1,75 @@
{ pkgs, lib, config, ... }:
{
system.stateVersion = "23.11";
boot.isContainer = true;
users.users = {
${config.user.name} = {
isNormalUser = true;
extraGroups = config.user.groups;
openssh.authorizedKeys.keys = [
"${config.user.keys.ssh.yubikey}"
];
};
};
nix = {
channel.enable = false;
package = pkgs.nixVersions.stable;
extraOptions = ''
experimental-features = nix-command flakes
'';
settings = {
auto-optimise-store = true;
trusted-users = [ "${config.user.name}" ];
};
gc = {
automatic = true;
dates = "daily";
options = "--delete-older-than 7d";
};
};
security.sudo = {
wheelNeedsPassword = false;
execWheelOnly = true;
};
time = {
timeZone = "America/New_York";
};
i18n.defaultLocale = "en_US.UTF-8";
console = {
font = "Lat2-Terminus16";
useXkbConfig = true;
};
networking = {
hostName = "wsl";
useDHCP = lib.mkDefault true;
firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];
};
};
services = {
openssh = {
enable = true;
startWhenNeeded = true;
settings = {
X11Forwarding = false;
PasswordAuthentication = false;
};
};
timesyncd = lib.mkDefault {
enable = true;
servers = [
"time.windows.com"
];
};
};
}

View file

@ -0,0 +1,27 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.docker;
in
{
options.modules.system.docker = { enable = mkEnableOption "Enable Docker"; };
config = mkIf cfg.enable {
virtualisation.docker = {
enable = true;
# Explicit storage driver for ext4/xfs filesystems
storageDriver = "overlay2";
};
# Add docker package to system packages
environment.systemPackages = with pkgs; [
docker
docker-compose
];
# Add user to docker group
users.users.${config.user.name}.extraGroups = [ "docker" ];
};
}

View file

@ -0,0 +1,30 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.system.sops;
in
{
options.modules.system.sops = { enable = mkEnableOption "Enable sops-nix"; };
config = mkIf cfg.enable {
# Smartcard daemon for Yubikey (GPG, etc.)
services.pcscd.enable = true;
services.udev.packages = [ pkgs.yubikey-personalization ];
environment.systemPackages = with pkgs; [
age
sops
];
# Per-machine age key for system secrets (boot-time, unattended)
# This is the sops-nix default path
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
# Symlink for root so `sudo sops` finds the key automatically
systemd.tmpfiles.rules = [
"d /root/.config/sops/age 0700 root root -"
"L+ /root/.config/sops/age/keys.txt - - - - /var/lib/sops-nix/key.txt"
];
};
}

323
user/bookmarks/default.nix Normal file
View file

@ -0,0 +1,323 @@
[
{
name = "toolbar";
toolbar = true;
bookmarks = [
{
name = "ArchWiki";
url = "https://wiki.archlinux.org/";
tags = [ "dev" "linux" "docs" ];
}
{
name = "NixOS";
bookmarks = [
{
name = "Nixpkgs";
url = "https://search.nixos.org/packages";
tags = [ "nix" "dev" "linux" ];
keyword = "pkgs";
}
{
name = "Home Manager";
bookmarks = [
{
name = "Home Manager Option Docs";
url = "https://nix-community.github.io/home-manager/options.xhtml";
tags = [ "nix" "dev" "linux" ];
keyword = "hm";
}
{
name = "Home Manager Option Search";
url = "https://home-manager-options.extranix.com";
tags = [ "nix" "dev" "linux" ];
keyword = "hm";
}
];
}
{
name = "Nix Docs";
bookmarks = [
{
name = "nix.dev";
url = "https://nix.dev";
tags = [ "nix" "dev" "docs" ];
keyword = "nix";
}
];
}
];
}
{
name = "Gentoo Wiki";
url = "https://wiki.gentoo.org";
tags = [ "dev" "linux" "docs" ];
keyword = "gentoo";
}
{
name = "Email";
bookmarks = [
{
name = "Gmail";
url = "https://gmail.com";
tags = [ "google" "email" ];
keyword = "gmail";
}
{
name = "ProtonMail";
url = "https://mail.protonmail.com";
tags = [ "email" "personal" "work" ];
keyword = "email";
}
];
}
{
name = "Work";
bookmarks = [
{
name = "Outlook";
url = "https://outlook.office365.com";
tags = [ "work" "email" "microsoft" ];
keyword = "work";
}
{
name = "Teams";
url = "https://teams.microsoft.com";
tags = [ "work" "microsoft" ];
keyword = "teams";
}
];
}
{
name = "Youtube";
url = "https://youtube.com";
tags = [ "social" "entertainment" "google" ];
keyword = "youtube";
}
{
name = "Substack";
url = "https://substack.com";
tags = [ "social" "blogging" "personal" ];
keyword = "blog";
}
{
name = "Amazon";
url = "https://amazon.com";
tags = [ "shopping" "amazon" ];
keyword = "amazon";
}
{
name = "Social";
bookmarks = [
{
name = "Twitter";
url = "https://x.com";
tags = [ "social" "forum" ];
keyword = "x";
}
{
name = "Reddit";
url = "https://reddit.com";
tags = [ "social" "forum" ];
keyword = "reddit";
}
{
name = "Twitch";
url = "https://twitch.com";
tags = [ "social" "entertainment" "amazon" ];
keyword = "twitch";
}
{
name = "Nostr";
url = "https://primal.net";
tags = [ "social" "nostr" "bitcoin" ];
keyword = "nostr";
}
];
}
{
name = "ChatGPT";
url = "https://chat.openai.com";
tags = [ "dev" "ai" "microsoft" ];
keyword = "ai";
}
{
name = "Dev";
bookmarks = [
{
name = "Github";
url = "https://github.com";
tags = [ "dev" "work" "personal" "microsoft" ];
keyword = "git";
}
{
name = "Gist";
url = "https://gist.github.com";
tags = [ "dev" "work" "personal" "microsoft" "blogging" ];
keyword = "gist";
}
{
name = "Stack Overflow";
url = "https://stackoverflow.com";
tags = [ "dev" "work" "forum" ];
}
{
name = "Learning";
bookmarks = [
{
name = "Coding";
bookmarks = [
{
name = "Leetcode";
url = "https://leetcode.com";
}
{
name = "CodeWars";
url = "https://codewars.com";
}
];
}
{
name = "Projects";
bookmarks = [
{
name = "Linux From Scratch";
url = "https://linuxfromscratch.org/lfs/view/stable/index.html";
}
];
}
];
}
{
name = "Documentation";
bookmarks = [
{
name = "MDN";
url = "https://developer.mozilla.org";
tags = [ "dev" "docs" ];
keyword = "mdn";
}
{
name = "DevDocs";
url = "https://devdocs.io";
tags = [ "dev" "docs" ];
keyword = "docs";
}
{
name = "Linux Kernel";
url = "https://docs.kernel.org";
}
{
name = "References";
bookmarks = [
{
name = "ASCII Table";
url = "https://asciitable.com";
tags = [ "dev" ];
keyword = "ascii";
}
{
name = "Regex Cheat Sheet";
url = "https://rexegg.com/regex-quickstart.php";
tags = [ "dev" ];
keyword = "regex";
}
];
}
];
}
{
name = "Tools";
bookmarks = [
{
name = "GitBook";
url = "https://gitbook.com";
tags = [ "dev" "docs" ];
}
{
name = "Namecheap";
url = "https://namecheap.com";
tags = [ "dev" "shopping" "hosting" ];
}
{
name = "LetsEncrypt";
url = "https://letsencrypt.com";
tags = [ "dev" "hosting" ];
}
{
name = "Gitea";
url = "https://gitea.com";
tags = [ "dev" "hosting" ];
}
{
name = "Hosting";
bookmarks = [
{
name = "DigitalOcean";
url = "https://digitalocean.com";
tags = [ "dev" "hosting" ];
}
{
name = "Supabase";
url = "https://supabase.com";
tags = [ "dev" "hosting" ];
}
{
name = "Vercel";
url = "https://vercel.com";
tags = [ "dev" "hosting" ];
}
{
name = "AWS";
url = "https://aws.amazon.com";
tags = [ "dev" "hosting" ];
}
{
name = "Azure";
url = "https://azure.microsoft.com";
tags = [ "dev" "hosting" ];
}
{
name = "Firebase";
url = "https://firebase.google.com";
tags = [ "dev" "hosting" ];
}
];
}
];
}
];
}
{
name = "Financials";
bookmarks = [
{
name = "Fidelity";
url = "https://fidelity.com";
tags = [ "banking" ];
keyword = "bank";
}
{
name = "Chase";
url = "https://chase.com";
tags = [ "banking" ];
}
{
name = "Wells Fargo";
url = "https://wellsfargo.com";
tags = [ "banking" ];
}
{
name = "Crapto";
bookmarks = [
{
name = "Coinbase";
url = "https://coinbase.com";
tags = [ "banking" ];
}
];
}
];
}
];
}
]

View file

@ -0,0 +1 @@
AGE-PLUGIN-YUBIKEY-1C8CXVQVZUK7AV2CFWEKFP

20
user/default.nix Normal file
View file

@ -0,0 +1,20 @@
{ lib, pkgs, ... }:
with lib;
{
options = {
user = mkOption {
description = "User Configurations";
type = types.attrs;
default = with pkgs; rec {
name = "bryan";
email = "bryan@ramos.codes";
shell = bash;
keys = import ./keys { inherit lib; };
groups = [ "wheel" "networkmanager" "home-manager" "input" ];
bookmarks = import ./bookmarks;
};
};
};
}

39
user/home.nix Normal file
View file

@ -0,0 +1,39 @@
{ lib, pkgs, config, ... }:
let
pass = pkgs.pass.withExtensions (exts: with exts; [
pass-audit
pass-otp
pass-update
]);
in
{
programs.home-manager.enable = true;
home.username = config.user.name;
home.homeDirectory = "/home/${config.user.name}";
# Essential packages for all users
home.packages = with pkgs; [
pass
wget curl fastfetch fd
unzip zip rsync
calc calcurse
age
sops
];
programs.bash.shellAliases = {
cal = "${pkgs.calcurse}/bin/calcurse";
calendar = "${pkgs.calcurse}/bin/calcurse";
};
# Default modules for all users (machines can override with mkForce false)
modules.user = {
bash.enable = lib.mkDefault true;
git.enable = lib.mkDefault true;
neovim.enable = lib.mkDefault true;
security.gpg.enable = lib.mkDefault true;
};
}

View file

@ -0,0 +1 @@
age1yubikey1qfapxqnnkh92zkgayzzm9n0gtpkwaqcvrzy4d4xa4rxnjua8vjhy72hh9r9

33
user/keys/default.nix Normal file
View file

@ -0,0 +1,33 @@
{ lib }:
with builtins;
let
extractName = filename:
let
# Remove .key extension
noKey = lib.removeSuffix ".key" filename;
# Remove .pub/.priv/.public/.private markers
noMarkers = replaceStrings
[ ".pub" ".priv" ".public" ".private" ]
[ "" "" "" "" ]
noKey;
in noMarkers;
constructKeys = dir: (
listToAttrs (
map (subdir: {
name = subdir;
value = listToAttrs (
map (file: {
name = extractName file;
value = readFile "${dir}/${subdir}/${file}";
}) (filter (file:
(readDir "${dir}/${subdir}").${file} == "regular" &&
lib.hasSuffix ".key" file
) (attrNames (readDir "${dir}/${subdir}")))
);
}) (filter (node: (readDir dir).${node} == "directory") (attrNames (readDir dir)))
)
);
in
constructKeys ./.

53
user/keys/pgp/company.pub.key Executable file
View file

@ -0,0 +1,53 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
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
uWsnbjV0Rg==
=56r2
-----END PGP PUBLIC KEY BLOCK-----

121
user/keys/pgp/work.pub.key Executable file
View file

@ -0,0 +1,121 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
Vv2c5XcFneMcDVBQ28VlwQ2fbIoDokz3Iw==
=my/o
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,109 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
1hbflYLyMYo1BxGR6bPS9gJA2w==
=5uun
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJM1HutPcWXdeTaAXY7ha8SlgeZFtLJGwNa3Kd/DL/R38fq5+fkh3iCoHgv+iiKcordtVTMhbOsHhz3H+Jm274c=

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXYU5c7AUD5tQQdpzQ73yy3ti3R7dArZ+f/wETN7L2Z2Hw6zo6hDid9/Q4yxdgM/FlTj/Ok2DHBWqxJsEe3S4shwsT9l2qJatjdcUK6zH3/0nFPxGYIaByj87aZ+5dwMoWNGlioPWciUdKeovvau1PwvdBxPabHHap6nwC9yPaSIVbZi4GgYv/zEvOB4LVYLuxLqr0pPdMNz1ddjmjsQCq5alC33jSZWkABERw3GlF02dNHbUq6cZlFq9BudbNWBQ8zFgj/C8amK4DHUSeU8w+ckTmO5PjDjINOnFr8kytDap+/5AQ6kr618evJ2JCwnBj6txb3SVGhcvn3/DJjf2H7flVhZEWIMEMu7452SXfz9mxp3Vu3UMJkjHUj6Lxl302M318k9j+w1fa8EHO7OQHQZajNKrEP5/UK2CDfpP2KIybX5HnEqBcEqoSKhRt7ytNX6VGzURk3/mmk9L+An5z7ve+zqlgNOA8uaIoebB4476+n5pGiNIedO3FRjPofEidYjf5NTZ9YDpqFc5KbfbhduuP63G/kqmgTxXMuTsWINY2xKEc0BPnlEGfezMN+eQpwWINOUxW1ZEk3OYMvC91EndbVwxVbm3aze9894T3+wVTipJ88xARCQeQpu1eaWDSaNduD+8LAouQiTA4whM+jBEeQoWZe6Wf6W4tBeCZ6Q==

View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDl4895aB9P5p/lp8Hq5rHun4clvhyTSHFi3U2d6OOBoW5Fm+VcQnW/xbjmCBsXk5BdiowsBxQhwnzdfz/KJL7J5RobomUEaVRwb9UwT88eJveLp14BG8j2J3SjfyhrCX+4jkPx0bPQk1HGcuYY+tPEXf1q/ps88Dhu0CARBIzYQOTYY6b1qWzxpDoFZGHjKG8g5iY6FIu65yKKvvVy1f8IgZ3l3IpwBWVamxgkTcYY0QYSrmzo1n7TXxwrWbvenAqBsQ0cBPs+gVa3uIr+1TJl0Az5SElBVGu3LvUdlk58trtPUj6TQR3YUkg7Vjll7WHOdqhux5ZQNhjkOsHerf0Tw86e6cEzgeTuIbQHIb0LcsUunwKcuh2+au7RO599cvHn0+xZE5MZBxloDDaJ3JsiliM8kyPP/U3ERj03cWLW7BqbT+sfjAOl21RCzk0iQxk1wt/8VmtCr9Adv7IyrtaYvf/bwRP+g+9ldmzKGt8Mdb605uVzZ70H/LLm17f40Te+QHaex5by/6p6cuwEEZtgIg53Wpglu0rA6UxrBfQEHKl/Jt3FLeE0mnEyYkkR2MnHNtyWRIXtuqYZMAm2Ub1pFHH7jQV1gGiDVTw6a2eIwK21a/hXtRjFUpFd1nB1n+KNfJBE4zT3wm3Ud7mKw/6rWnoRyhYZvGXkFdp+iEs49Q==

View file

@ -0,0 +1,25 @@
# Navigation
alias cd='cd -L'
# Colors
eval "$(dircolors -b)"
alias ls='ls --color=auto'
# Search
alias grep='grep --color=auto'
# Tree (uses eza if available)
if command -v eza >/dev/null 2>&1; then
alias tree='eza --tree --icons=never'
fi
# Open (graphical environment only)
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
alias open='xdg-open'
fi
if command -v nvim >/dev/null 2>&1; then
alias vim='nvim'
fi
alias cdg='cd "$(git rev-parse --show-toplevel 2>/dev/null)"'

View file

@ -0,0 +1,21 @@
# Use gpg-agent for SSH (YubiKey support)
if command -v gpgconf >/dev/null 2>&1; then
export SSH_AUTH_SOCK
SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
fi
# Source prompt and aliases
BASH_CONFIG_DIR="${BASH_SOURCE%/*}"
. "$BASH_CONFIG_DIR/prompt"
. "$BASH_CONFIG_DIR/aliases"
# Vi mode
set -o vi
# Completion
bind 'set completion-ignore-case on'
bind 'set completion-map-case on'
if command -v direnv >/dev/null 2>&1; then
eval "$(direnv hook bash)"
fi

View file

@ -0,0 +1,179 @@
# Detect graphical environment once at source time
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
_gfx=1
_py_sym=$'\ue73c'
_js_sym=$'\ue781'
_nix_sym=$'\ue843'
_proj_sym=$'\ueb45 '
_branch_sym=$'\uf418'
else
_gfx=""
_py_sym="py"
_js_sym="js"
_nix_sym="nix"
_proj_sym="../"
_branch_sym="git"
fi
# Pre-compute colored icons
_python_icon="\[\033[01;33m\]$_py_sym\[\033[00m\]"
_node_icon="\[\033[01;93m\]$_js_sym\[\033[00m\]"
_nix_icon="\[\033[01;34m\]$_nix_sym\[\033[00m\]"
# SSH check once at source time
if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then
_ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n"
else
_ssh_PS1=""
fi
# Static prompt parts
_green_arrow="\[\033[01;32m\]>> "
_white_text="\[\033[00m\]"
# Cache: dir -> "git_root|superproject|" or "-" for non-git
declare -A _dir_cache
# Find git root by walking up (no git spawn)
_find_git_root() {
local dir="$PWD"
while [ -n "$dir" ]; do
if [ -e "$dir/.git" ]; then
_git_root="$dir"
return 0
fi
dir="${dir%/*}"
done
return 1
}
# Find superproject by walking up from git root
_find_superproject() {
local dir="${_git_root%/*}"
_superproject=""
while [ -n "$dir" ]; do
if [ -e "$dir/.git" ]; then
_superproject="$dir"
return 0
fi
dir="${dir%/*}"
done
return 1
}
# Read branch from .git/HEAD (no git spawn)
_read_branch() {
local git_dir="$_git_root/.git"
local head_file
# Submodule: .git is a file with "gitdir: <path>"
if [ -f "$git_dir" ]; then
read -r _ head_file < "$git_dir"
head_file="$head_file/HEAD"
else
head_file="$git_dir/HEAD"
fi
[ -f "$head_file" ] || return 1
local head
read -r head < "$head_file"
if [[ "$head" == "ref: refs/heads/"* ]]; then
_git_branch="${head#ref: refs/heads/}"
else
# Detached HEAD - short hash
_git_branch="${head:0:7}"
fi
}
# Build venv icons (must run every prompt - env can change)
_build_venv_icons() {
venv_icons=""
[ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon "
[ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon "
[ -d "$_git_root/node_modules" ] && venv_icons+="$_node_icon "
}
# Main prompt logic
_set_prompt() {
local cached="${_dir_cache[$PWD]}"
# Check cache
if [ -z "$cached" ]; then
if _find_git_root; then
_find_superproject
_dir_cache[$PWD]="$_git_root|$_superproject|"
else
_dir_cache[$PWD]="-"
cached="-"
fi
fi
# Non-git directory
if [ "$cached" = "-" ] || [ "${_dir_cache[$PWD]}" = "-" ]; then
venv_icons=""
[ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon "
[ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon "
PS1="$_ssh_PS1\n\[\033[01;34m\]\w\[\033[00m\]\n$venv_icons$_green_arrow$_white_text"
return
fi
# Parse cache
[ -z "$cached" ] && cached="${_dir_cache[$PWD]}"
IFS='|' read -r _git_root _superproject _ <<< "$cached"
# Get branch (can change without cd) - if fails, git root is gone
if ! _read_branch; then
unset "_dir_cache[$PWD]"
_set_prompt
return
fi
# Build paths using bash string ops (no readlink spawn)
local git_curr_dir="${PWD#$_git_root}"
local git_root_dir="${_git_root##*/}"
# Build working_dir
local working_dir
if [ -n "$_superproject" ]; then
local super_name="${_superproject##*/}"
working_dir="\[\033[01;34m\]$_proj_sym$super_name/$git_root_dir$git_curr_dir\[\033[00m\]"
elif [ -z "$git_curr_dir" ]; then
working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir\[\033[00m\]"
else
working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir$git_curr_dir\[\033[00m\]"
fi
# Build branch PS1
local git_branch_PS1
if [ -n "$_gfx" ]; then
git_branch_PS1="\[\033[01;31m\]$_git_branch $_branch_sym:\[\033[00m\]"
else
git_branch_PS1="\[\033[01;31m\]$_git_branch:\[\033[00m\]"
fi
# Build venv icons
_build_venv_icons
PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text"
}
# Invalidate cache for current directory
_prompt_cache_invalidate() {
unset "_dir_cache[$PWD]"
}
# Wrap git to invalidate cache on repo-creating commands
git() {
command git "$@"
local ret=$?
[[ "$1" =~ ^(init|clone)$ ]] && _prompt_cache_invalidate
return $ret
}
if [ -n "$PROMPT_COMMAND" ]; then
PROMPT_COMMAND="_set_prompt;$PROMPT_COMMAND"
else
PROMPT_COMMAND="_set_prompt"
fi

View file

@ -0,0 +1,32 @@
{ lib, config, ... }:
with lib;
let
cfg = config.modules.user.bash;
in
{ options.modules.user.bash = { enable = mkEnableOption "user.bash"; };
config = mkIf cfg.enable {
programs.bash = {
enable = true;
initExtra = "source ~/.config/bash/bashrc";
};
home.file.".config/bash" = {
source = ./bash;
recursive = true;
};
programs = {
ripgrep.enable = true;
eza = {
enable = true;
enableBashIntegration = true;
enableFishIntegration = false;
enableZshIntegration = false;
enableNushellIntegration = false;
enableIonIntegration = false;
};
};
};
}

34
user/modules/default.nix Normal file
View file

@ -0,0 +1,34 @@
let
mkModules = dir: isRoot:
let
entries = builtins.readDir dir;
names = builtins.attrNames entries;
excludedDirs = [ "config" "scripts" ];
isSubmodule = path:
builtins.pathExists "${path}/.git" &&
builtins.readFileType "${path}/.git" == "regular";
isModuleDir = path:
builtins.pathExists path &&
builtins.readFileType path == "directory" &&
!(builtins.elem (builtins.baseNameOf path) excludedDirs) &&
!(isSubmodule path);
isModule = file: file == "default.nix";
in
builtins.concatMap (name:
let
path = "${dir}/${name}";
in
if isModuleDir path then
mkModules path false
else if isModule name && !isRoot then
[ dir ]
else
[]
) names;
in
{
imports = mkModules ./. true;
}

View file

@ -0,0 +1,32 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.modules.user.git;
in
{ options.modules.user.git = { enable = mkEnableOption "user.git"; };
config = mkIf cfg.enable {
programs = {
git = {
enable = true;
};
gh = {
enable = true;
settings.git_protocol = "ssh";
};
};
home = {
packages = with pkgs; [
git-crypt
];
file.".config/git" = {
source = ./git;
recursive = true;
};
};
programs.bash.initExtra = import ./scripts/cdg.nix;
};
}

View file

@ -0,0 +1,9 @@
# Git Config
My global git configuration.
## Install
```bash
git clone git@github.com:itme-brain/git.git ~/.config/git
```

View file

@ -0,0 +1,28 @@
[init]
defaultBranch = master
# Use vimdiff for diffs and merge conflicts
[diff]
tool = vimdiff
[merge]
tool = vimdiff
[mergetool]
keepBackup = false
[mergetool "vimdiff"]
trustExitCode = true
[user]
name = Bryan Ramos
email = bryan@ramos.codes
signingKey = F1F3466458452B2DF351F1E864D12BA95ACE1F2D
# Auto-set upstream on first push
[push]
autoSetupRemote = true
# Enable per-project with: git config --local commit.gpgSign true
[commit]
gpgSign = false

View file

@ -0,0 +1,14 @@
# JavaScript / Node
node_modules
# Nix
.direnv
result
# Haskell
dist-newstyle
# Nuxt
.nuxt/
.output/
dist

View file

@ -0,0 +1,24 @@
''
function cdg() {
if [[ $1 == "--help" ]]; then
echo "A simple utility for navigating to the root of a git repo"
return 0
fi
if [[ -n "$1" ]]; then
echo "Invalid command: $1. Try 'cdg --help'."
return 1
fi
local root_dir
root_dir=$(git rev-parse --show-toplevel 2>/dev/null)
local git_status=$?
if [ $git_status -ne 0 ]; then
echo "Error: Not a git repo."
return 1
fi
cd "$root_dir"
}
''

View file

@ -0,0 +1,83 @@
{ config, ... }:
let
hyprland = config.modules.user.gui.wm.hyprland;
in
{
scrolling = {
history = 10000;
multiplier = 3;
};
window = {
opacity = if hyprland.enable then 0.9 else 1;
};
keyboard.bindings = [
{
key = "Enter";
mods = "Alt | Shift";
action = "SpawnNewInstance";
}
];
colors = {
primary = {
background = "#000000";
foreground = "#cdd6f4";
};
normal = {
black = "#1e2127";
red = "#e06c75";
green = "#98c379";
yellow = "#d19a66";
blue = "#61afef";
magenta = "#c678dd";
cyan = "#56b6c2";
white = "#abb2bf";
};
bright = {
black = "#5c6370";
red = "#e06c75";
green = "#98c379";
yellow = "#d19a66";
blue = "#61afef";
magenta = "#c678dd";
cyan = "#56b6c2";
white = "#ffffff";
};
};
font = {
size = 12;
normal = {
family = "Terminess Nerd Font Propo";
style = "Regular";
};
bold = {
family = "Terminess Nerd Font Propo";
style = "Bold";
};
italic = {
family = "Terminess Nerd Font Propo";
style = "Italic";
};
bold_italic = {
family = "Terminess Nerd Font Propo";
style = "Bold Italic";
};
};
#cursor = {
# shape = "Block";
# blinking = "Always";
# blink_interval = 750;
#};
}

View file

@ -0,0 +1,15 @@
{ lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.alacritty;
in
{ options.modules.user.gui.alacritty = { enable = mkEnableOption "Enable Alacritty terminal"; };
config = mkIf cfg.enable {
programs.alacritty = {
enable = true;
settings = import ./config/alacritty.nix { inherit config; };
};
};
}

View file

@ -0,0 +1,53 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.browser.chromium;
in
{ options.modules.user.gui.browser.chromium = { enable = mkEnableOption "Enable Chromium browser"; };
config = mkIf cfg.enable {
programs = {
chromium = rec {
enable = true;
package = pkgs.ungoogled-chromium;
extensions =
let
vrs = package.version;
in
[
rec {
id = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
crxPath = builtins.fetchurl {
url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc";
name = "ublock_${version}.crx";
sha256 = "0ycnkna72n969crgxfy2lc1qbndjqrj46b9gr5l9b7pgfxi5q0ll";
};
version = "1.62.0";
}
rec {
id = "dbepggeogbaibhgnhhndojpepiihcmeb";
crxPath = builtins.fetchurl {
url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc";
name = "vimium_${version}.crx";
sha256 = "0m8xski05w2r8igj675sxrlkzxlrl59j3a7m0r6c8pwcvka0r88d";
};
version = "2.1.2";
}
rec {
id = "naepdomgkenhinolocfifgehidddafch";
crxPath = builtins.fetchurl {
url = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=${vrs}&acceptformat=crx2,crx3&x=id%3D${id}%26uc";
name = "browserpass_${version}.crx";
sha256 = "074sc9hxh7vh5j79yjhsrnhb5k4dv3bh5vip0jr30hkkni7nygbd";
};
version = "3.9.0";
}
];
};
browserpass = {
enable = true;
};
};
};
}

View file

@ -0,0 +1,338 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.browser.firefox;
passffHost= {
home.packages = with pkgs; [
passff-host
];
home.file = {
".mozilla/native-messaging-hosts/passff.json" = {
text = ''
{
"name": "passff",
"description": "Host for communicating with zx2c4 pass",
"path": "${pkgs.passff-host}/share/passff-host/passff.py",
"type": "stdio",
"allowed_extensions": [ "passff@invicem.pro" ]
}
'';
};
};
assertions =
let
pinentry = config.services.gpg-agent.pinentry.package;
in
[
{
assertion = pinentry != pkgs.pinentry-curses || pinentry != pkgs.pinentry-tty;
message = "Firefox plugin passff requires graphical pinentry";
}
];
};
in
{
options.modules.user.gui.browser.firefox = { enable = mkEnableOption "Enable Firefox browser"; };
config = mkIf cfg.enable (passffHost // {
programs.firefox = {
enable = true;
profiles = {
"${config.user.name}" = {
isDefault = true;
#bookmarks = config.user.bookmarks;
extensions = {
packages = with pkgs.nur.repos.rycee.firefox-addons; [
ublock-origin
tridactyl
#darkreader
tampermonkey
clearurls
passff
multi-account-containers
];
};
search = {
force = true;
default = "google";
engines = {
"Startpage" = {
urls = [{
template = "https://www.startpage.com/sp/search?q={searchTerms}";
}];
icon = "https://www.startpage.com/sp/cdn/favicons/favicon--default.ico";
};
};
};
containersForce = true;
containers = {
Banking = {
color = "green";
icon = "dollar";
id = 1;
};
Personal = {
color = "orange";
icon = "fingerprint";
id = 2;
};
Work = {
color = "yellow";
icon = "briefcase";
id = 3;
};
Personal_Work = {
color = "turquoise";
icon = "briefcase";
id = 4;
};
Social = {
color = "red";
icon = "chill";
id = 5;
};
Shopping = {
color = "purple";
icon = "cart";
id = 6;
};
Google = {
color = "pink";
icon = "vacation";
id = 7;
};
};
settings = {
"layout.spellcheckDefault" = 0;
"ui.key.menuAccessKeyFocuses" = false;
"signon.rememberSignons" = false;
"extensions.pocket.enabled" = false;
"extensions.autoDisableScopes" = 0;
# May break extensions due to Nix
"extensions.enabledScopes" = 5;
# May break stuff but increases privacy
#"extensions.webextensions.restrictedDomains" = "";
#"privacy.resistFingerprinting" = true;
#"privacy.resistFingerprinting.letterboxing" = true;
#"privacy.resistFingerprinting.block_mozAddonManager" = true;
"browser.startup.homepage_override.mstone" = "ignore";
"browser.aboutConfig.showWarning" = false;
"browser.startup.page" = 0;
"browser.formfill.enable" = false;
"places.history.enabled" = false;
"browser.urlbar.suggest.history" = false;
"browser.urlbar.suggest.topsites" = false;
"browser.urlbar.sponsoredTopSites" = false;
"browser.urlbar.autoFill" = false;
"browser.urlbar.suggest.pocket" = false;
"browser.urlbar.suggest.quicksuggest.nonsponsored" = false;
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
"browser.toolbars.bookmarks.showOtherBookmarks" = false;
"browser.aboutwelcome.showModal" = false;
"browser.migrate.content-modal.about-welcome-behavior" = "";
"browser.newtabpage.enabled" = false;
"browser.newtabpage.activity-stream.showSponsored" = false;
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
"browser.newtabpage.activity-stream.default.sites" = "";
"extensions.getAddons.showPane" = false;
"extensions.htmlaboutaddons.recommendations.enabled" = false;
"browser.discovery.enabled" = false;
"browser.shopping.experience2023.enabled" = false;
"datareporting.policy.dataSubmissionEnabled" = false;
"datareporting.healthreport.uploadEnabled" = false;
"toolkit.telemetry.unified" = false;
"toolkit.telemetry.enabled" = false;
"toolkit.telemetry.server" = "";
"toolkit.telemetry.archive.enabled" = false;
"toolkit.telemetry.newProfilePing.enabled" = false;
"toolkit.telemetry.shutdownPingSender.enabled" = false;
"toolkit.telemetry.updatePing.enabled" = false;
"toolkit.telemetry.bhrPing.enabled" = false;
"toolkit.telemetry.firstShutdownPing.enabled" = false;
"toolkit.telemetry.coverage.opt-out" = false;
"toolkit.coverage.endpoint.base" = "";
"browser.newtabpage.activity-stream.feeds.telemetry" = false;
"browser.newtabpage.activity-stream.telemetry" = false;
"app.shield.optoutstudies.enabled" = false;
"app.normandy.enabled" = false;
"app.normandy.api_url" = "";
"breakpad.reportURL" = false;
"browser.tabs.crashReporting.sendReport" = false;
"browser.crashReports.unsubmittedCheck.autoSubmit2" = false;
"captivedetect.canonicalURL" = "";
"network.captive-portal-service.enabled" = false;
"network.connectivity-service.enabled" = false;
"browser.safebrowsing.downloads.remote.enabled" = false;
"network.prefetch-next" = false;
"network.dns.disablePrefetch" = true;
"network.predictor.enabled" = false;
"network.predictor.enable-prefetch" = false;
"network.http.speculative-parallel-limit" = 0;
"network.http.speculativeConnect.enabled" = false;
"network.proxy.sock_remote_dns" = true;
"network.file.disable_unc_paths" = true;
"network.gio.supported-protocols" = "";
"browser.urlbar.speculativeConnect.enabled" = false;
"browser.search.suggest.enabled" = false;
"browser.urlbar.suggest.searches" = false;
"browser.urlbar.clipboard.featureGate" = false;
"browser.urlbar.richSuggestions.featureGate" = false;
"browser.urlbar.trending.featureGate" = false;
"browser.urlbar.addons.featureGate" = false;
"browser.urlbar.pocket.featureGate" = false;
"browser.urlbar.weather.featureGate" = false;
"browser.urlbar.yelp.featureGate" = false;
"browser.urlbar.suggest.engines" = false;
"signon.autofillForms" = false;
"signon.formlessCapture.enabled" = false;
"network.auth.subresource-http-auth-allow" = 1;
"browser.privatebrowsing.forceMediaMemoryCache" = true;
"media.memory_cache_max_size" = 65536;
"browser.sessionstore.privacy_level" = 2;
"browser.shell.shortcutFavicons" = false;
"security.ssl.require_safe_negotiation" = true;
"security.tls.enable_0rtt_data" = false;
"security.OCSP.enabled" = true;
"security.OCSP.require" = true;
"security.cert_pinning.enforcement_level" = 2;
"security.remote_settings.crlite_filters.enabled" = true;
"security.pki.crlite_mode" = 2;
"dom.security.https_only_mode" = true;
"dom.security.https_only_mode_send_http_background_request" = false;
"security.ssl.treat_unsafe_negotiation_as_broken" = true;
"browser.xul.error_pages.expert_bad_cert" = true;
"network.http.referer.XOriginTrimmingPolicy" = 2;
"privacy.userContext.enabled" = true;
"privacy.userContext.ui.enabled" = true;
"media.peerconnection.ice.proxy_only_if_behind_proxy" = true;
"media.peerconnection.ice.default_address_only" = true;
"dom.disable_window_move_resize" = true;
"browser.download.start_downloads_in_tmp_dir" = false;
"browser.helperApps.deleteTempFileOnExit" = true;
"browser.uitour.enabled" = false;
"devtools.debugger.remote-enabled" = false;
"permissions.manager.defaultsUrl" = "";
"webchannel.allowObject.urlWhitelist" = "";
"network.IDN_show_punycode" = true;
"pdfjs.disabled" = false;
"pdfjs.enableScripting" = false;
"browser.tabs.searchclipboardfor.middleclick" = false;
"browser.contentanalysis.default_allow" = false;
"browser.download.useDownloadDir" = true;
"browser.download.alwaysOpenPanel" = true;
"browser.download.manager.addToRecentDocs" = false;
"browser.download.always_ask_before_handling_new_types" = true;
"extensions.postDownloadThirdPartyPrompt" = true;
"browser.contentblocking.category" = "strict";
"privacy.sanitize.sanitizeOnShutdown" = true;
"privacy.clearOnShutdown.cache" = true;
"privacy.clearOnShutdown_v2.cache" = true;
"privacy.clearOnShutdown.downloads" = true;
"privacy.clearOnShutdown.formdata" = true;
"privacy.clearOnShutdown.history" = true;
"privacy.clearOnShutdown_v2.historyFormDataAndDownloads" = true;
"privacy.clearOnShutdown.cookies" = false;
"privacy.clearOnShutdown.offlineApss" = true;
"privacy.clearOnShutdown.sessions" = true;
"privacy.clearOnShutdown_v2.cookiesAndStorage" = false;
"privacy.clearSiteData.cache" = true;
"privacy.clearSiteData.cookiesAndStorage" = false;
"privacy.clearSiteData.historyFormDataAndDownloads" = true;
"privacy.cpd.cache" = true;
"privacy.clearHistory.cache" = true;
"privacy.cpd.formdata" = true;
"privacy.cpd.history" = true;
"privacy.clearHistory.historyFormDataAndDownloads" = true;
"privacy.cpd.cookies" = false;
"privacy.cpd.sessions" = true;
"privacy.cpd.offlineApps" = false;
"privacy.clearHistory.cookiesAndStorage" = false;
"privacy.sanitize.timeSpan" = 0;
"privacy.window.maxInnerWidth" = 1600;
"privacy.window.maxInnerHeight" = 900;
"privacy.spoof_english" = 1;
"browser.display.use_system_colors" = false;
"widget.non-native-theme.enabled" = true;
"browser.link.open_newwindow" = 3;
"browser.link.open_newwindow.restriction" = 0;
"browser.chrome.site_icons" = false;
"browser.download.forbid_open_with" = true;
"extensions.blocklist.enabled" = true;
"network.http.referer.spoofSource" = false;
"security.dialog_enable_delay" = 1000;
"privacy.firstparty.isolate" = false;
"extensions.webcompat.enable_shims" = true;
"security.tls.version.enable-deprecated" = false;
"extensions.webcompat-reporter.enabled" = false;
"extensions.quarantinedDomains.enabled" = true;
"media.videocontrols.picture-in-picture.enabled" = false;
# VA-API hardware video acceleration (NVIDIA)
"media.ffmpeg.vaapi.enabled" = true;
"media.rdd-ffmpeg.enabled" = true;
"media.av1.enabled" = false; # GTX 1650 doesn't support AV1 decode
"gfx.x11-egl.force-enabled" = true;
};
};
};
policies = {
WebsiteFilter = {
Block = [
"*://*.pokemonshowdown.com/*"
];
};
};
};
});
}

View file

@ -0,0 +1,36 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.corn;
in
{ options.modules.user.gui.corn = { enable = mkEnableOption "Enable Bitcoin client applications"; };
config = mkIf cfg.enable {
home.packages = with pkgs; [
#trezor-suite
#trezorctl
#trezord
sparrow
];
#systemd.user.services = {
# trezord = {
# Unit = {
# Description = "Trezor Bridge";
# After = [ "network.target" ];
# Wants = [ "network.target" ];
# PartOf = [ "graphical-session.target" ];
# };
# Service = {
# ExecStart = "${pkgs.trezord}/bin/trezord-go";
# Restart = "always";
# };
# Install = {
# WantedBy = [ "default.target" ];
# };
# };
#};
};
}

View file

@ -0,0 +1,30 @@
{ lib, config, ... }:
let
programs = config.programs;
defaultBrowser =
if programs.firefox.enable then "firefox.desktop"
else if programs.brave.enable then "brave-browser.desktop"
else if programs.chromium.enable then "chromium.desktop"
else null;
types = [
"text/html" "application/xhtml+xml"
"x-scheme-handler/http" "x-scheme-handler/https"
"application/pdf"
"image/png" "image/jpeg" "image/jpg" "image/gif"
"image/webp" "image/avif" "image/bmp" "image/tiff" "image/svg+xml"
"video/mp4" "video/webm" "video/mkv" "video/avi"
"video/x-matroska" "video/quicktime"
];
in
{
xdg.mimeApps = lib.mkIf (defaultBrowser != null) {
enable = true;
defaultApplications = builtins.listToAttrs (
map (t: { name = t; value = [ defaultBrowser ]; }) types
);
};
}

View file

@ -0,0 +1,14 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.dev.design;
in
{ options.modules.user.gui.dev.design = { enable = mkEnableOption "Enable design tools"; };
config = mkIf cfg.enable {
home.packages = with pkgs; [
penpot-desktop
];
};
}

View file

@ -0,0 +1,16 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.modules.user.gui.dev.pcb;
in
{ options.modules.user.gui.dev.pcb = { enable = mkEnableOption "Enable PCB development suite"; };
config = mkIf cfg.enable {
home.packages = with pkgs; [
arduino-ide
kicad-small
ngspice
];
};
}

View file

@ -0,0 +1,82 @@
{
"settings": {
"general": {
"menuBar": {
"hide": false
},
"tray": {
"disable": false
},
"taskbar": {
"flash": true
},
"window": {
"transparent": false,
"hideOnClose": false
}
},
"privacy": {
"blockApi": {
"science": true,
"typingIndicator": false,
"fingerprinting": true
},
"permissions": {
"video": null,
"audio": true,
"fullscreen": true,
"notifications": null,
"display-capture": true,
"background-sync": false
}
},
"advanced": {
"csp": {
"enabled": true
},
"cspThirdParty": {
"spotify": true,
"gif": true,
"hcaptcha": true,
"youtube": true,
"twitter": true,
"twitch": true,
"streamable": true,
"vimeo": true,
"soundcloud": true,
"paypal": true,
"audius": true,
"algolia": true,
"reddit": true,
"googleStorageApi": true
},
"currentInstance": {
"radio": 0
},
"devel": {
"enabled": true
},
"redirection": {
"warn": true
},
"optimize": {
"gpu": true
},
"webApi": {
"webGl": true
},
"unix": {
"autoscroll": false
}
}
},
"update": {
"notification": {
"version": "",
"till": ""
}
},
"screenShareStore": {
"audio": false
}
}

Some files were not shown because too many files have changed in this diff Show more