Секреты вынесены в config/init.custom.conf + config/secrets.env

- config/init.conf — шаблон (секреты всегда пустые), в git
- config/init.custom.conf — боевой конфиг, монтируется в /lampac/init.conf,
  собирается scripts/config-secrets.py apply из шаблона + secrets.env
- docker-compose.yaml: монтирует ./config/init.custom.conf:/lampac/init.conf
- scripts/config-secrets.py: apply/export/check/status
- scripts/install-git-hooks.sh: post-merge/post-checkout -> apply,
  pre-commit -> check --staged
- scripts/restore-lampa-main.sh: восстановление веб-клиента из архива
- .gitignore: init.custom.conf, secrets.env, lampa-main/, restore.sh zip
- lampa-main смонтирован как bind-mount ./lampa-main:/lampac/wwwroot/lampa-main,
  переживает docker compose up -d (раньше исчезал из writable-слоя)
- README: раздел «Секреты», обновлена «Структура проекта» и «Обновление»,
  «Безопасность» переписана под новую схему
- AGENT: правило 10 дополнено init.custom.conf/secrets.env/хуками,
  §11 Tg-notify ссылается на apply, §1 упоминает lampa-main bind-mount
This commit is contained in:
2026-10-04 15:42:16 +00:00
parent 24d2dac051
commit 25c591b45a
7 changed files with 297 additions and 11 deletions
+11
View File
@@ -1,5 +1,7 @@
# Секреты
config/passwd
config/secrets.env # токены ботов и mutations_api_secret
config/init.custom.conf # боевой конфиг со secrets (собирается config-secrets.py)
# Локальные данные и кеш (только корневые каталоги; якорь «/» обязателен —
# иначе «cache/» игнорирует и mods/Music/Services/Cache/ и т.п.)
@@ -14,3 +16,12 @@ config/passwd
# Серверный override (Proxmox: том DLNA -> /mnt/torrents), живёт только на сервере
docker-compose.override.yml
# Секреты (не коммитить — см. README, раздел «Секреты»)
config/passwd
config/secrets.env
config/init.custom.conf
# Веб-клиент Lampa (качается LampaCron с github.com, нет в образе,
# пропадает при пересоздании контейнера; см. scripts/restore-lampa-main.sh)
wwwroot/lampa-main/
+4 -2
View File
@@ -30,6 +30,8 @@ docker cp lampac:/lampac/module/<Имя> "mods/<Имя>"
Сейчас в `mods/`: `AdminPanel`, `Tracks`, `Tg-notify.bot`, `TelegramAuth`, `TelegramAuthBot`. **`mods/Music` удалён** (remote-коммит `remove Music`): в образе у Music `manifest.enable: false`, поэтому модуль не грузится, `/music.js` → 404, секция `"Music"` и плагин убраны из `init.conf`. `mods/WatchTogether` тоже удалён (вернуть — той же механикой, он в списке «ещё можно включить» в README).
**Веб-клиент `lampa-main` — bind-mount из `./lampa-main/`** (в .gitignore). В образе его нет, `LampaCron` качает с GitHub; при пересоздании контейнера без mount клиент исчезает. См. `scripts/restore-lampa-main.sh`.
### 2. Механика загрузки модуля (4 независимых фильтра)
Модуль грузится, только если выполнены **все**:
@@ -116,12 +118,12 @@ docker exec lampac sh -c '...' # осмотр состояния в конте
`config/passwd`, cookie `accspasswd`, заголовок `lcrqpasswd`, `bot_token` в `TelegramBot`, `bot_token` в `TelegramAuthBot`, `mutations_api_secret`. Маршруты `/adminpanel`, `/weblog`, `Tracks`, `Catalog`, `DLNA`, proxy-модули — только для доверенной сети без WAF не открывать.
**Секреты в git не попадают:** в закоммиченном `config/init.conf` токены и секрет должны быть **пустыми** (так делает и remote — коммит `remove`), реальные значения живут только в рабочем файле на сервере (копия до мержа — `/root/init.conf.local`). Перед `git add config/init.conf`: `git diff config/init.conf | grep -E ":[0-9]{8,}:AA|\"mutations_api_secret\": \"[a-f0-9]{16}"` → должно быть пусто.
**Секреты:** в git `config/init.conf` — секреты **пустые**. Реальные значения — в `config/secrets.env` (gitignored), боевой конфиг — `config/init.custom.conf` (gitignored). Собирается скриптом: `python3 scripts/config-secrets.py apply`. Перед коммитом: `python3 scripts/config-secrets.py check` (должен выдать `секретов в config/init.conf нет — ok`). После `git pull`/смены ветки `apply` запускается автоматически через git-хуки (`scripts/install-git-hooks.sh`).
### 11. Tg-notify.bot (Telegram-бот)
- Механика: long polling по `TelegramBot.bot_token`; если токен пуст — polling не стартует (в логе `[TelegramBot] Установите bot_token…`, сервер работает). После правки секции: `docker compose restart lampac`.
- **Токен только локально:** в git поле пустое (правило 10). На сервере в `init.conf` вставлен `@KlinkerZabbixBot` — после `git pull`/переписывания конфига проверять, что polling снова стартовал (`[TelegramBot] @KlinkerZabbixBot запущен!`). Второй экземпляр на том же токене даёт `Conflict: terminated by other getUpdates request`.
- **Токен только локально:** в git `config/init.conf` поле пустое (правило 10). Реальный токен живёт в `config/secrets.env`, а `docker-compose.yaml` монтирует `config/init.custom.conf` (собрать: `python3 scripts/config-secrets.py apply`). После `git pull` хук `post-merge` делает `apply` сам; если нет хуков — выполнить вручную. Второй экземпляр на том же токене даёт `Conflict: terminated by other getUpdates request`.
- Три подсистемы: трекер серий (Trakt→TMDB), трекер озвучек (параллельно Mirage/Collaps/VideoHub через `lampac_host`: `/lite/mirage`, `/lite/collaps`, `/lite/cdnvideohub`), клиентский плагин `tg-notify.js` (вкладка «TG Подписки», перехват 🔔). Полный разбор — в README.md.
- Данные подписок: `database/tgnotify/{users.json,subscriptions.json}` (том `./database/`, в .gitignore). Не «чинить» вручную — это живые данные бота.
- Диагностика: `GET /tg-notify.js` (200 без авторизации), `GET /api/tg/voices` (200), `/api/tg/*` остальной — требует `user_uid`. Озвучки могут не работать, если балансеры недоступны (проверить `GET /lite/mirage` — тело `disable`/ошибка, `/lite/collaps` — 503); тогда уведомления приходят только о выходе серий.
+49 -8
View File
@@ -64,22 +64,61 @@ ls -ln /var/lib/docker/volumes/lampac_database/_data/Sisi.sql # uid/gid 1000
Проверить uid процесса в контейнере: `docker exec lampac id` → `uid=1000(lampac)`.
Правило: **любой bind-mount файл/каталог, в который пишет контейнер, должен принадлежать `1000:1000`** (см. `.gitignore` для `config/passwd`, `dlna/`).
Правило: **любой bind-mount файл/каталог, в который пишет контейнер, должен принадлежать `1000:1000`** (см. `.gitignore` для `config/passwd`, `dlna/`, `lampa-main/`).
## Секреты (вынесены из git)
Модули `TelegramBot`, `TelegramAuthBot`, `TelegramAuth` читают `bot_token` и `mutations_api_secret` из конфига. В репозитории эти поля **всегда пустые** — реальные значения живут в файле, который не коммитится.
| Файл | В git? | Назначение |
|---|---|---|
| `config/init.conf` | ✅ | Шаблон конфига, секреты **пустые**. Именно его редактируете для обычных настроек (`listen`, `SkipModules`, `initPlugins`, модули). |
| `config/secrets.env` | ❌ .gitignore | Токены ботов и `mutations_api_secret`. `TELEGRAM_NOTIFY_BOT_TOKEN`, `TELEGRAM_AUTH_BOT_TOKEN`, `TELEGRAM_AUTH_MUTATIONS_SECRET`. |
| `config/init.custom.conf` | ❌ .gitignore | Боевой конфиг, который монтируется в контейнер. Собирается из шаблона + `secrets.env` скриптом. |
| `scripts/config-secrets.py` | ✅ | Собирает/разбирает боевой конфиг: `apply`, `export`, `check`, `status`. |
| `scripts/install-git-hooks.sh` | ✅ | Устанавливает хуки: `post-merge`/`post-checkout` → `apply`, `pre-commit` → `check`. |
| `docker-compose.yaml` | ✅ | Монтирует `./config/init.custom.conf:/lampac/init.conf` (не `init.conf`). |
**Рабочий процесс:**
```bash
# первый раз / после git pull:
python3 scripts/config-secrets.py apply # init.custom.conf = шаблон + secrets.env
docker compose up -d # рекреация из-за нового mount
# правки в init.conf:
# 1) правите config/init.conf (шаблон)
# 2) python3 scripts/config-secrets.py apply -> обновляет init.custom.conf
# 3) docker compose restart lampac
# перед коммитом:
python3 scripts/config-secrets.py export # секреты -> пустые в шаблоне
git add config/init.conf
git commit -m "..."
```
**Git-хуки (рекомендуется):** `bash scripts/install-git-hooks.sh` — после `git pull`/смены ветки `apply` запустится сам, а `pre-commit` не даст случайно закоммитить секреты.
> `docker-compose.override.yml` (в .gitignore) — серверный override для Proxmox (том DLNA -> `/mnt/torrents`), живёт только на сервере.
## Структура проекта
| Путь | Назначение |
|---|---|
| `docker-compose.yaml` | Основной сервис (образ закреплён на `ghcr.io/lampac-nextgen/lampac:1.68.0`) |
| `config/init.conf` | Конфигурация сервера (JSON, монтируется в `/lampac/init.conf`) |
| `config/passwd` | Пароль root для `/adminpanel` и `/weblog` (не в git) |
| volume `lampac_cache` | Кеш (`/lampac/cache`) — Docker named volume, не файловый том |
| `docker-compose.yaml` | Основной сервис (образ `ghcr.io/lampac-nextgen/lampac:1.68.1`) |
| `docker-compose.override.yml` | Серверный override (Proxmox: том DLNA -> `/mnt/torrents`), в .gitignore |
| `config/init.conf` | Шаблон конфига (JSON, в git, секреты **пустые**) |
| `config/init.custom.conf` | Боевой конфиг (в .gitignore, монтируется в `/lampac/init.conf`) |
| `config/secrets.env` | Токены ботов и `mutations_api_secret` (в .gitignore) |
| `config/passwd` | Пароль root для `/adminpanel` и `/weblog` (в .gitignore) |
| `lampa-main/` | Веб-клиент Lampa (bind-mount в `/lampac/wwwroot/lampa-main`, в .gitignore, качается LampaCron) |
| volume `lampac_cache` | Кеш (`/lampac/cache`) — Docker named volume |
| volume `lampac_database` | SQLite-базы Sync, TimeCode, tg-notify, tracks, storage (`/lampac/database`) — Docker named volume |
| `dlna/` | Локальные медиа для DLNA (`/lampac/data/dlna`) |
| `plugins/extensions.json` | Каталог плагинов для веб-клиента (раздел «Плагины»), отдаётся через `/extensions` |
| `mods/<Имя>/` | Пользовательские копии встроенных модулей с `enable: true` |
> `docker-compose.override.yml` (в .gitignore) — серверное override для Proxmox (том DLNA -> `/mnt/torrents`), живёт только на сервере.
> Веб-клиент `lampa-main` **нет в образе**: `LampaCron` качает его с GitHub при старте, и он живёт только в writable-слое контейнера. При пересоздании контейнера (`docker compose up -d`) клиент исчезает, если не смонтирован как bind-mount. См. раздел «Секреты» и `scripts/restore-lampa-main.sh`.
## Включённые модули из `mods/`
@@ -287,7 +326,9 @@ docker compose pull
docker compose up -d
```
Перед обновлением проверить, что в `docker-compose.yaml` раскомментированы нужные тома. Данные сохраняются в примонтированных томах: `config/`, `cache/`, `database/`, `dlna/`, `mods/`.
Перед обновлением проверить, что в `docker-compose.yaml` раскомментированы нужные тома. Данные сохраняются в примонтированных томах: `config/`, `cache/`, `database/`, `dlna/`, `mods/`, `lampa-main/`.
`wwwroot/lampa-main` смонтирован как bind-mount из `./lampa-main/` (см. раздел «Структура проекта»), поэтому переживает рекреацию контейнера. Если директория на хосте пуста — `bash scripts/restore-lampa-main.sh scripts/lampa-main.zip` (архив берётся с https://github.com/yumata/lampa/archive/refs/heads/main.zip).
**Образ закреплён точным digest** (`1.68.0@sha256:752dafd8…`): тег `1.68.0` на ghcr — mutable, его переиздавали, и в новой сборке **копии модулей в `mods/`** (Roslyn компилирует их на каждом старте) перестают собираться — `CS0103` по символам из старого билда и crash-loop сервера. Чтобы обновить образ: взять новый digest, пересобрать копии модулей в `mods/` из нового образа (`docker cp lampac:/lampac/module/<Имя> mods/<Имя>` + вернуть `"enable": true`) и заменить digest в `docker-compose.yaml`. Не делать «просто» `docker compose pull` без фиксации digest.
@@ -297,6 +338,6 @@ docker compose up -d
- Не публикуй в интернет без аутентификации, WAF и ограничения сети: `/adminpanel`, `/weblog`, `/database-editor`, `Tracks`, `Transcoding`, `Catalog`, `DLNA`, proxy-модули.
- Нигде не раскрывай `config/passwd`, cookie `accspasswd`, заголовок `lcrqpasswd`.
- **Секреты в `config/init.conf` не коммитятся**: `TelegramBot.bot_token`, `TelegramAuthBot.bot_token`, `mutations_api_secret`. В git они должны быть пустыми — реальные значения живут только в файле на сервере (`/root/init.conf.local` — резервная копия перед мержем). Перед `git add config/init.conf` проверять `git diff` на токены (`git diff config/init.conf | grep -E ":[0-9]{8,}:AA"` → пусто).
- **Секреты в git не попадают**: `TelegramBot.bot_token`, `TelegramAuthBot.bot_token`, `mutations_api_secret` хранятся в `config/secrets.env` (в .gitignore). В git `config/init.conf` эти поля всегда пустые. Реальный боевой конфиг — `config/init.custom.conf` (тоже gitignored), собирается скриптом `scripts/config-secrets.py apply` из шаблона + секретов. Перед `git add config/init.conf`: `python3 scripts/config-secrets.py check` → должно быть `check: секретов в config/init.conf нет — ок`.
- Для публикации за reverse proxy: [deployment/reverse-proxy](https://docs.lampac.dev/deployment/reverse-proxy) (HTTPS в ответах, реальный IP клиента, WebSocket, SSE, streaming).
- `shm_size: 4096mb` — рекомендованный минимум для Playwright/Chromium.
+2 -1
View File
@@ -11,13 +11,14 @@ services:
shm_size: 4096mb
volumes:
- ./config/passwd:/lampac/passwd
- ./config/init.conf:/lampac/init.conf
- ./config/init.custom.conf:/lampac/init.conf
- ./plugins/extensions.json:/lampac/module/LampaWeb/plugins/extensions.json
- ./plugins:/lampac/wwwroot/plugins
- ./mods:/lampac/mods
- cache:/lampac/cache
- database:/lampac/database
- ./dlna:/lampac/data/dlna
- ./lampa-main:/lampac/wwwroot/lampa-main
volumes:
cache:
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Вынос секретов Lampac из git: шаблон config/init.conf <-> боевой config/init.custom.conf.
В git (config/init.conf) секреты всегда ПУСТЫЕ — это делает рабочее дерево чистым,
поэтому git pull/merge никогда не конфликтует и секреты не уезжают в remote.
Для сервера собирается config/init.custom.conf (шаблон + config/secrets.env),
он монтируется в контейнер вместо init.conf (см. docker-compose.yaml) и
в .gitignore.
Команды:
apply config/init.conf + config/secrets.env -> config/init.custom.conf (после git pull)
export config/init.custom.conf -> config/init.conf, секреты обнуляются (перед git add)
check проверяет, что в config/init.conf (или в staged-версии) нет секретов
status показывает, что где лежит
Обычно руками ничего не нужно: scripts/install-git-hooks.sh вешает
post-merge/post-checkout на apply и pre-commit на check.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATE = os.path.join(ROOT, "config", "init.conf")
LIVE = os.path.join(ROOT, "config", "init.custom.conf")
SECRETS = os.path.join(ROOT, "config", "secrets.env")
# (секция, поле) в init.conf -> ключ в config/secrets.env
MAP = [
(("TelegramBot", "bot_token"), "TELEGRAM_NOTIFY_BOT_TOKEN"),
(("TelegramAuthBot", "bot_token"), "TELEGRAM_AUTH_BOT_TOKEN"),
(("TelegramAuth", "mutations_api_secret"), "TELEGRAM_AUTH_MUTATIONS_SECRET"),
(("TelegramAuthBot", "mutations_api_secret"), "TELEGRAM_AUTH_MUTATIONS_SECRET"),
]
def load_secrets() -> dict[str, str] | None:
"""None, если secrets.env отсутствует (тогда apply просто предупреждает)."""
if not os.path.exists(SECRETS):
return None
out: dict[str, str] = {}
for line in open(SECRETS, encoding="utf-8"):
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
out[k.strip()] = v.strip()
return out
def load_json(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def save_json(path: str, data: dict) -> None:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
os.replace(tmp, path)
def cmd_apply() -> None:
"""Шаблон + секреты -> боевой файл, который монтируется в контейнер."""
tpl = load_json(TEMPLATE)
sec = load_secrets()
if sec is None:
print(f"apply: нет {os.path.relpath(SECRETS, ROOT)} — "
f"{os.path.relpath(LIVE, ROOT)} не создан (секреты останутся пустыми)")
return
missing = []
for (section, field), key in MAP:
value = sec.get(key, "")
if not value:
missing.append(key)
if section in tpl:
tpl[section][field] = value
save_json(LIVE, tpl)
# контейнер пишет в боевой конфиг через AdminPanel (uid 1000)
try:
os.chown(LIVE, 1000, 1000)
except PermissionError:
pass
os.chmod(LIVE, 0o644)
print(f"apply: {os.path.relpath(LIVE, ROOT)} собран из шаблона + secrets.env")
if missing:
print(f" внимание: нет значения для {', '.join(sorted(set(missing)))} — поле останется пустым")
def cmd_export() -> None:
"""Боевой файл -> шаблон, секреты обнуляются (для git add)."""
if not os.path.exists(LIVE):
print(f"export: {os.path.relpath(LIVE, ROOT)} нет — нечего экспортировать")
return
live = load_json(LIVE)
changed = 0
for (section, field), _ in MAP:
if section in live and live.get(section, {}).get(field):
live[section][field] = ""
changed += 1
save_json(TEMPLATE, live)
print(f"export: шаблон обновлён из боевого конфига, секретов обнулено: {changed}")
def read_template_for_check(staged: bool) -> str:
if staged:
try:
return subprocess.check_output(
["git", "show", ":config/init.conf"], cwd=ROOT, text=True
)
except subprocess.CalledProcessError:
sys.exit("check: config/init.conf не в индексе")
return open(TEMPLATE, encoding="utf-8").read()
def cmd_check(staged: bool = False) -> None:
"""Падает (exit 1), если в шаблоне есть непустой секрет."""
data = json.loads(read_template_for_check(staged))
found = []
for (section, field), key in MAP:
value = (data.get(section) or {}).get(field)
if value:
found.append(f"{section}.{field} ({key}) = {value[:6]}…")
if found:
sys.exit(
"check: ОТКАЗ — в config/init.conf есть секреты, коммитить нельзя:\n "
+ "\n ".join(found)
+ "\nВыполните: python3 scripts/config-secrets.py export"
)
print("check: секретов в config/init.conf нет — ок")
def cmd_status() -> None:
tpl = load_json(TEMPLATE)
live = load_json(LIVE) if os.path.exists(LIVE) else None
print(f"{'путь':28} {'есть?' :6} секреты")
for (section, field), key in MAP:
t = (tpl.get(section) or {}).get(field)
l = (live.get(section) or {}).get(field) if live else None
print(
f"{section}.{field:24} шаблон={'заполнен' if t else 'пусто':8} "
f"боевой={'заполнен' if l else ('нет файла' if live is None else 'пусто')}"
)
print(f"secrets.env: {'есть' if os.path.exists(SECRETS) else 'НЕТ'}")
def main() -> None:
args = sys.argv[1:]
cmd = args[0] if args else "status"
if cmd == "apply":
cmd_apply()
elif cmd == "export":
cmd_export()
elif cmd == "check":
cmd_check(staged="--staged" in args)
elif cmd == "status":
cmd_status()
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Вешает git-хуки, которые держат секреты вне git.
# post-merge / post-checkout -> apply (собирает config/init.custom.conf)
# pre-commit -> check (отменяет коммит с секретами)
set -e
cd "$(git rev-parse --show-toplevel)"
mkdir -p .git/hooks
cat > .git/hooks/post-merge <<'H'
#!/bin/sh
exec python3 scripts/config-secrets.py apply
H
cat > .git/hooks/post-checkout <<'H'
#!/bin/sh
exec python3 scripts/config-secrets.py apply
H
cat > .git/hooks/pre-commit <<'H'
#!/bin/sh
exec python3 scripts/config-secrets.py check --staged
H
chmod +x .git/hooks/post-merge .git/hooks/post-checkout .git/hooks/pre-commit
echo "хуки установлены:"
ls -l .git/hooks/post-merge .git/hooks/post-checkout .git/hooks/pre-commit
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Восстанавливает веб-клиент Lampa (wwwroot/lampa-main), который LampaCron
# берёт с github.com и который пропадает при пересоздании контейнера
# (нет в образе, живёт только в writable-слое).
#
# bash scripts/restore-lampa-main.sh scripts/lampa-main.zip
set -e
cd "$(git rev-parse --show-toplevel)"
ZIP="${1:-scripts/lampa-main.zip}"
if [ ! -f "$ZIP" ]; then
echo "нет $ZIP — берите архив с https://github.com/yumata/lampa/archive/refs/heads/main.zip"
exit 1
fi
docker exec lampac sh -c "rm -rf /lampac/wwwroot/lampa-main /lampac/wwwroot/lampa.zip"
docker cp "$ZIP" lampac:/lampac/wwwroot/lampa.zip
docker exec lampac sh -c '
cd /lampac/wwwroot
unzip -q -o lampa-main.zip
mv lampa-main-main lampa-main 2>/dev/null || mv lampa-main lampa-main 2>/dev/null || true
chown -R 1000:1000 lampa-main
rm -f lampa.zip
# инъекция lampainit.js (делает LampaCron при следующем старте)
python3 - <<PY
import os
p="/lampac/wwwroot/lampa-main/index.html"
html=open(p,encoding="utf-8").read()
if "src=\"/lampainit.js\"" not in html:
html=html.replace("</body>", "<script src=\"/lampainit.js\"></script></body>")
open(p,"w",encoding="utf-8").write(html)
print("index.html: lampainit.js внедрён")
else:
print("index.html: уже был")
PY
'
echo "lampa-main восстановлен ($(docker exec lampac sh -c 'find /lampac/wwwroot/lampa-main -type f | wc -l') файлов)"