Update documentation to centralize Vaultwarden integration details and enhance backup scripts

Refactor README, architecture, and backup documentation to emphasize the use of Vaultwarden for credential management across various services. Update scripts for Nextcloud, Gitea, Paperless, and others to reference Vaultwarden for sensitive information. Remove outdated references to previous backup strategies and ensure clarity on credential retrieval processes. This improves security practices and streamlines backup operations.
This commit is contained in:
2026-02-28 00:52:56 +03:00
parent f319133cee
commit 16c254510a
34 changed files with 1677 additions and 437 deletions

View File

@@ -27,7 +27,7 @@ OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
ERR=$(mktemp)
trap "rm -f '$ERR'" EXIT
# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md
# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/vaultwarden-secrets.md
PG_ENV_ARGS=""
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then

View File

@@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz"
ERR=$(mktemp)
trap "rm -f '$ERR'" EXIT
# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md
# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/vaultwarden-secrets.md
PG_ENV_ARGS=""
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then

View File

@@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz"
ERR=$(mktemp)
trap "rm -f '$ERR'" EXIT
# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md
# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/vaultwarden-secrets.md
PG_ENV_ARGS=""
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then

View File

@@ -18,7 +18,7 @@ fi
# Секреты из Vaultwarden (объект RESTIC)
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md"
exit 1
fi
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then

View File

@@ -22,7 +22,7 @@ fi
# Секреты из Vaultwarden (объект RESTIC)
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md"
exit 1
fi
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Pre-hook для certbot: проверка beget.ini перед renew
# Путь: /etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh
# При отсутствии файла или неверных правах — exit 1, certbot не выполнит renew.
BEGET_INI="/root/.secrets/certbot/beget.ini"
if [ ! -f "$BEGET_INI" ]; then
echo "check-beget-credentials: $BEGET_INI not found" >&2
exit 1
fi
mode=$(stat -c '%a' "$BEGET_INI" 2>/dev/null)
owner=$(stat -c '%u' "$BEGET_INI" 2>/dev/null)
if [ "$mode" != "600" ]; then
echo "check-beget-credentials: $BEGET_INI has mode $mode, expected 600" >&2
exit 1
fi
if [ "$owner" != "0" ]; then
echo "check-beget-credentials: $BEGET_INI owner $owner, expected root (0)" >&2
exit 1
fi
exit 0

View File

@@ -0,0 +1,116 @@
#!/bin/bash
# deploy-beget-credentials.sh — деплой кредов Beget для certbot DNS-01 в CT 100
# Секреты из Vaultwarden (объект beget). Атомарная запись beget.ini.
#
# Использование:
# /root/scripts/deploy-beget-credentials.sh # деплой
# /root/scripts/deploy-beget-credentials.sh --dry-run # проверка без записи
#
# Ротация: сменил пароль в Vaultwarden → запустил скрипт → готово.
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=100
BEGET_INI_PATH="/root/.secrets/certbot/beget.ini"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
BEGET_USER=$(bw get username "beget" 2>/dev/null)
BEGET_PASS=$(bw get password "beget" 2>/dev/null)
if [ -z "$BEGET_USER" ] || [ -z "$BEGET_PASS" ]; then
err "beget: missing username or password in Vaultwarden"
exit 1
fi
}
gen_ini() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
dns_beget_api_username = ${BEGET_USER}
dns_beget_api_password = ${BEGET_PASS}
EOF
echo "$tmp"
}
push_ini_atomic() {
local tmp="$1"
local dir
dir=$(dirname "$BEGET_INI_PATH")
pct exec "$CT_ID" -- mkdir -p "$dir"
pct push "$CT_ID" "$tmp" "${BEGET_INI_PATH}.tmp"
pct exec "$CT_ID" -- bash -c "mv ${BEGET_INI_PATH}.tmp ${BEGET_INI_PATH} && chmod 600 ${BEGET_INI_PATH} && chown root:root ${BEGET_INI_PATH}"
log "beget.ini written (atomic), chmod 600, owner root"
}
deploy_pre_hook() {
local hook_path="/etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh"
local hook_src
hook_src="$(cd "$(dirname "$0")" && pwd)/certbot-hooks/check-beget-credentials.sh"
if [ ! -f "$hook_src" ]; then
log "pre-hook source not found ($hook_src), skip"
return 0
fi
if pct exec "$CT_ID" -- test -f "$hook_path" 2>/dev/null; then
pct push "$CT_ID" "$hook_src" "$hook_path"
pct exec "$CT_ID" -- chmod +x "$hook_path"
log "pre-hook updated"
else
pct push "$CT_ID" "$hook_src" "$hook_path"
pct exec "$CT_ID" -- chmod +x "$hook_path"
log "pre-hook deployed"
fi
}
main() {
log "deploy-beget-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push beget.ini and deploy pre-hook"
log " dns_beget_api_username=$BEGET_USER"
log " dns_beget_api_password=***"
exit 0
fi
tmp=$(gen_ini)
trap "rm -f $tmp" EXIT
push_ini_atomic "$tmp"
deploy_pre_hook
log "done"
}
main

View File

@@ -0,0 +1,94 @@
#!/bin/bash
# deploy-galene-credentials.sh — деплой TURN-кредов Galene в CT 108
# Секреты из Vaultwarden (объект GALENE, поле config — JSON ice-servers).
#
# Использование:
# /root/scripts/deploy-galene-credentials.sh
# /root/scripts/deploy-galene-credentials.sh --dry-run
#
# Ротация: сменил TURN username/credential в Vaultwarden → запустил скрипт → systemctl restart galene
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=108
ICE_SERVERS_PATH="/opt/galene-data/data/ice-servers.json"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local config
config=$(bw get item "GALENE" 2>/dev/null | jq -r '.fields[] | select(.name=="config") | .value // empty')
if [ -z "$config" ]; then
err "GALENE: missing config field (JSON ice-servers)"
exit 1
fi
if ! echo "$config" | jq . >/dev/null 2>&1; then
err "GALENE config: invalid JSON"
exit 1
fi
ICE_CONFIG="$config"
}
push_ice_servers() {
local tmp
tmp=$(mktemp)
echo "$ICE_CONFIG" | jq -c . > "$tmp"
pct push "$CT_ID" "$tmp" "${ICE_SERVERS_PATH}.tmp"
rm -f "$tmp"
pct exec "$CT_ID" -- bash -c "chmod 600 ${ICE_SERVERS_PATH}.tmp && mv ${ICE_SERVERS_PATH}.tmp ${ICE_SERVERS_PATH}"
log "ice-servers.json written (atomic), chmod 600"
}
restart_galene() {
pct exec "$CT_ID" -- systemctl restart galene
log "galene restarted"
}
main() {
log "deploy-galene-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push ice-servers.json and restart galene"
log " config: $(echo "$ICE_CONFIG" | jq -c .)"
exit 0
fi
push_ice_servers
restart_galene
log "done"
}
main

View File

@@ -0,0 +1,117 @@
#!/bin/bash
# deploy-gitea-credentials.sh — деплой кредов Gitea в CT 103
# Секреты из Vaultwarden (объект GITEA). Атомарная запись .env.
#
# Использование:
# /root/scripts/deploy-gitea-credentials.sh
# /root/scripts/deploy-gitea-credentials.sh --dry-run
#
# Ротация: сменил пароль/токен в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=103
GITEA_PATH="/opt/gitea"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local item
item=$(bw get item "GITEA" 2>/dev/null)
POSTGRES_PASSWORD=$(bw get password "GITEA" 2>/dev/null)
GITEA_RUNNER_REGISTRATION_TOKEN=$(echo "$item" | jq -r '.fields[] | select(.name=="GITEA_RUNNER_REGISTRATION_TOKEN") | .value // empty')
if [ -z "$POSTGRES_PASSWORD" ]; then
err "GITEA: missing password (POSTGRES_PASSWORD)"
exit 1
fi
if [ -z "$GITEA_RUNNER_REGISTRATION_TOKEN" ]; then
err "GITEA: missing GITEA_RUNNER_REGISTRATION_TOKEN field"
exit 1
fi
}
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
EOF
echo "$tmp"
}
push_env_atomic() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${GITEA_PATH}/.env.tmp && chmod 600 ${GITEA_PATH}/.env.tmp && mv ${GITEA_PATH}/.env.tmp ${GITEA_PATH}/.env"
log ".env written (atomic), chmod 600"
}
push_compose() {
local compose_src="${SCRIPT_DIR}/gitea/docker-compose.yml"
if [ -f "$compose_src" ]; then
pct push "$CT_ID" "$compose_src" "${GITEA_PATH}/docker-compose.yml"
log "docker-compose.yml pushed"
else
log "WARN: ${compose_src} not found, skipping compose push"
fi
}
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${GITEA_PATH} && docker compose up -d --force-recreate"
log "Gitea started"
}
main() {
log "deploy-gitea-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env and run compose"
log " POSTGRES_PASSWORD=***"
log " GITEA_RUNNER_REGISTRATION_TOKEN=***"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_atomic "$tmp"
push_compose
run_compose
log "done"
}
main

View File

@@ -0,0 +1,176 @@
#!/bin/bash
# deploy-immich-credentials.sh — деплой кредов Immich и immich-deduper на VM 200
# Секреты из Vaultwarden (объекты IMMICH, IMMICH_DEDUPER).
#
# Использование:
# /root/scripts/deploy-immich-credentials.sh
# /root/scripts/deploy-immich-credentials.sh --dry-run
#
# Требования: bw, jq, /root/.bw-master, SSH без пароля root@host → admin@192.168.1.200
#
# Vaultwarden: IMMICH — поля DB_PASSWORD, IMMICH_API_KEY, GEMINI_API_KEY и др. (см. .env).
# IMMICH_DEDUPER — поля PSQL_PASS, DEDUP_*, IMMICH_PATH, PSQL_*.
set -e
VM_SSH="admin@192.168.1.200"
IMMICH_PATH="/opt/immich"
DEDUPER_PATH="/opt/immich-deduper"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_field() {
local item="$1" name="$2"
echo "$item" | jq -r ".fields[] | select(.name==\"$name\") | .value // empty"
}
get_immich_secrets() {
local id
id=$(bw list items --search IMMICH 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true
[ -z "$id" ] && id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true
[ -z "$id" ] && { err "IMMICH not found in Vaultwarden"; exit 1; }
IMMICH_ITEM=$(bw get item "$id" 2>/dev/null) || { err "IMMICH get item failed for id=$id"; exit 1; }
DB_PASSWORD=$(get_field "$IMMICH_ITEM" "DB_PASSWORD")
IMMICH_API_KEY=$(get_field "$IMMICH_ITEM" "IMMICH_API_KEY")
GEMINI_API_KEY=$(get_field "$IMMICH_ITEM" "GEMINI_API_KEY")
if [ -z "$DB_PASSWORD" ]; then err "IMMICH: missing DB_PASSWORD field"; exit 1; fi
if [ -z "$IMMICH_API_KEY" ]; then err "IMMICH: missing IMMICH_API_KEY field"; exit 1; fi
}
get_deduper_secrets() {
local id
id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH_DEDUPER") | .id' | head -1)
[ -z "$id" ] && { err "IMMICH_DEDUPER not found in Vaultwarden"; exit 1; }
DEDUP_ITEM=$(bw get item "$id" 2>/dev/null) || {
err "IMMICH_DEDUPER not found in Vaultwarden"
exit 1
}
PSQL_PASS=$(get_field "$DEDUP_ITEM" "PSQL_PASS")
[ -z "$PSQL_PASS" ] && PSQL_PASS=$(echo "$DEDUP_ITEM" | jq -r '.login.password // empty')
DEDUP_PORT=$(get_field "$DEDUP_ITEM" "DEDUP_PORT")
DEDUP_DATA=$(get_field "$DEDUP_ITEM" "DEDUP_DATA")
DEDUP_IMAGE=$(get_field "$DEDUP_ITEM" "DEDUP_IMAGE")
IMMICH_PATH_FIELD=$(get_field "$DEDUP_ITEM" "IMMICH_PATH")
PSQL_HOST=$(get_field "$DEDUP_ITEM" "PSQL_HOST")
PSQL_PORT=$(get_field "$DEDUP_ITEM" "PSQL_PORT")
PSQL_DB=$(get_field "$DEDUP_ITEM" "PSQL_DB")
[ -z "$PSQL_PASS" ] && PSQL_PASS="${DB_PASSWORD:-}"
DEDUP_PORT="${DEDUP_PORT:-8086}"
DEDUP_DATA="${DEDUP_DATA:-/opt/immich-deduper/data}"
DEDUP_IMAGE="${DEDUP_IMAGE:-razgrizhsu/immich-deduper:latest-cpu}"
IMMICH_PATH_FIELD="${IMMICH_PATH_FIELD:-/mnt/data/library}"
PSQL_HOST="${PSQL_HOST:-database}"
PSQL_PORT="${PSQL_PORT:-5432}"
PSQL_DB="${PSQL_DB:-immich}"
}
gen_immich_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
# Immich .env (generated from Vaultwarden)
UPLOAD_LOCATION=/mnt/data/library
DB_DATA_LOCATION=/mnt/data/postgres
IMMICH_VERSION=v2
DB_PASSWORD=${DB_PASSWORD}
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
IMMICH_URL=http://immich-server:2283
IMMICH_API_KEY=${IMMICH_API_KEY}
DB_HOST=immich_postgres
DB_PORT=5432
EXTERNAL_IMMICH_URL=https://immich.katykhin.ru
GEMINI_API_KEY=${GEMINI_API_KEY}
EOF
echo "$tmp"
}
gen_deduper_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
# Deduper .env (generated from Vaultwarden)
DEDUP_PORT=${DEDUP_PORT}
DEDUP_DATA=${DEDUP_DATA}
DEDUP_IMAGE=${DEDUP_IMAGE}
IMMICH_PATH=${IMMICH_PATH_FIELD}
PSQL_HOST=${PSQL_HOST}
PSQL_PORT=${PSQL_PORT}
PSQL_DB=${PSQL_DB}
PSQL_USER=postgres
PSQL_PASS=${PSQL_PASS}
EOF
echo "$tmp"
}
push_to_vm() {
local local_file="$1" remote_path="$2"
scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -q "$local_file" "${VM_SSH}:/tmp/deploy-env.tmp" || {
err "scp to ${VM_SSH} failed. Ensure SSH key from Proxmox: ssh-copy-id ${VM_SSH}"
exit 1
}
ssh -o BatchMode=yes -o ConnectTimeout=10 "$VM_SSH" "sudo mv /tmp/deploy-env.tmp ${remote_path} && sudo chmod 600 ${remote_path}" || {
err "ssh to ${VM_SSH} failed"
exit 1
}
}
run_compose() {
ssh -o BatchMode=yes "$VM_SSH" "cd ${IMMICH_PATH} && sudo docker compose up -d --force-recreate"
ssh -o BatchMode=yes "$VM_SSH" "cd ${DEDUPER_PATH} && sudo docker compose up -d --force-recreate"
log "Immich and deduper started"
}
main() {
log "deploy-immich-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_immich_secrets
get_deduper_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env files and run compose"
log " DB_PASSWORD=*** IMMICH_API_KEY=***"
exit 0
fi
tmp_immich=$(gen_immich_env)
tmp_deduper=$(gen_deduper_env)
trap "rm -f $tmp_immich $tmp_deduper" EXIT
push_to_vm "$tmp_immich" "${IMMICH_PATH}/.env"
log "Immich .env written"
push_to_vm "$tmp_deduper" "${DEDUPER_PATH}/.env"
log "Deduper .env written"
run_compose
log "done"
}
main

View File

@@ -0,0 +1,116 @@
#!/bin/bash
# deploy-invidious-credentials.sh — деплой кредов Invidious в CT 107
# Секреты из Vaultwarden (объект INVIDIOUS). Атомарная запись .env.
#
# Использование:
# /root/scripts/deploy-invidious-credentials.sh
# /root/scripts/deploy-invidious-credentials.sh --dry-run
#
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=107
INVIDIOUS_PATH="/opt/invidious"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local item
item=$(bw get item "INVIDIOUS" 2>/dev/null)
POSTGRES_USER=$(echo "$item" | jq -r '.login.username // empty')
POSTGRES_PASSWORD=$(bw get password "INVIDIOUS" 2>/dev/null)
INVIDIOUS_COMPANION_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="SERVER_SECRET_KEY") | .value // empty')
HMAC_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="HMAC_KEY") | .value // empty')
if [ -z "$POSTGRES_USER" ] || [ -z "$POSTGRES_PASSWORD" ]; then
err "INVIDIOUS: missing username or password"
exit 1
fi
if [ -z "$INVIDIOUS_COMPANION_KEY" ]; then
err "INVIDIOUS: missing SERVER_SECRET_KEY field"
exit 1
fi
if [ -z "$HMAC_KEY" ]; then
err "INVIDIOUS: missing HMAC_KEY field"
exit 1
fi
}
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
POSTGRES_USER=${POSTGRES_USER}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=invidious
INVIDIOUS_COMPANION_KEY=${INVIDIOUS_COMPANION_KEY}
HMAC_KEY=${HMAC_KEY}
EOF
echo "$tmp"
}
push_env_atomic() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${INVIDIOUS_PATH}/.env.tmp && chmod 600 ${INVIDIOUS_PATH}/.env.tmp && mv ${INVIDIOUS_PATH}/.env.tmp ${INVIDIOUS_PATH}/.env"
log ".env written (atomic), chmod 600"
}
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${INVIDIOUS_PATH} && docker compose up -d --force-recreate"
log "Invidious started"
}
main() {
log "deploy-invidious-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env and run compose"
log " POSTGRES_USER=$POSTGRES_USER"
log " POSTGRES_PASSWORD=***"
log " INVIDIOUS_COMPANION_KEY=***"
log " HMAC_KEY=***"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_atomic "$tmp"
run_compose
log "done"
}
main

View File

@@ -0,0 +1,125 @@
#!/bin/bash
# deploy-nextcloud-credentials.sh — деплой кредов Nextcloud в CT 101
# Секреты из Vaultwarden (объект NEXTCLOUD). Атомарная запись .env, обновление config.php через occ.
#
# Использование:
# /root/scripts/deploy-nextcloud-credentials.sh
# /root/scripts/deploy-nextcloud-credentials.sh --dry-run
#
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=101
NEXTCLOUD_PATH="/opt/nextcloud"
CONFIG_PATH="/mnt/nextcloud-data/html/config/config.php"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local item
item=$(bw get item "NEXTCLOUD" 2>/dev/null)
POSTGRES_PASSWORD=$(bw get password "NEXTCLOUD" 2>/dev/null)
NEXTCLOUD_TRUSTED_DOMAINS=$(echo "$item" | jq -r '.fields[] | select(.name=="NEXTCLOUD_TRUSTED_DOMAINS") | .value // empty')
DBPASSWORD=$(echo "$item" | jq -r '.fields[] | select(.name=="dbpassword") | .value // empty')
SECRET=$(echo "$item" | jq -r '.fields[] | select(.name=="secret") | .value // empty')
PASSWORDSALT=$(echo "$item" | jq -r '.fields[] | select(.name=="passwordsalt") | .value // empty')
INSTANCEID=$(echo "$item" | jq -r '.fields[] | select(.name=="instanceid") | .value // empty')
if [ -z "$POSTGRES_PASSWORD" ]; then
err "NEXTCLOUD: missing password (POSTGRES_PASSWORD)"
exit 1
fi
NEXTCLOUD_TRUSTED_DOMAINS="${NEXTCLOUD_TRUSTED_DOMAINS:-cloud.katykhin.ru 192.168.1.101}"
}
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS}
EOF
echo "$tmp"
}
push_env_atomic() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${NEXTCLOUD_PATH}/.env.tmp && chmod 600 ${NEXTCLOUD_PATH}/.env.tmp && mv ${NEXTCLOUD_PATH}/.env.tmp ${NEXTCLOUD_PATH}/.env"
log ".env written (atomic), chmod 600"
}
push_compose() {
local compose_src="${SCRIPT_DIR}/nextcloud/docker-compose.yml"
if [ -f "$compose_src" ]; then
pct push "$CT_ID" "$compose_src" "${NEXTCLOUD_PATH}/docker-compose.yml"
log "docker-compose.yml pushed"
fi
}
update_config_occ() {
[ -n "$DBPASSWORD" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set dbpassword --value="$DBPASSWORD" 2>/dev/null || true
[ -n "$SECRET" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set secret --value="$SECRET" 2>/dev/null || true
[ -n "$PASSWORDSALT" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set passwordsalt --value="$PASSWORDSALT" 2>/dev/null || true
[ -n "$INSTANCEID" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set instanceid --value="$INSTANCEID" 2>/dev/null || true
log "config.php updated via occ"
}
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${NEXTCLOUD_PATH} && docker compose up -d --force-recreate"
log "Nextcloud started"
}
main() {
log "deploy-nextcloud-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env, compose, update config, run compose"
log " POSTGRES_PASSWORD=***"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_atomic "$tmp"
push_compose
run_compose
update_config_occ
log "done"
}
main

View File

@@ -0,0 +1,129 @@
#!/bin/bash
# deploy-paperless-credentials.sh — деплой кредов Paperless в CT 104
# Секреты из Vaultwarden (объект PAPERLESS). Атомарная запись docker-compose.env.
#
# Использование:
# /root/scripts/deploy-paperless-credentials.sh
# /root/scripts/deploy-paperless-credentials.sh --dry-run
#
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=104
PAPERLESS_PATH="/opt/paperless"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local item
item=$(bw get item "PAPERLESS" 2>/dev/null)
POSTGRES_PASSWORD=$(bw get password "PAPERLESS" 2>/dev/null)
PAPERLESS_URL=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_URL") | .value // empty')
PAPERLESS_SECRET_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_SECRET_KEY") | .value // empty')
PAPERLESS_TIME_ZONE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_TIME_ZONE") | .value // empty')
PAPERLESS_OCR_LANGUAGE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGE") | .value // empty')
PAPERLESS_OCR_LANGUAGES=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGES") | .value // empty')
if [ -z "$POSTGRES_PASSWORD" ]; then
err "PAPERLESS: missing password (POSTGRES_PASSWORD)"
exit 1
fi
if [ -z "$PAPERLESS_SECRET_KEY" ]; then
err "PAPERLESS: missing PAPERLESS_SECRET_KEY field"
exit 1
fi
PAPERLESS_URL="${PAPERLESS_URL:-https://docs.katykhin.ru}"
PAPERLESS_TIME_ZONE="${PAPERLESS_TIME_ZONE:-Europe/Moscow}"
PAPERLESS_OCR_LANGUAGE="${PAPERLESS_OCR_LANGUAGE:-rus+eng}"
PAPERLESS_OCR_LANGUAGES="${PAPERLESS_OCR_LANGUAGES:-rus}"
}
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
PAPERLESS_URL=${PAPERLESS_URL}
PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
PAPERLESS_TIME_ZONE=${PAPERLESS_TIME_ZONE}
PAPERLESS_OCR_LANGUAGE=${PAPERLESS_OCR_LANGUAGE}
PAPERLESS_OCR_LANGUAGES=${PAPERLESS_OCR_LANGUAGES}
EOF
echo "$tmp"
}
push_env_atomic() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${PAPERLESS_PATH}/docker-compose.env.tmp && chmod 600 ${PAPERLESS_PATH}/docker-compose.env.tmp && mv ${PAPERLESS_PATH}/docker-compose.env.tmp ${PAPERLESS_PATH}/docker-compose.env"
log "docker-compose.env written (atomic), chmod 600"
}
push_compose() {
local compose_src="${SCRIPT_DIR}/paperless/docker-compose.yml"
if [ -f "$compose_src" ]; then
pct push "$CT_ID" "$compose_src" "${PAPERLESS_PATH}/docker-compose.yml"
log "docker-compose.yml pushed"
else
log "WARN: ${compose_src} not found, skipping compose push"
fi
}
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${PAPERLESS_PATH} && docker compose up -d --force-recreate"
log "Paperless started"
}
main() {
log "deploy-paperless-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push docker-compose.env and run compose"
log " POSTGRES_PASSWORD=***"
log " PAPERLESS_URL=$PAPERLESS_URL"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_atomic "$tmp"
push_compose
run_compose
log "done"
}
main

View File

@@ -0,0 +1,130 @@
#!/bin/bash
# deploy-rag-credentials.sh — деплой кредов RAG-service в CT 105
# Секреты из Vaultwarden (объект RAG_SERVICE). Атомарная запись .env.
#
# Использование:
# /root/scripts/deploy-rag-credentials.sh
# /root/scripts/deploy-rag-credentials.sh --dry-run
#
# Ротация: сменил RAG_API_KEY в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
# Vaultwarden: создать запись RAG_SERVICE с полем RAG_API_KEY (тип hidden).
set -e
CT_ID=105
RAG_PATH="/home/rag-service"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
local item
item=$(bw get item "RAG_SERVICE" 2>/dev/null) || {
err "RAG_SERVICE not found in Vaultwarden. Create it: type Login, add custom field RAG_API_KEY (hidden)."
exit 1
}
RAG_API_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="RAG_API_KEY") | .value // empty')
if [ -z "$RAG_API_KEY" ]; then
err "RAG_SERVICE: missing RAG_API_KEY field"
exit 1
fi
}
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
# RAG Service Configuration (generated from Vaultwarden)
# Модель
RAG_MODEL=sentence-transformers/all-MiniLM-L12-v2
RAG_CACHE_DIR=data/models
# VectorStore
RAG_VECTORS_PATH=data/vectors/vectors.npz
RAG_MAX_EXAMPLES=10000
RAG_SCORE_MULTIPLIER=5.0
# Батч-обработка
RAG_BATCH_SIZE=16
# Минимальная длина текста
RAG_MIN_TEXT_LENGTH=3
# API настройки
RAG_API_HOST=0.0.0.0
RAG_API_PORT=8000
# Безопасность
RAG_API_KEY=${RAG_API_KEY}
RAG_ALLOW_NO_AUTH=false
# Автосохранение векторов
RAG_AUTOSAVE_INTERVAL=600
# Логирование
LOG_LEVEL=INFO
EOF
echo "$tmp"
}
push_env_atomic() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${RAG_PATH}/.env.tmp && chmod 600 ${RAG_PATH}/.env.tmp && mv ${RAG_PATH}/.env.tmp ${RAG_PATH}/.env"
log ".env written (atomic), chmod 600"
}
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${RAG_PATH} && docker compose up -d --force-recreate"
log "RAG-service started"
}
main() {
log "deploy-rag-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env and run compose"
log " RAG_API_KEY=***"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_atomic "$tmp"
run_compose
log "done"
}
main

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Deploy SSH public key to all LXC containers and VM 200 in homelab.
# Run from machine that can reach Proxmox (192.168.1.150).
# Usage: ./deploy-ssh-keys-homelab.sh [path-to-public-key]
# Default: ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub
set -e
PROXMOX="${PROXMOX:-root@192.168.1.150}"
KEY_FILE="${1:-$HOME/.ssh/id_rsa.pub}"
[ -f "$HOME/.ssh/id_ed25519.pub" ] && [ ! -f "$KEY_FILE" ] && KEY_FILE="$HOME/.ssh/id_ed25519.pub"
if [ ! -f "$KEY_FILE" ]; then
echo "Usage: $0 [path-to-public-key]"
echo "No key found at $KEY_FILE"
exit 1
fi
CT_IDS="100 101 103 104 105 107 108 109"
echo "Deploying key from $KEY_FILE to homelab hosts..."
# Copy key to Proxmox temp, then deploy from there
TMP_KEY="/tmp/deploy-ssh-key-$$.pub"
scp -q "$KEY_FILE" "$PROXMOX:$TMP_KEY"
trap "ssh $PROXMOX 'rm -f $TMP_KEY'" EXIT
# Proxmox host
echo "Proxmox (192.168.1.150)..."
ssh "$PROXMOX" "mkdir -p /root/.ssh && chmod 700 /root/.ssh && grep -qF \"\$(cat $TMP_KEY)\" /root/.ssh/authorized_keys 2>/dev/null || cat $TMP_KEY >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys"
# LXC containers
for id in $CT_IDS; do
echo "CT $id (192.168.1.$id)..."
ssh "$PROXMOX" "pct exec $id -- bash -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' && pct push $id $TMP_KEY /tmp/key.pub && pct exec $id -- bash -c 'grep -qF \"\$(cat /tmp/key.pub)\" /root/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && rm /tmp/key.pub'"
done
# VM 200 (admin user; root may be disabled)
echo "VM 200 (admin@192.168.1.200)..."
ssh "$PROXMOX" "scp -o StrictHostKeyChecking=accept-new $TMP_KEY admin@192.168.1.200:/tmp/key.pub && ssh admin@192.168.1.200 'mkdir -p /home/admin/.ssh /root/.ssh && chmod 700 /home/admin/.ssh /root/.ssh 2>/dev/null; grep -qF \"\$(cat /tmp/key.pub)\" /home/admin/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /home/admin/.ssh/authorized_keys; echo \"\$(cat /tmp/key.pub)\" | sudo tee -a /root/.ssh/authorized_keys >/dev/null; chmod 600 /home/admin/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null; rm /tmp/key.pub'"
echo "Done. Connect: ssh root@192.168.1.{100,101,103,104,105,107,108,109}, ssh admin@192.168.1.200"

View File

@@ -0,0 +1,111 @@
#!/bin/bash
# deploy-vpn-route-check.sh — идемпотентный деплой vpn-route-check на CT 100
# Секреты берутся из Vaultwarden (объект localhost), .env генерируется на Proxmox и пушится в CT.
#
# Использование:
# /root/scripts/deploy-vpn-route-check.sh # деплой
# /root/scripts/deploy-vpn-route-check.sh --dry-run # только проверка, без записи и compose
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=100
CT_PATH="/opt/docker/vpn-route-check"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
# --- 1. Разблокировка bw (reuse session если возможно)
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
# --- 2. Получить секреты из Vaultwarden (localhost)
get_secrets() {
local host user pass
host=$(bw get item "localhost" 2>/dev/null | jq -r '.fields[] | select(.name=="ROUTER_TELNET_HOST") | .value // empty')
user=$(bw get username "localhost" 2>/dev/null)
pass=$(bw get password "localhost" 2>/dev/null)
if [ -z "$user" ] || [ -z "$pass" ]; then
err "localhost: missing username or password in Vaultwarden"
exit 1
fi
host="${host:-192.168.1.1}"
ROUTER_TELNET_HOST="$host"
ROUTER_TELNET_USER="$user"
ROUTER_TELNET_PASSWORD="$pass"
}
# --- 3. Сгенерировать .env во временный файл
gen_env() {
local tmp
tmp=$(mktemp)
cat > "$tmp" << EOF
ROUTER_TELNET_HOST=${ROUTER_TELNET_HOST}
ROUTER_TELNET_USER=${ROUTER_TELNET_USER}
ROUTER_TELNET_PASSWORD=${ROUTER_TELNET_PASSWORD}
EOF
echo "$tmp"
}
# --- 4. Атомарно записать .env в CT 100
push_env_to_ct() {
local tmp="$1"
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${CT_PATH}/.env.tmp && chmod 600 ${CT_PATH}/.env.tmp && mv ${CT_PATH}/.env.tmp ${CT_PATH}/.env"
log ".env written to CT $CT_ID (atomic)"
}
# --- 5. docker compose up -d
run_compose() {
pct exec "$CT_ID" -- bash -c "cd ${CT_PATH} && docker compose up -d --force-recreate"
log "vpn-route-check started"
}
# --- main
main() {
log "deploy-vpn-route-check start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push .env and run compose"
log " ROUTER_TELNET_HOST=$ROUTER_TELNET_HOST"
log " ROUTER_TELNET_USER=$ROUTER_TELNET_USER"
log " ROUTER_TELNET_PASSWORD=***"
exit 0
fi
tmp=$(gen_env)
trap "rm -f $tmp" EXIT
push_env_to_ct "$tmp"
run_compose
log "done"
}
main

View File

@@ -0,0 +1,95 @@
#!/bin/bash
# deploy-wireguard-credentials.sh — деплой конфига WireGuard в CT 109
# Секреты из Vaultwarden (объект LOCAL_VPN_SERVER_WG, поле wg0_conf — полный конфиг).
#
# Использование:
# /root/scripts/deploy-wireguard-credentials.sh
# /root/scripts/deploy-wireguard-credentials.sh --dry-run
#
# Перед первым запуском: создать в Vaultwarden запись LOCAL_VPN_SERVER_WG,
# добавить кастомное поле wg0_conf (hidden) с полным содержимым /etc/wireguard/wg0.conf.
#
# Ротация: сменил ключи в Vaultwarden → запустил скрипт → systemctl restart wg-quick@wg0
#
# Требования: bw, jq, /root/.bw-master (chmod 600)
set -e
CT_ID=109
WG_CONF_PATH="/etc/wireguard/wg0.conf"
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
esac
done
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
log() { echo "[$(date -Iseconds)] $*"; }
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
ensure_bw_unlocked() {
local status
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
if [ "$status" = "unlocked" ]; then
log "bw already unlocked, reusing session"
return 0
fi
if [ ! -f "$BW_MASTER_FILE" ]; then
err "Missing $BW_MASTER_FILE"
exit 1
fi
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
err "bw unlock failed"
exit 1
}
log "bw unlocked"
}
get_secrets() {
WG_CONF=$(bw get item "LOCAL_VPN_SERVER_WG" 2>/dev/null | jq -r '.fields[] | select(.name=="wg0_conf") | .value // empty')
if [ -z "$WG_CONF" ]; then
err "LOCAL_VPN_SERVER_WG not found or missing wg0_conf field. Create it in Vaultwarden, add field wg0_conf with full wg0.conf content."
exit 1
fi
if ! echo "$WG_CONF" | grep -q '\[Interface\]'; then
err "wg0_conf: invalid format (expected [Interface] section)"
exit 1
fi
}
push_conf() {
local tmp
tmp=$(mktemp)
echo "$WG_CONF" > "$tmp"
pct push "$CT_ID" "$tmp" "${WG_CONF_PATH}.tmp"
rm -f "$tmp"
pct exec "$CT_ID" -- bash -c "chmod 600 ${WG_CONF_PATH}.tmp && mv ${WG_CONF_PATH}.tmp ${WG_CONF_PATH}"
log "wg0.conf written (atomic), chmod 600"
}
restart_wg() {
pct exec "$CT_ID" -- systemctl restart wg-quick@wg0
log "wg-quick@wg0 restarted"
}
main() {
log "deploy-wireguard-credentials start (dry_run=$DRY_RUN)"
ensure_bw_unlocked
get_secrets
if [ "$DRY_RUN" = true ]; then
log "DRY-RUN: would push wg0.conf and restart WireGuard"
log " wg0_conf: $(echo "$WG_CONF" | head -3)..."
exit 0
fi
push_conf
restart_wg
log "done"
}
main

View File

@@ -0,0 +1,74 @@
# Шаблон для /opt/gitea/ на CT 103
# Секреты в .env (генерируется deploy-gitea-credentials.sh из Vaultwarden).
# .env не коммитить.
services:
db:
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
env_file: .env
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: gitea
volumes:
- gitea-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
server:
image: docker.gitea.com/gitea:1.25
container_name: gitea
restart: unless-stopped
depends_on:
db:
condition: service_healthy
env_file: .env
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: ${POSTGRES_PASSWORD}
GITEA__server__DOMAIN: 192.168.1.103
GITEA__server__ROOT_URL: http://192.168.1.103:3000/
GITEA__server__SSH_PORT: 2222
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
runner:
image: docker.io/gitea/act_runner:latest
restart: unless-stopped
depends_on:
server:
condition: service_healthy
env_file: .env
environment:
GITEA_INSTANCE_URL: http://server:3000
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
GITEA_RUNNER_NAME: gitea-103-runner
GITEA_RUNNER_LABELS: docker:docker://alpine:latest
volumes:
- runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
volumes:
gitea-data:
gitea-postgres:
runner-data:

View File

@@ -0,0 +1,84 @@
# Шаблон для /opt/invidious/docker-compose.yml на CT 107
# Секреты в .env (генерируется deploy-invidious-credentials.sh из Vaultwarden).
# .env не коммитить.
services:
invidious:
image: quay.io/invidious/invidious:latest
restart: unless-stopped
ports:
- "3000:3000"
env_file: .env
environment:
INVIDIOUS_CONFIG: |
db:
dbname: invidious
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
host: invidious-db
port: 5432
check_tables: true
invidious_companion:
- private_url: "http://companion:8282/companion"
invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}"
external_port: 443
domain: "video.katykhin.ru"
https_only: true
use_pubsub_feeds: true
use_innertube_for_captions: true
hmac_key: "${HMAC_KEY}"
default_user_preferences:
default_home: Popular
dark_mode: "light"
player_style: "youtube"
vr_mode: false
automatic_instance_redirect: false
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1
interval: 30s
timeout: 5s
retries: 2
logging:
options:
max-size: "1G"
max-file: "4"
depends_on:
invidious-db:
condition: service_healthy
companion:
image: quay.io/invidious/invidious-companion:latest
env_file: .env
environment:
SERVER_SECRET_KEY: ${INVIDIOUS_COMPANION_KEY}
restart: unless-stopped
logging:
options:
max-size: "1G"
max-file: "4"
cap_drop:
- ALL
read_only: true
volumes:
- companioncache:/var/tmp/youtubei.js:rw
security_opt:
- no-new-privileges:true
invidious-db:
image: docker.io/library/postgres:14
restart: unless-stopped
volumes:
- postgresdata:/var/lib/postgresql/data
- ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
env_file: .env
environment:
POSTGRES_DB: invidious
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
volumes:
postgresdata:
companioncache:

View File

@@ -0,0 +1,52 @@
# Шаблон для /opt/nextcloud/ на CT 101
# Секреты в .env (генерируется deploy-nextcloud-credentials.sh из Vaultwarden).
# .env не коммитить.
services:
db:
image: docker.io/library/postgres:16
restart: unless-stopped
volumes:
- /mnt/nextcloud-data/pgdata:/var/lib/postgresql/data
env_file: .env
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: docker.io/library/redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes
nextcloud:
image: docker.io/nextcloud:latest
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
ports:
- "8080:80"
volumes:
- /mnt/nextcloud-data/html:/var/www/html
- /mnt/nextcloud-extra:/mnt/nextcloud-extra
- /opt/nextcloud/php-uploads.ini:/usr/local/etc/php/conf.d/zz-uploads.ini:ro
env_file: .env
environment:
APACHE_BODY_LIMIT: "0"
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS}
OVERWRITEPROTOCOL: https
OVERWRITEHOST: cloud.katykhin.ru
OVERWRITECLIURL: https://cloud.katykhin.ru
REDIS_HOST: redis
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

View File

@@ -1,15 +1,17 @@
#!/bin/bash
# Add vault.katykhin.ru → 192.168.1.103:8280 via NPM API + Access List (LAN + VPN only)
# Usage: NPM_EMAIL=j3tears100@gmail.com NPM_PASSWORD=xxx ./npm-add-proxy-vault.sh
# Usage: NPM_EMAIL=... NPM_PASSWORD=... ./npm-add-proxy-vault.sh
# NPM credentials: Vaultwarden, объект NPM_ADMIN (username=email, password)
# Run from host that can reach NPM, or: ssh root@192.168.1.150 "pct exec 100 -- bash -s" < scripts/npm-add-proxy-vault.sh
# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env or below)
# NPM credentials: see docs/containers/container-100.md
# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env)
set -e
NPM_URL="${NPM_URL:-http://192.168.1.100:81}"
API="$NPM_URL/api"
NPM_EMAIL="${NPM_EMAIL:-j3tears100@gmail.com}"
NPM_PASSWORD="${NPM_PASSWORD:-kqEUubVq02DJTS8}"
if [ -z "$NPM_EMAIL" ] || [ -z "$NPM_PASSWORD" ]; then
echo "Set NPM_EMAIL and NPM_PASSWORD (from Vaultwarden, объект NPM_ADMIN)"
exit 1
fi
echo "1. Getting token..."
TOKEN=$(curl -s -X POST "$API/tokens" \

View File

@@ -0,0 +1,41 @@
# Шаблон для /opt/paperless/ на CT 104
# Секреты в docker-compose.env (генерируется deploy-paperless-credentials.sh из Vaultwarden).
# docker-compose.env не коммитить.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
restart: unless-stopped
volumes:
- /mnt/paperless-data/pgdata:/var/lib/postgresql
env_file: docker-compose.env
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "8000:8000"
volumes:
- /mnt/paperless-data/data:/usr/src/paperless/data
- /mnt/paperless-data/media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
redisdata:

View File

@@ -30,7 +30,7 @@ fi
# Секреты из Vaultwarden (объект RESTIC)
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md"
exit 1
fi
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then

View File

@@ -0,0 +1,16 @@
# Шаблон для /opt/docker/vpn-route-check/docker-compose.yml на CT 100
# Секреты в .env (генерируется deploy-vpn-route-check.sh из Vaultwarden).
# .env не коммитить.
services:
vpn-route-check:
build: .
container_name: vpn-route-check
network_mode: host
env_file: .env
volumes:
- vpn-route-check-data:/data
restart: unless-stopped
volumes:
vpn-route-check-data: