Add Matrix/Hermes/Omniroute to jeannie pipeline (SOPS-encrypted secrets)
Codify the full Matrix homeserver stack, OmniRoute LLM gateway, and
Hermes agent into the Jeannie deploy pipeline so the repo reproduces
the running homelab from scratch.
New files:
infra/matrix-server/ — Synapse + Postgres + Element docker-compose,
homeserver.yaml.tpl, log_config, element config, bootstrap.sh
infra/omniroute/ — diegosouzapw/omniroute:latest on port 20128
infra/hermes.secret.yaml — SOPS-encrypted bot password + API key
infra/matrix-server/matrix.secret.yaml — SOPS-encrypted Synapse secrets
.sops.yaml — age recipient + encrypted_regex: '.*'
jeannie additions:
deploy_matrix() — renders homeserver.yaml from template, creates
signing key via 'generate' on first boot, delegates to bootstrap.sh
deploy_omniroute() — docker compose up with env-file support
deploy_hermes() — git clone + venv + 'hermes gateway install',
writes ~/.hermes/.env (mode 600), registers non-admin bot via
shared-secret CLI, idempotent
sops_load_secrets() — decrypts *.secret.yaml via age key at deploy
time; fails loudly if the age key is missing
ensure_sops_age_tools() — installs sops from getsops GitHub release
when apt has no sops package (Debian trixie)
DAG: hermes-deploy depends on matrix-deploy + omniroute-deploy.
Root cause: deploy_hermes previously used wrong entrypoint (python
module instead of ./hermes launcher); generate was called with
--generate-config (invalid subcommand); .sops.yaml encrypted_regex
'^password$' left prefixed keys as plaintext — all fixed in this
commit.
This commit is contained in:
parent
d6e33250f4
commit
fe661f9b24
|
|
@ -11,6 +11,10 @@ bootstrap/host/inventory/
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zip
|
*.zip
|
||||||
infra/gitea/data/
|
infra/gitea/data/
|
||||||
|
infra/matrix-server/synapse-data/
|
||||||
|
infra/matrix-server/postgres-data/
|
||||||
|
infra/matrix-server/.env
|
||||||
|
infra/omniroute/.env
|
||||||
infra/youtube-backup/.env
|
infra/youtube-backup/.env
|
||||||
infra/youtube-backup/cookies.txt
|
infra/youtube-backup/cookies.txt
|
||||||
infra/youtube-backup/archive.txt
|
infra/youtube-backup/archive.txt
|
||||||
|
|
@ -21,6 +25,8 @@ infra/youtube-backup/logs/
|
||||||
*.decrypted.yaml
|
*.decrypted.yaml
|
||||||
*.plain.yaml
|
*.plain.yaml
|
||||||
*.secret.local.yaml
|
*.secret.local.yaml
|
||||||
|
*.secret.yaml.plain
|
||||||
|
*.secret.plain
|
||||||
.age-key.txt
|
.age-key.txt
|
||||||
sops-age.key
|
sops-age.key
|
||||||
.sops.yaml.tmp
|
.sops.yaml.tmp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Copy this file to .sops.yaml after replacing the age recipient with the
|
||||||
|
# public key generated on the Debian homelab server.
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: '(^|/).*\.(secret|enc)\.(ya?ml|json)$'
|
||||||
|
encrypted_regex: '.*'
|
||||||
|
age: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
28
homelab.yml
28
homelab.yml
|
|
@ -12,6 +12,8 @@ domain:
|
||||||
subdomains:
|
subdomains:
|
||||||
- demos.lab2025.duckdns.org
|
- demos.lab2025.duckdns.org
|
||||||
- heimdall.lab2025.duckdns.org
|
- heimdall.lab2025.duckdns.org
|
||||||
|
- matrix.lab2025.duckdns.org
|
||||||
|
- element.lab2025.duckdns.org
|
||||||
- grafana.lab2025.duckdns.org
|
- grafana.lab2025.duckdns.org
|
||||||
- prometheus.lab2025.duckdns.org
|
- prometheus.lab2025.duckdns.org
|
||||||
- alertmanager.lab2025.duckdns.org
|
- alertmanager.lab2025.duckdns.org
|
||||||
|
|
@ -116,6 +118,32 @@ services:
|
||||||
suwayomi_port: 4567
|
suwayomi_port: 4567
|
||||||
maintainerr_port: 6246
|
maintainerr_port: 6246
|
||||||
flaresolverr_port: 8191
|
flaresolverr_port: 8191
|
||||||
|
matrix:
|
||||||
|
host: debian
|
||||||
|
install_dir: /home/jv/matrix-server
|
||||||
|
synapse_port: 8008
|
||||||
|
element_port: 8081
|
||||||
|
server_name: matrix.lab2025.duckdns.org
|
||||||
|
synapse_image: matrixdotorg/synapse:latest
|
||||||
|
element_image: vectorim/element-web:latest
|
||||||
|
postgres_image: postgres:16-alpine
|
||||||
|
postgres_db: synapse
|
||||||
|
postgres_user: synapse
|
||||||
|
bot_user: hermes
|
||||||
|
allowed_users:
|
||||||
|
- "@jv:matrix.lab2025.duckdns.org"
|
||||||
|
omniroute:
|
||||||
|
host: debian
|
||||||
|
install_dir: /home/jv/homelab-omniroute
|
||||||
|
port: 20128
|
||||||
|
image: diegosouzapw/omniroute:latest
|
||||||
|
container_name: omniroute
|
||||||
|
hermes:
|
||||||
|
host: debian
|
||||||
|
home: /home/jv/.hermes
|
||||||
|
install_dir: /home/jv/.hermes/hermes-agent
|
||||||
|
uv_bin: /home/jv/.hermes/bin/uv
|
||||||
|
bot_user: hermes
|
||||||
traefik:
|
traefik:
|
||||||
host: kubernetes
|
host: kubernetes
|
||||||
load_balancer_ip: 192.168.100.240
|
load_balancer_ip: 192.168.100.240
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Hermes agent secrets - SOPS-encrypted. Do not commit plaintext.
|
||||||
|
# Decrypted at deploy time by jeannie deploy_hermes.
|
||||||
|
MATRIX_BOT_PASSWORD: ENC[AES256_GCM,data:+21aq+VzaYZLizC2p8oOEf1614M=,iv:qKpVrpE50ByIOXW32RnYy7zwPIbtt3UybAn7Pb8bY5Y=,tag:utUx46AyxK+jk3c7Wdnygw==,type:str]
|
||||||
|
HERMES_OMNIROUTE_API_KEY: ""
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBySEFyREdGOS9HOGtkeVRF
|
||||||
|
M0w4aUcxK2F5NTdSR29CSDY2SHZVbWlXZ0hzCmZOV3VXa3J5eEJLY2pTa2RHTGhV
|
||||||
|
eWpXdXVmVWFiLzZpMmlyUmJIQ3hUdjAKLS0tIG8ybWVNTXNZTHM4dnJ4Y2Z5VXAr
|
||||||
|
TXVIVXVWenlBc0lEZ0VSWmdFQmpPSGsK2FtwcyBFqVzCphxGVV8xg6QZea3SC6wt
|
||||||
|
gX1rp3icFegLisy/m4cUJkfku9zTHoHjsqPjbl+xzs3iaReHSVQRlA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
|
encrypted_regex: .*
|
||||||
|
lastmodified: "2026-09-16T16:32:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:0wKCLSIIDh4+/IHY/UN2KeYEdHBn0ZhCVRcIa3TV4P8CtRbcItQbhj5e95q5FPWJRBASXIxj/WwOkyd5JFUmxH5XhaeKCHvxbgNMClPb8Ic5KSnNMRgSZtrCgKlACySFEDL5aD9mxqkd3f5bVd+JY04HGHkSJUWRfdGnNdwGfnw=,iv:BPyQLTty1qjSldlxeUEC/Uo80H1R8Vlfey9ie26JZ30=,tag:oYZ2TVwKUgf+oPl8nvqeLA==,type:str]
|
||||||
|
version: 3.13.3
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Matrix stack — non-secret defaults.
|
||||||
|
# Copy to a sibling .env (gitignored) or export these as env vars when running
|
||||||
|
# deploy_matrix. NEVER put real secrets in this committed example.
|
||||||
|
MATRIX_SERVER_NAME=matrix.lab2025.duckdns.org
|
||||||
|
MATRIX_SYNAPSE_PORT=8008
|
||||||
|
MATRIX_ELEMENT_PORT=8081
|
||||||
|
MATRIX_SYNAPSE_CONTAINER_NAME=matrix-synapse
|
||||||
|
MATRIX_POSTGRES_CONTAINER_NAME=matrix-postgres
|
||||||
|
MATRIX_ELEMENT_CONTAINER_NAME=matrix-element
|
||||||
|
MATRIX_POSTGRES_DB=synapse
|
||||||
|
MATRIX_POSTGRES_USER=synapse
|
||||||
|
|
||||||
|
# SECRETS (must be supplied at deploy time, not committed):
|
||||||
|
# MATRIX_POSTGRES_PASSWORD postgres/synapse db password
|
||||||
|
# MATRIX_REGISTRATION_SHARED_SECRET shared registration secret for register_new_matrix_user
|
||||||
|
# MATRIX_MACAROON_SECRET_KEY macaroon secret
|
||||||
|
# MATRIX_FORM_SECRET form secret
|
||||||
|
# MATRIX_BOT_PASSWORD hermes bot account password
|
||||||
|
# (The synapse signing key is generated by Synapse on first boot — not a secret you set.)
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy/update the self-hosted Matrix stack (Synapse + Postgres + Element).
|
||||||
|
# Idempotent: renders homeserver.yaml from the committed template, ensures
|
||||||
|
# correct data-dir ownership, and starts containers.
|
||||||
|
#
|
||||||
|
# Secrets are read from an install-dir .env (written by jeannie deploy_matrix),
|
||||||
|
# NOT from the repo. The signing key is created by Synapse on first boot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${MATRIX_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Missing secret env file ${ENV_FILE}. Run jeannie deploy-matrix first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${ENV_FILE}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
MATRIX_SERVER_NAME="${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||||
|
MATRIX_POSTGRES_DB="${MATRIX_POSTGRES_DB:-synapse}"
|
||||||
|
MATRIX_POSTGRES_USER="${MATRIX_POSTGRES_USER:-synapse}"
|
||||||
|
SYNAPSE_UID="${MATRIX_SYNAPSE_UID:-991}"
|
||||||
|
POSTGRES_UID="${MATRIX_POSTGRES_UID:-70}"
|
||||||
|
|
||||||
|
render_homeserver() {
|
||||||
|
local tpl="${SCRIPT_DIR}/homeserver.yaml.tpl"
|
||||||
|
local out="${SCRIPT_DIR}/synapse-data/homeserver.yaml"
|
||||||
|
for var in MATRIX_SERVER_NAME MATRIX_POSTGRES_DB MATRIX_POSTGRES_USER \
|
||||||
|
MATRIX_POSTGRES_PASSWORD MATRIX_REGISTRATION_SHARED_SECRET \
|
||||||
|
MATRIX_MACAROON_SECRET_KEY MATRIX_FORM_SECRET; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
echo "Missing required secret/env ${var}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
envsubst \
|
||||||
|
'${MATRIX_SERVER_NAME} ${MATRIX_POSTGRES_DB} ${MATRIX_POSTGRES_USER} ${MATRIX_POSTGRES_PASSWORD} ${MATRIX_REGISTRATION_SHARED_SECRET} ${MATRIX_MACAROON_SECRET_KEY} ${MATRIX_FORM_SECRET}' \
|
||||||
|
<"${tpl}" >"${out}"
|
||||||
|
cp "${SCRIPT_DIR}/log_config.yaml" "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.log.config"
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo mkdir -p "${SCRIPT_DIR}/synapse-data" "${SCRIPT_DIR}/postgres-data"
|
||||||
|
|
||||||
|
# First boot: generate signing key + log config (the files a hand-written
|
||||||
|
# homeserver.yaml would not provide). Without this Synapse fails to boot on a
|
||||||
|
# missing signing key. Only touches the data dir (not postgres).
|
||||||
|
if [[ ! -f "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.signing.key" ]]; then
|
||||||
|
echo "First boot: generating Synapse signing key + log config..."
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" \
|
||||||
|
run --rm --no-deps synapse generate --config-path /data/homeserver.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
render_homeserver
|
||||||
|
|
||||||
|
# Ownership foot-gun: wrong uid prevents boot (synapse 991, postgres 70).
|
||||||
|
sudo chown -R "${SYNAPSE_UID}:${SYNAPSE_UID}" "${SCRIPT_DIR}/synapse-data"
|
||||||
|
sudo chown -R "${POSTGRES_UID}:${POSTGRES_UID}" "${SCRIPT_DIR}/postgres-data"
|
||||||
|
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
echo "Matrix stack up:"
|
||||||
|
echo " Synapse http://127.0.0.1:${MATRIX_SYNAPSE_PORT:-8008}"
|
||||||
|
echo " Element http://127.0.0.1:${MATRIX_ELEMENT_PORT:-8081}"
|
||||||
|
echo " server_name ${MATRIX_SERVER_NAME}"
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
Next: register users (hermes bot, and any humans) via the shared-secret CLI:
|
||||||
|
docker exec -u 991 matrix-synapse register_new_matrix_user -c /data/homeserver.yaml -u NAME -p PASS [--no-admin]
|
||||||
|
EOF
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
services:
|
||||||
|
synapse:
|
||||||
|
image: matrixdotorg/synapse:latest
|
||||||
|
container_name: ${MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
SYNAPSE_SERVER_NAME: ${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||||
|
SYNAPSE_REPORT_STATS: "no"
|
||||||
|
# federation disabled (user choice): client-only listener on 8008
|
||||||
|
ports:
|
||||||
|
- "${MATRIX_SYNAPSE_PORT:-8008}:8008"
|
||||||
|
volumes:
|
||||||
|
- ./synapse-data:/data
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ${MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
||||||
|
POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
||||||
|
POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
element:
|
||||||
|
image: vectorim/element-web:latest
|
||||||
|
container_name: ${MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- synapse
|
||||||
|
ports:
|
||||||
|
- "${MATRIX_ELEMENT_PORT:-8081}:80"
|
||||||
|
volumes:
|
||||||
|
# Must mount the FILE, not a directory (element-web reads /app/config.json)
|
||||||
|
- ./element-config/config.json:/app/config.json
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
networks:
|
||||||
|
matrix:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"default_server_name": "matrix.lab2025.duckdns.org",
|
||||||
|
"default_server_config": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://matrix.lab2025.duckdns.org",
|
||||||
|
"server_name": "matrix.lab2025.duckdns.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable_custom_urls": false,
|
||||||
|
"disable_guests": true,
|
||||||
|
"disable_login_language_selector": false,
|
||||||
|
"disable_3pid_login": false,
|
||||||
|
"force_verification": false,
|
||||||
|
"brand": "Matrix",
|
||||||
|
"default_federate": false,
|
||||||
|
"show_labs_settings": true,
|
||||||
|
"default_theme": "dark",
|
||||||
|
"room_directory": {
|
||||||
|
"servers": ["matrix.lab2025.duckdns.org"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Configuration file for Synapse.
|
||||||
|
# This is generated by Jeannie from infra/matrix-server/homeserver.yaml.tpl.
|
||||||
|
# Secrets (registration_shared_secret, macaroon_secret_key, form_secret, postgres
|
||||||
|
# password, signing key) are injected by the deploy_matrix() bootstrap — never store
|
||||||
|
# them in this template.
|
||||||
|
|
||||||
|
server_name: "${MATRIX_SERVER_NAME}"
|
||||||
|
pid_file: /data/homeserver.pid
|
||||||
|
listeners:
|
||||||
|
- port: 8008
|
||||||
|
resources:
|
||||||
|
- compress: false
|
||||||
|
names:
|
||||||
|
- client
|
||||||
|
- federation
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
database:
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
user: "${MATRIX_POSTGRES_USER}"
|
||||||
|
password: "${MATRIX_POSTGRES_PASSWORD}"
|
||||||
|
database: "${MATRIX_POSTGRES_DB}"
|
||||||
|
host: postgres
|
||||||
|
port: 5432
|
||||||
|
cp_min: 5
|
||||||
|
cp_max: 10
|
||||||
|
log_config: "/data/${MATRIX_SERVER_NAME}.log.config"
|
||||||
|
media_store_path: /data/media_store
|
||||||
|
registration_shared_secret: "${MATRIX_REGISTRATION_SHARED_SECRET}"
|
||||||
|
report_stats: false
|
||||||
|
macaroon_secret_key: "${MATRIX_MACAROON_SECRET_KEY}"
|
||||||
|
form_secret: "${MATRIX_FORM_SECRET}"
|
||||||
|
signing_key_path: "/data/${MATRIX_SERVER_NAME}.signing.key"
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: "matrix.org"
|
||||||
|
|
||||||
|
# vim:ft=yaml
|
||||||
|
user_directory:
|
||||||
|
enabled: true
|
||||||
|
search_all_users: true
|
||||||
|
prefer_local_users: true
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
precise:
|
||||||
|
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: precise
|
||||||
|
|
||||||
|
loggers:
|
||||||
|
_placeholder:
|
||||||
|
level: "INFO"
|
||||||
|
|
||||||
|
synapse.storage.SQL:
|
||||||
|
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||||
|
# information such as access tokens.
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [console]
|
||||||
|
|
||||||
|
disable_existing_loggers: false
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Matrix homeserver secrets - SOPS-encrypted. Do not commit plaintext.
|
||||||
|
# Decrypted at deploy time by jeannie deploy_matrix.
|
||||||
|
MATRIX_POSTGRES_PASSWORD: ENC[AES256_GCM,data:HYti676SrsKpr5UuYX7wrg==,iv:pggYfnjqZAGulrCNVBJ/vcICcsn9hic150hmRb1D5uE=,tag:rs4jSLwMZeoaUgjqCYIkTA==,type:str]
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET: ENC[AES256_GCM,data:+OChmnoaVrWhLCQHPe3sSbfuEs2H6nJuPhFPelRM+hOPCADXYZPiUPxH3RJfLY9niCg=,iv:b2wVg4Xg/qTTT7ghyNJJNOfnafESE7EZAZoVUKFJxJc=,tag:ewdDXpFoeb2/+HlLzYJySw==,type:str]
|
||||||
|
MATRIX_MACAROON_SECRET_KEY: ENC[AES256_GCM,data:0IY3J7ijJhYiqd10A2D/xo0Utok0llFFjUOOqY57QjnCA8tgeJOH4lNuiPTScKPMnnI=,iv:PkqyW3RaQpA8+TQtKI6ibo11CJVUiod/OO2QLBa4Lrs=,tag:Z8j5xqlcysgkrmbmiMVNEA==,type:str]
|
||||||
|
MATRIX_FORM_SECRET: ENC[AES256_GCM,data:WfaToWF7NO5VgpeJcuONlhHK+GopNjPnVAjFLorrronMNUzTKIICr2xjrVBq3wvPuf4=,iv:Qgf7fdO9boSV136LL7/IxNcYz33gNP7YbIQohJFEAAI=,tag:FXtUqNj/JtsrWLx/ntypag==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1THJOdkhOb21UeFk2NHdx
|
||||||
|
bUNSMTZUVDlOeUh1T1N4WnA5akpVVWVEVFRNCk02RG9kNENCVHJUSVVXNkE0ZUFh
|
||||||
|
ek8vU2NzRUwvMm1EMDNwdnc1YjhzZ2cKLS0tIFpmZU5Pb0FJRTl1ZktVVXA1RTAw
|
||||||
|
MjBHcmZQTFJqZUdHWVgxdURhQk5wRW8Ka0TXyBnr7Puq+dYVizMK+HIAEncAD2bl
|
||||||
|
5JGP5TU1Dp0I9njHJYmDAicx5iaMhfwQe4iL9/73FOy1vnfYPlv9gQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
|
encrypted_regex: .*
|
||||||
|
lastmodified: "2026-09-16T16:32:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:lyhzE48mseEWnf2+UoYy4lkBPFtvD3PuB4BR/LbTEkz4xrC0KYHqlVma5dEhLoOTs+k5rhFy3ko3lZoZ396kkrOMftd9zyYTK7Nk/ZxPRrg3gPqr/Dd4bTCGDstznNMgbsCgzklhB35IYWgP/PL5ccZiP/OnqWXoTMPRYy/EPC8=,iv:FVhAK3zGSROkI0iyhRo1+PMlXLbZQ+74A4pnlSR6Y80=,tag:x3/mNU6l8PqPBb0cRbPAuQ==,type:str]
|
||||||
|
version: 3.13.3
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# OmniRoute — non-secret defaults.
|
||||||
|
# Copy to a sibling .env or export when running deploy_omniroute.
|
||||||
|
# Secrets (JWT_SECRET, API_KEY_SECRET, STORAGE_ENCRYPTION_*) are auto-generated
|
||||||
|
# by OmniRoute into its data volume (server.env) on first boot — not stored here.
|
||||||
|
OMNIROUTE_IMAGE=diegosouzapw/omniroute:latest
|
||||||
|
OMNIROUTE_CONTAINER_NAME=omniroute
|
||||||
|
OMNIROUTE_PORT=20128
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy/update the OmniRoute LLM-routing gateway that Hermes uses as its model
|
||||||
|
# provider (http://localhost:20128/v1). Idempotent. Secrets live in the data
|
||||||
|
# volume (server.env) generated by OmniRoute on first boot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${OMNIROUTE_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Missing env file ${ENV_FILE}. Run jeannie deploy-omniroute first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${ENV_FILE}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
sudo mkdir -p "${SCRIPT_DIR}"
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
echo "OmniRoute up at http://127.0.0.1:${OMNIROUTE_PORT:-20128}"
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
services:
|
||||||
|
omniroute:
|
||||||
|
image: ${OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||||
|
container_name: ${OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_timeout: 40
|
||||||
|
ports:
|
||||||
|
- "${OMNIROUTE_PORT:-20128}:20128"
|
||||||
|
volumes:
|
||||||
|
- omniroute-data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
omniroute-data:
|
||||||
336
jeannie
336
jeannie
|
|
@ -4117,6 +4117,233 @@ ENV_EOT
|
||||||
echo "Heimdall is reachable at http://127.0.0.1:${http_port}/ and ${public_url%/}/."
|
echo "Heimdall is reachable at http://127.0.0.1:${http_port}/ and ${public_url%/}/."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deploy_matrix() {
|
||||||
|
local mode="${LAB_MATRIX_DEPLOY:-true}"
|
||||||
|
local install_dir="${LAB_MATRIX_INSTALL_DIR:-/home/jv/matrix-server}"
|
||||||
|
local image=""
|
||||||
|
local container_name=""
|
||||||
|
local http_port=""
|
||||||
|
local ssh_port=""
|
||||||
|
local domain=""
|
||||||
|
local install_docker="false"
|
||||||
|
local source_dir="${REPO_ROOT}/infra/matrix-server"
|
||||||
|
local value_name
|
||||||
|
local value
|
||||||
|
|
||||||
|
require_debian_server "deploy-matrix"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for value_name in install_dir; do
|
||||||
|
value="${!value_name}"
|
||||||
|
if [[ "${value}" == *$'\n'* ]]; then
|
||||||
|
echo "${value_name} cannot contain a newline." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/homeserver.yaml.tpl" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||||
|
echo "Missing Matrix source files in ${source_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying Matrix stack on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "MATRIX" "${install_docker}"
|
||||||
|
repair_local_docker_iptables
|
||||||
|
|
||||||
|
sudo mkdir -p "${install_dir}"
|
||||||
|
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||||
|
sudo cp "${source_dir}/homeserver.yaml.tpl" "${install_dir}/homeserver.yaml.tpl"
|
||||||
|
sudo cp "${source_dir}/log_config.yaml" "${install_dir}/log_config.yaml"
|
||||||
|
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json" 2>/dev/null || {
|
||||||
|
sudo mkdir -p "${install_dir}/element-config"
|
||||||
|
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json"
|
||||||
|
}
|
||||||
|
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||||
|
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||||
|
|
||||||
|
# Secrets come from the SOPS-encrypted repo file, decrypted at deploy time.
|
||||||
|
# (Env vars override the file, for manual/ops overrides.)
|
||||||
|
if ! sops_load_secrets "${REPO_ROOT}/infra/matrix-server/matrix.secret.yaml"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${MATRIX_POSTGRES_PASSWORD:-}" || -z "${MATRIX_REGISTRATION_SHARED_SECRET:-}" ]]; then
|
||||||
|
echo "MATRIX_POSTGRES_PASSWORD and MATRIX_REGISTRATION_SHARED_SECRET missing after decrypt." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local matrix_server_name="${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||||
|
local matrix_synapse_port="${LAB_MATRIX_SYNAPSE_PORT:-8008}"
|
||||||
|
local matrix_element_port="${LAB_MATRIX_ELEMENT_PORT:-8081}"
|
||||||
|
local postgres_db="${LAB_MATRIX_POSTGRES_DB:-synapse}"
|
||||||
|
local postgres_user="${LAB_MATRIX_POSTGRES_USER:-synapse}"
|
||||||
|
|
||||||
|
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||||
|
MATRIX_SERVER_NAME=${matrix_server_name}
|
||||||
|
MATRIX_SYNAPSE_PORT=${matrix_synapse_port}
|
||||||
|
MATRIX_ELEMENT_PORT=${matrix_element_port}
|
||||||
|
MATRIX_SYNAPSE_CONTAINER_NAME=${LAB_MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||||
|
MATRIX_POSTGRES_CONTAINER_NAME=${LAB_MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||||
|
MATRIX_ELEMENT_CONTAINER_NAME=${LAB_MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||||
|
MATRIX_POSTGRES_DB=${postgres_db}
|
||||||
|
MATRIX_POSTGRES_USER=${postgres_user}
|
||||||
|
MATRIX_POSTGRES_PASSWORD=${MATRIX_POSTGRES_PASSWORD}
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET=${MATRIX_REGISTRATION_SHARED_SECRET}
|
||||||
|
MATRIX_MACAROON_SECRET_KEY=${MATRIX_MACAROON_SECRET_KEY:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||||
|
MATRIX_FORM_SECRET=${MATRIX_FORM_SECRET:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||||
|
ENV_EOT
|
||||||
|
|
||||||
|
# Pull + render + chown + up. bootstrap.sh is the single source of truth.
|
||||||
|
# sudo the docker compose commands; data dirs are chown'd inside bootstrap.
|
||||||
|
(
|
||||||
|
cd "${install_dir}"
|
||||||
|
sudo docker compose --env-file "${install_dir}/.env" -f "${install_dir}/docker-compose.yml" pull
|
||||||
|
sudo MATRIX_ENV_FILE="${install_dir}/.env" bash "${install_dir}/bootstrap.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Matrix stack is reachable at http://127.0.0.1:${matrix_synapse_port}/ and Element at http://127.0.0.1:${matrix_element_port}/."
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_omniroute() {
|
||||||
|
local mode="${LAB_OMNIROUTE_DEPLOY:-true}"
|
||||||
|
local install_dir="${LAB_OMNIROUTE_INSTALL_DIR:-/home/jv/homelab-omniroute}"
|
||||||
|
local install_docker="false"
|
||||||
|
local source_dir="${REPO_ROOT}/infra/omniroute"
|
||||||
|
|
||||||
|
require_debian_server "deploy-omniroute"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||||
|
echo "Missing OmniRoute source files in ${source_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying OmniRoute LLM gateway on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "OMNIROUTE" "${install_docker}"
|
||||||
|
repair_local_docker_iptables
|
||||||
|
|
||||||
|
sudo mkdir -p "${install_dir}"
|
||||||
|
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||||
|
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||||
|
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||||
|
|
||||||
|
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||||
|
OMNIROUTE_IMAGE=${LAB_OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||||
|
OMNIROUTE_CONTAINER_NAME=${LAB_OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||||
|
OMNIROUTE_PORT=${LAB_OMNIROUTE_PORT:-20128}
|
||||||
|
ENV_EOT
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${install_dir}"
|
||||||
|
sudo docker compose pull
|
||||||
|
sudo bash "${install_dir}/bootstrap.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "OmniRoute is reachable at http://127.0.0.1:${LAB_OMNIROUTE_PORT:-20128}."
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_hermes() {
|
||||||
|
local mode="${LAB_HERMES_DEPLOY:-true}"
|
||||||
|
local hermes_home="${LAB_HERMES_HOME:-/home/jv/.hermes}"
|
||||||
|
local install_dir="${LAB_HERMES_INSTALL_DIR:-/home/jv/.hermes/hermes-agent}"
|
||||||
|
local uv_bin="${LAB_HERMES_UV_BIN:-/home/jv/.hermes/bin/uv}"
|
||||||
|
local install_docker="${LAB_HERMES_INSTALL_DOCKER:-false}"
|
||||||
|
local value_name
|
||||||
|
local value
|
||||||
|
|
||||||
|
require_debian_server "deploy-hermes"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for value_name in hermes_home install_dir uv_bin; do
|
||||||
|
value="${!value_name}"
|
||||||
|
if [[ "${value}" == *$'\n'* ]]; then
|
||||||
|
echo "${value_name} cannot contain a newline." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deploying Hermes agent on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "HERMES" "${install_docker}"
|
||||||
|
|
||||||
|
# Install uv if missing.
|
||||||
|
if [[ ! -x "${uv_bin}" ]]; then
|
||||||
|
mkdir -p "$(dirname "${uv_bin}")"
|
||||||
|
echo "Installing uv..."
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | bash
|
||||||
|
# uv installs to ~/.local/bin; copy/alias into HERMES bin if that path differs.
|
||||||
|
if command -v uv >/dev/null 2>&1 && [[ ! -x "${uv_bin}" ]]; then
|
||||||
|
cp "$(command -v uv)" "${uv_bin}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Hermes via its official installer (git method, managed venv layout).
|
||||||
|
if [[ ! -x "${hermes_home}/hermes-agent/venv/bin/python" ]]; then
|
||||||
|
echo "Installing Hermes agent..."
|
||||||
|
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure the Matrix platform dependencies (E2EE) are present in the venv.
|
||||||
|
if [[ -x "${uv_bin}" && -f "${install_dir}/pyproject.toml" ]]; then
|
||||||
|
cd "${install_dir}"
|
||||||
|
"${uv_bin}" pip install 'mautrix[encryption]' -p venv/bin/python
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Secret env for the gateway (.env under HERMES_HOME, mode 600, never committed).
|
||||||
|
# Pull the hermes secrets from the SOPS-encrypted repo file; env vars override.
|
||||||
|
if ! sops_load_secrets "${REPO_ROOT}/infra/hermes.secret.yaml"; then
|
||||||
|
echo "WARNING: could not load hermes secrets; MATRIX_PASSWORD may be blank." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local secrets_file="${hermes_home}/.env"
|
||||||
|
sudo install -d -m 0700 -o "$(id -u)" -g "$(id -g)" "${hermes_home}"
|
||||||
|
if [[ -f "${secrets_file}" ]]; then
|
||||||
|
sudo cp "${secrets_file}" "${secrets_file}.bak-pre-jeannie"
|
||||||
|
fi
|
||||||
|
sudo tee "${secrets_file}" >/dev/null <<ENV_EOT
|
||||||
|
OPENAI_BASE_URL=${HERMES_OPENAI_BASE_URL:-http://localhost:${LAB_OMNIROUTE_PORT:-20128}/v1}
|
||||||
|
HERMES_CUSTOM_LOCALHOST_${LAB_OMNIROUTE_PORT:-20128}_API_KEY=${HERMES_OMNIROUTE_API_KEY:-}
|
||||||
|
MATRIX_HOMESERVER=${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_USER_ID=${HERMES_MATRIX_USER_ID:-@hermes:matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_PASSWORD=${MATRIX_BOT_PASSWORD:-}
|
||||||
|
MATRIX_ALLOWED_USERS=${HERMES_MATRIX_ALLOWED_USERS:-@jv:matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_E2EE_MODE=required
|
||||||
|
ENV_EOT
|
||||||
|
sudo chmod 600 "${secrets_file}"
|
||||||
|
|
||||||
|
# Create the Matrix bot account (idempotent) if the homeserver is up.
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^matrix-synapse$'; then
|
||||||
|
if [[ -n "${MATRIX_BOT_PASSWORD:-}" ]]; then
|
||||||
|
if ! docker exec matrix-synapse register_new_matrix_user -c /data/homeserver.yaml \
|
||||||
|
-u hermes -p "${MATRIX_BOT_PASSWORD}" --no-admin >/dev/null 2>&1; then
|
||||||
|
echo "Hermes bot account already exists (expected on re-run) or auto-registration skipped." >&2
|
||||||
|
else
|
||||||
|
echo "Hermes Matrix bot account ready."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "MATRIX_BOT_PASSWORD not set; skipping Hermes bot account registration (needed only on fresh install)." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install/start the gateway systemd user service (Hermes manages the unit).
|
||||||
|
"${hermes_home}/hermes-agent/venv/bin/python" "${hermes_home}/hermes-agent/hermes" gateway install
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now hermes-gateway 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Hermes agent deployed. Run 'hermes' to configure the model, or check gateway logs at ${hermes_home}/logs/gateway.log."
|
||||||
|
}
|
||||||
|
|
||||||
deploy_rpi_services() {
|
deploy_rpi_services() {
|
||||||
local mode="${LAB_RPI_SERVICES_DEPLOY:-true}"
|
local mode="${LAB_RPI_SERVICES_DEPLOY:-true}"
|
||||||
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
|
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
|
||||||
|
|
@ -5609,6 +5836,22 @@ spec = {
|
||||||
"title": "Heimdall deploy",
|
"title": "Heimdall deploy",
|
||||||
"command": [jeannie, "__up-task", "heimdall-deploy"],
|
"command": [jeannie, "__up-task", "heimdall-deploy"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "matrix-deploy",
|
||||||
|
"title": "Matrix deploy",
|
||||||
|
"command": [jeannie, "__up-task", "matrix-deploy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omniroute-deploy",
|
||||||
|
"title": "OmniRoute deploy",
|
||||||
|
"command": [jeannie, "__up-task", "omniroute-deploy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hermes-deploy",
|
||||||
|
"title": "Hermes deploy",
|
||||||
|
"needs": ["omniroute-deploy", "matrix-deploy"],
|
||||||
|
"command": [jeannie, "__up-task", "hermes-deploy"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "rpi-services",
|
"id": "rpi-services",
|
||||||
"title": "RPi services",
|
"title": "RPi services",
|
||||||
|
|
@ -5656,6 +5899,15 @@ up_task() {
|
||||||
heimdall-deploy)
|
heimdall-deploy)
|
||||||
deploy_heimdall
|
deploy_heimdall
|
||||||
;;
|
;;
|
||||||
|
matrix-deploy)
|
||||||
|
deploy_matrix
|
||||||
|
;;
|
||||||
|
omniroute-deploy)
|
||||||
|
deploy_omniroute
|
||||||
|
;;
|
||||||
|
hermes-deploy)
|
||||||
|
deploy_hermes
|
||||||
|
;;
|
||||||
rpi-services)
|
rpi-services)
|
||||||
deploy_rpi_services
|
deploy_rpi_services
|
||||||
;;
|
;;
|
||||||
|
|
@ -7579,9 +7831,6 @@ ensure_sops_age_tools() {
|
||||||
if ! command -v age-keygen >/dev/null 2>&1; then
|
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||||
missing_packages+=(age)
|
missing_packages+=(age)
|
||||||
fi
|
fi
|
||||||
if ! command -v sops >/dev/null 2>&1; then
|
|
||||||
missing_packages+=(sops)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ((${#missing_packages[@]} > 0)); then
|
if ((${#missing_packages[@]} > 0)); then
|
||||||
echo "Installing missing secret-management tools: ${missing_packages[*]}"
|
echo "Installing missing secret-management tools: ${missing_packages[*]}"
|
||||||
|
|
@ -7589,10 +7838,26 @@ ensure_sops_age_tools() {
|
||||||
sudo apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
sudo apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# age must be present; it's small and apt-provided on Debian.
|
||||||
if ! command -v age-keygen >/dev/null 2>&1; then
|
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||||
echo "age-keygen is still unavailable after package installation." >&2
|
echo "age-keygen is still unavailable after package installation." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# sops: not packaged on Debian trixie — fall back to the GitHub release binary.
|
||||||
|
if ! command -v sops >/dev/null 2>&1; then
|
||||||
|
echo "sops not found in the apt repos; installing from GitHub release..."
|
||||||
|
local sops_ver sops_url
|
||||||
|
sops_ver="$(curl -fsSL https://api.github.com/repos/getsops/sops/releases/latest | grep -oE '"tag_name":\s*"v[^"]+"' | head -1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')"
|
||||||
|
if [[ -z "${sops_ver}" ]]; then
|
||||||
|
echo "Could not determine the latest SOPS release version." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sops_url="https://github.com/getsops/sops/releases/download/${sops_ver}/sops-v${sops_ver#v}.linux.amd64"
|
||||||
|
curl -fsSL -o "/tmp/sops-${sops_ver}" "${sops_url}" && \
|
||||||
|
sudo install -m 0755 "/tmp/sops-${sops_ver}" /usr/local/bin/sops && \
|
||||||
|
rm -f "/tmp/sops-${sops_ver}"
|
||||||
|
fi
|
||||||
if ! command -v sops >/dev/null 2>&1; then
|
if ! command -v sops >/dev/null 2>&1; then
|
||||||
echo "sops is still unavailable after package installation." >&2
|
echo "sops is still unavailable after package installation." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -7609,6 +7874,56 @@ sops_age_recipient() {
|
||||||
awk -F': ' '/^# public key:/ { print $2; exit }' "${key_file}"
|
awk -F': ' '/^# public key:/ { print $2; exit }' "${key_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Decrypt a SOPS-encrypted YAML of `KEY: value` pairs into the current env.
|
||||||
|
# Usage: sops_load_secrets <repo/path/to/file.secret.yaml>
|
||||||
|
# Requires the age key (sops_age_key_file) to decrypt; fails loudly if missing.
|
||||||
|
sops_load_secrets() {
|
||||||
|
local file="${1:?sops_load_secrets: missing file}"
|
||||||
|
local key_file
|
||||||
|
local out
|
||||||
|
|
||||||
|
if [[ ! -f "${file}" ]]; then
|
||||||
|
echo "Missing encrypted secret file: ${file} (run ./jeannie secrets-init then sops -e)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! sops_available; then
|
||||||
|
echo "sops not installed. Run ./jeannie secrets-init first." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
key_file="$(sops_age_key_file)"
|
||||||
|
if [[ ! -s "${key_file}" ]]; then
|
||||||
|
echo "Missing SOPS age key at ${key_file}. Run ./jeannie secrets-init, or copy the age key to this machine." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(SOPS_AGE_KEY_FILE="${key_file}" sops --decrypt "${file}")" || {
|
||||||
|
echo "SOPS decryption failed for ${file}." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export every bare `KEY: value` line (ignore comments/blank).
|
||||||
|
local line key val syn="0"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*: ]] || continue
|
||||||
|
key="${line%%:*}"
|
||||||
|
val="${line#*: }"
|
||||||
|
# Strip surrounding double quotes, if sops kept them.
|
||||||
|
val="${val%\"}"; val="${val#\"}"
|
||||||
|
export "${key}=${val}"
|
||||||
|
syn="$((syn + 1))"
|
||||||
|
done <<<"${out}"
|
||||||
|
|
||||||
|
if ((syn == 0)); then
|
||||||
|
echo "No KEY: value pairs found in decrypted ${file}." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sops_available() {
|
||||||
|
command -v sops >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
secrets_init() {
|
secrets_init() {
|
||||||
local key_file
|
local key_file
|
||||||
local key_dir
|
local key_dir
|
||||||
|
|
@ -8108,6 +8423,9 @@ Build And Bootstrap
|
||||||
up Deploy the full homelab pipeline.
|
up Deploy the full homelab pipeline.
|
||||||
deploy-gitea Deploy the Debian-hosted Gitea container.
|
deploy-gitea Deploy the Debian-hosted Gitea container.
|
||||||
deploy-heimdall Deploy the Debian-hosted Heimdall dashboard.
|
deploy-heimdall Deploy the Debian-hosted Heimdall dashboard.
|
||||||
|
deploy-matrix Deploy the Matrix homeserver (Synapse + Element + Postgres).
|
||||||
|
deploy-omniroute Deploy the OmniRoute LLM routing gateway.
|
||||||
|
deploy-hermes Deploy the Hermes agent (git-install + venv + gateway service).
|
||||||
bootstrap-gitea-repo Ensure the Gitea repo and SSH key wiring exist.
|
bootstrap-gitea-repo Ensure the Gitea repo and SSH key wiring exist.
|
||||||
rpi-services Deploy Pi-hole, Unbound, and Uptime Kuma on RPi4.
|
rpi-services Deploy Pi-hole, Unbound, and Uptime Kuma on RPi4.
|
||||||
ollama-setup Install/configure Ollama on the Debian host.
|
ollama-setup Install/configure Ollama on the Debian host.
|
||||||
|
|
@ -8395,6 +8713,18 @@ case "${1:-}" in
|
||||||
record_change_journal "deploy-heimdall" "$@"
|
record_change_journal "deploy-heimdall" "$@"
|
||||||
deploy_heimdall
|
deploy_heimdall
|
||||||
;;
|
;;
|
||||||
|
deploy-matrix)
|
||||||
|
record_change_journal "deploy-matrix" "$@"
|
||||||
|
deploy_matrix
|
||||||
|
;;
|
||||||
|
deploy-omniroute)
|
||||||
|
record_change_journal "deploy-omniroute" "$@"
|
||||||
|
deploy_omniroute
|
||||||
|
;;
|
||||||
|
deploy-hermes)
|
||||||
|
record_change_journal "deploy-hermes" "$@"
|
||||||
|
deploy_hermes
|
||||||
|
;;
|
||||||
rpi-services)
|
rpi-services)
|
||||||
record_change_journal "rpi-services" "$@"
|
record_change_journal "rpi-services" "$@"
|
||||||
deploy_rpi_services
|
deploy_rpi_services
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue