Compare commits

...
2 Commits
Author SHA1 Message Date
Black_Raven e1672d7cbe feat(wallpaper_changer): Added listing and history 2026-10-16 21:44:08 +03:00
Black_Raven 9e44cb4c99 feat(ThinkPad): Added sops and 1-st key
Also have an instruktion for sops
2026-10-16 21:43:45 +03:00
6 changed files with 262 additions and 6 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
keys:
- &kirill age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
key_groups:
- age:
- *kirill
+1
View File
@@ -11,6 +11,7 @@
./modules/grab.nix ./modules/grab.nix
./modules/thinkpad.nix ./modules/thinkpad.nix
./modules/fingerprint.nix ./modules/fingerprint.nix
./secrets/secrets.nix
(import ../modules/common.nix { (import ../modules/common.nix {
inherit lib; inherit lib;
+14
View File
@@ -0,0 +1,14 @@
{
sops = {
age.keyFile = "/root/age.key";
secrets = {
wallhaven = {
format = "binary";
sopsFile = ./wallhaven.pem;
owner = "kirill";
mode = "0400";
};
};
};
}
+14
View File
@@ -0,0 +1,14 @@
{
"data": "ENC[AES256_GCM,data:0Dw6uMFnVNZKg2ANaHRcMicOPQ7U+mi5V6dia47GDUMM,iv:ScI+ESgBXXcu5hlDkP6m50FL8lp7S3E2cLbf1M9ZAbQ=,tag:VBwdRDyWV3unNUhSVguu2g==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3bWFwMUlwMmxJenZib2Qy\nRk5TMkVtQS9kLzgyWlFNbFVFbzJmUVNXZFRBCkVKQldtcTd5ZElzcVpienhDM3dh\nTmFQZTN0KytZVnZQMmNZNGFQSUlIbXMKLS0tIERBVmhGMmgwZmNwTHB6eEZ0bFJX\nWFN2cVE0WmFna2V5UWE2NXJ6OU9YM2cKWitmQY994raC3Kz/QYXVmiONTomW/JYB\n0PADqEp32xsw/gFnTwi7zbfFPD63ME2PYJ4I9wNKcgR/cFoW2Sv8Jg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg"
}
],
"lastmodified": "2026-10-16T17:19:39Z",
"mac": "ENC[AES256_GCM,data:+CSe7gGwiSANibzDJ4DVTq1OXykiR1vv49szqlxCZyAeKqnxbEjaNFCdslxPAa8SGttDqDxM9aO59DSO3AKxzR9J4kfKqZ7eA8/cNC0DKltzS4AkLkPcGjFpctdUiYENXaPrtk1tV8e/GeW1//f3R61hzAEama8jjkoeAYaDZIM=,iv:aD4c8y2a8whXyG0ig05bryZPfh/NrETeth+O5D4/bPE=,tag:r1EbYa0lsr/u2Rw6ZFjYsQ==,type:str]",
"version": "3.13.3"
}
}
+170
View File
@@ -0,0 +1,170 @@
# Секреты (sops-nix)
Кратко: как всё устроено, как добавить новый секрет и как поднять рабочий sops на новом устройстве.
## Как устроено
- `.sops.yaml` в корне репозитория — список age-реципиентов и правила выбора ключа по пути файла.
- Зашифрованные секреты хоста: `host/<Host>/secrets/<файл>`.
- Метаданные секретов: `host/<Host>/secrets/secrets.nix`, подключается из `host/<Host>/configuration.nix` через `imports`.
- При активации sops-nix расшифровывает секреты в `/run/secrets/<имя>` (runtime, в `/nix/store` ничего не попадает).
- age-ключ выводится из существующего SSH-ключа.
Текущее правило в `.sops.yaml`:
```yaml
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
```
То есть правило выбирается по пути файла: всё, что лежит в `host/<Host>/secrets/`, шифруется на перечисленных реципиентов.
## Добавить новый секрет
1. Придумай имя, дальше для примера — `mysecret`.
2. Положи открытое значение во временный файл **внутри** `host/<Host>/secrets/` (путь важен — см. `path_regex`):
```bash
printf '%s' 'СЕКРЕТНОЕ_ЗНАЧЕНИЕ' > host/ThinkPad/secrets/mysecret.plain
```
3. Зашифруй (приватный ключ не нужен — sops берёт публичного реципиента из `.sops.yaml`):
```bash
nix-shell -p sops --run \
"sops --input-type binary --output-type binary \
--encrypt host/ThinkPad/secrets/mysecret.plain \
--output host/ThinkPad/secrets/mysecret.enc"
rm host/ThinkPad/secrets/mysecret.plain
```
`--input-type`/`--output-type` обязательны для бинарных данных, иначе sops пытается определить тип по расширению.
4. Опиши секрет в `host/<Host>/secrets/secrets.nix`:
```nix
{
sops = {
age.keyFile = "/root/age.key";
secrets = {
mysecret = {
format = "binary"; # yaml | json | binary | dotenv | ini
sopsFile = ./mysecret.enc;
owner = "kirill"; # владелец расшифрованного файла
mode = "0400";
};
};
};
}
```
`format = "binary"` — для произвольных байтов (`.pem`, токены). Для структурированных данных используй `yaml`/`json`.
`owner`/`mode` нужны, если секрет читает пользовательский процесс (а не root-сервис).
5. `git add` — **Nix видит только файлы, добавленные в git** — и ребилд:
```bash
git add host/ThinkPad/secrets/
sudo nixos-rebuild switch --flake .#ThinkPad
```
6. Проверка:
```bash
sudo ls -l /run/secrets/mysecret
```
## Изменить или посмотреть существующий секрет
Используется приватный ключ (`/root/age.key`), поэтому нужен root:
```bash
sudo SOPS_AGE_KEY_FILE=/root/age.key nix-shell -p sops --run \
"sops host/ThinkPad/secrets/wallhaven.pem"
```
`sops` сам не ищет `/root/age.key`, путь передаётся через `SOPS_AGE_KEY_FILE`.
## Новое устройство
1. Убедись, что на устройстве есть приватный SSH-ключ, из которого выводится age-ключ. Ключ должен быть **без пароля** — иначе расшифровка при активации будет требовать ввода.
2. Создай age-ключ:
```bash
sudo nix-shell -p ssh-to-age --run \
"ssh-to-age -private-key -i /home/kirill/.ssh/<ваш_ключ> -o /root/age.key"
sudo chmod 600 /root/age.key
```
3. Проверь, что публичный age-реципиент устройства совпадает с тем, что в `.sops.yaml`:
```bash
nix-shell -p ssh-to-age --run \
"ssh-to-age -i /home/kirill/.ssh/<ваш_ключ>.pub"
```
Должно вывести:
```
age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
```
Если вывод другой — добавь новый реципиент (см. следующий раздел).
4. Создай структуру секретов хоста:
```
host/<Host>/secrets/secrets.nix
host/<Host>/secrets/<файлы секретов>
```
Минимальный `secrets.nix`:
```nix
{
sops.age.keyFile = "/root/age.key";
secrets = {
# ...
};
}
```
И добавь `./secrets/secrets.nix` в `imports` в `host/<Host>/configuration.nix`.
5. `git add` + ребилд:
```bash
git add host/<Host>/secrets/
sudo nixos-rebuild switch --flake .#<Host>
```
## Добавить устройство к существующим секретам
1. Получи age-реципиент устройства (шаг 3 выше).
2. Добавь его в `.sops.yaml`:
```yaml
keys:
- &kirill age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
- &newdevice age1...новый_реципиент... # замени на реальный
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
key_groups:
- age:
- *kirill
- *newdevice
```
3. Перешифруй существующие секреты под новый список реципиентов (нужен приватный ключ, значит root):
```bash
sudo SOPS_AGE_KEY_FILE=/root/age.key nix-shell -p sops --run \
"sops updatekeys host/ThinkPad/secrets/wallhaven.pem"
```
4. `git add` + ребилд на этом устройстве.
+56 -6
View File
@@ -1,12 +1,17 @@
{ pkgs, lib, config, inputs, host, ... }: { pkgs, lib, config, inputs, host, osConfig, ... }:
let let
secretPath =
if osConfig ? sops.secrets.wallhaven
then osConfig.sops.secrets.wallhaven.path
else "";
wallpaper_changer = pkgs.writers.writePython3Bin "wallpaper_changer" { wallpaper_changer = pkgs.writers.writePython3Bin "wallpaper_changer" {
libraries = [ pkgs.python3Packages.requests ]; libraries = [ pkgs.python3Packages.requests ];
flakeIgnore = [ "E111" "E121" "E241" "E501" "E701" "E731" ]; flakeIgnore = [ "E111" "E121" "E241" "E501" "E701" "E731" ];
} /*py*/ '' } /*py*/ ''
import requests as requests import requests as requests
from random import choice from random import choice
from os import listdir, makedirs, remove, replace, system from os import environ, getuid, listdir, makedirs, remove, replace, system
from os.path import exists, getmtime, isfile from os.path import exists, getmtime, isfile
from subprocess import run from subprocess import run
@@ -88,18 +93,61 @@ let
remove(f"{folder}/{name}") remove(f"{folder}/{name}")
def read_history(path):
try:
with open(path) as file:
return [line.strip() for line in file if line.strip()]
except OSError:
return []
def write_history(path, ids):
try:
with open(path, "w") as file:
file.write("\n".join(dict.fromkeys(ids)) + "\n")
except OSError:
pass
folder = "${config.home.homeDirectory}/wallpapers" folder = "${config.home.homeDirectory}/wallpapers"
url = "https://wallhaven.cc/api/v1/collections/BlackRaven21/2284568" url = "https://wallhaven.cc/api/v1/collections/BlackRaven21/2284568"
runtime = environ.get("XDG_RUNTIME_DIR") or f"/run/user/{getuid()}"
history = f"{runtime}/wallpaper_changer_history"
token = None
try:
with open("${secretPath}") as file:
token = file.read().strip() or None
except OSError:
pass
filename = None filename = None
notify("Updating wallpaper...", 0) notify("Updating wallpaper...", 0)
try: try:
response = requests.get(url, timeout=10) params = {}
response.raise_for_status() if token:
json = response.json() params["apikey"] = token
wallpaper = choice(json['data']) response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
page_json = response.json()
walls = list(page_json['data'])
for page in range(2, page_json['meta']['last_page'] + 1):
params['page'] = page
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
walls.extend(response.json()['data'])
seen = read_history(history)
candidates = [wall for wall in walls if wall['id'] not in seen]
if not candidates:
seen = []
candidates = walls
wallpaper = choice(candidates)
link = wallpaper['path'] link = wallpaper['path']
format = wallpaper['file_type'] format = wallpaper['file_type']
id = wallpaper['id'] id = wallpaper['id']
@@ -119,6 +167,8 @@ let
else: else:
notify("Using cached wallpaper", 100) notify("Using cached wallpaper", 100)
write_history(history, seen + [id])
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
notify("Offline mode") notify("Offline mode")
try: try: