Compare commits

..
12 Commits
Author SHA1 Message Date
av aeddc1f168 tasks.py: версия authelia CLI берётся из docker-compose
Linting / Ansible Lint (push) Canceled after 0s
Linting / YAML Lint (push) Canceled after 0s
- константа AUTHELIA_DOCKER была прибита к 4.39.4, а сервис уже на 4.39.20 — конфиг валидировался чужим бинарником
- authelia-validate-config сам создаёт temp/: директория в .gitignore, на свежем клоне задача падала
2026-08-06 10:39:02 +03:00
av 045b2426b3 Authelia: увеличено время жизни сессии, Outline получает refresh token
- session cookies: inactivity 1 week, expiration 1 month, remember_me 3 months вместо дефолтных 5 минут / 1 часа / 1 месяца
- клиенту outline-app выданы scope offline_access и grant refresh_token с кастомным lifespan (refresh token 3 месяца): без них ValidateSSOAccessTask не мог продлить access token и ротацией jwtSecret убивал все сессии Outline
- consent_mode переведён в pre-configured, чтобы при повторном входе не показывался экран согласия
2026-08-06 10:38:48 +03:00
av 58519f1413 eget: обновлены версии инструментов
- rclone 1.75.0, restic 0.19.1, resticprofile 0.33.1, btop 1.4.7,
  gobackup 3.1.1, task 3.52.0, zellij 0.44.3; dust уже актуален
- gobackup 3.x переименовал ключи passphrase и MSSQL, но наши конфиги
  используют только local-хранилище и sqlite
2026-08-06 10:19:14 +03:00
av 1fcc189afc GoAccess: update to 1.11
- базовый образ и тег локальной сборки с jq подняты одной парой
2026-08-06 10:02:27 +03:00
av 16ab872ba4 Miniflux: update to 2.3.3 2026-08-06 10:02:26 +03:00
av 85ebeb4197 AI-модель tududi задаётся явно, правило роутинга в bifrost убрано
- с tududi 1.3.1 модель берётся из LLM_MODEL, а не захардкожена — шлём
  deepseek/deepseek-v4-pro, Bifrost резолвит провайдера по префиксу
- ключ и base URL переведены на пару LLM_*, приоритетную над OPENAI_*
- routing_rules оставлен пустым массивом, а не удалён: при
  source_of_truth=config.json отсутствующая секция не трогает строки в config.db
2026-08-06 10:00:47 +03:00
av a09f3c629e tududi: TUDUDI_TRUST_PROXY переведён на 1
- `true` доверял всей цепочке X-Forwarded-For, что позволяло подделать IP
  клиента; у нас ровно один хоп — Caddy
- tududi с 1.3.x сам приводит значение к 1 и пишет предупреждение на каждый старт
2026-08-06 10:00:34 +03:00
av 90bdb4a3ea Tududi: update to 1.3.1 2026-08-06 09:43:28 +03:00
av 88a53076e9 bifrost: в compose записано, как проверять обновления образа
- тег образа соответствует релизу transports/vX.Y.Z, а теги 2.1.x рядом на
  Docker Hub — это helm-chart
- конфиг после апдейта валидировать по config.schema.json: неизвестные ключи
  Bifrost игнорирует молча, в логах ошибки не будет
2026-08-06 09:20:50 +03:00
av 4b6d5cdc85 bifrost: исправлено расположение source_of_truth в конфиге
- ключ лежал внутри config_store, где Bifrost его молча игнорировал — шлюз
  работал в дефолтном режиме split вместо заявленного config.json
- поднят на верхний уровень, теперь providers и governance из config.json
  авторитетно перезаписывают строки в config.db на старте
2026-08-06 09:18:15 +03:00
av b794eea87a Bifrost: update to 1.6.8 2026-08-06 09:17:54 +03:00
av 9acf687530 Tuwunel: update to 1.8.3 2026-08-06 09:17:51 +03:00
11 changed files with 93 additions and 62 deletions
+30 -17
View File
@@ -875,16 +875,18 @@ session:
## The inactivity time before the session is reset. If expiration is set to 1h, and this is set to 5m, if the user
## does not select the remember me option their session will get destroyed after 1h, or after 5m since the last
## time Authelia detected user activity.
# inactivity: '5 minutes'
## Дефолты (5 минут / 1 час) для личного сервера слишком агрессивны: любая пауза в работе
## заставляет логиниться заново. Держим сессию живой неделю простоя.
inactivity: "1 week"
## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the
## user.
# expiration: '1 hour'
expiration: "1 month"
## The time before the cookie expires and the session is destroyed if remember me IS selected by the user. Setting
## this value to -1 disables remember me for this session cookie domain. If allowed and the user uses the remember
## me checkbox this overrides the expiration option and disables the inactivity option.
# remember_me: '1 month'
remember_me: "3 months"
## Cookie Session Domain default 'name' value.
# name: 'authelia_session'
@@ -1370,13 +1372,17 @@ identity_providers:
## The lifespans configure the expiration for these token types in the duration common syntax. In addition to this
## syntax the lifespans can be customized per-client.
# lifespans:
## Configures the default/fallback lifespan for given token types. This behaviour applies to all clients and all
## grant types but you can override this behaviour using the custom lifespans.
# access_token: '1 hour'
# authorize_code: '1 minute'
# id_token: '1 hour'
# refresh_token: '90 minutes'
## Дефолты: access_token '1 hour', authorize_code '1 minute', id_token '1 hour', refresh_token '90 minutes'.
##
## Outline раз в час (после каждого auth.info, если lastSignedInAt старше часа) запускает
## ValidateSSOAccessTask: обновляет access token по refresh token и дёргает userinfo. Если обновить
## не удалось — ротирует jwtSecret пользователя и убивает ВСЕ свои сессии, то есть выкидывает на
## повторный вход. С дефолтным refresh token в 90 минут это происходит почти при каждом визите.
lifespans:
custom:
outline:
access_token: "1 hour"
refresh_token: "3 months"
## Cross-Origin Resource Sharing (CORS) settings.
# cors:
@@ -1486,20 +1492,25 @@ identity_providers:
# audience: []
## Scopes this client is allowed to request.
## offline_access нужен, чтобы Authelia выдала refresh token: без него Outline не может
## продлить access token и убивает свою сессию (см. кастомный lifespan 'outline' выше).
scopes:
- "openid"
- "profile"
- "email"
- "offline_access"
## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing.
# grant_types:
# - 'authorization_code'
grant_types:
- "authorization_code"
- "refresh_token"
## Response Types configures which responses this client can be sent.
## It's not recommended to define this unless you know what you're doing.
# response_types:
# - 'code'
## Задан явно: без этого Authelia ругается на scope 'offline_access'.
response_types:
- "code"
## Response Modes configures which response modes this client supports.
# response_modes:
@@ -1512,14 +1523,16 @@ identity_providers:
## The custom lifespan name to use for this client. This must be configured independent of the client before
## utilization. Custom lifespans are reusable similar to authorization policies.
# lifespan: ''
lifespan: "outline"
## The consent mode controls how consent is obtained.
# consent_mode: 'auto'
## 'pre-configured' — согласие запоминается на pre_configured_consent_duration, поэтому при
## повторном входе экран согласия не показывается и редирект проходит незаметно.
consent_mode: "pre-configured"
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured' in the duration common syntax.
# pre_configured_consent_duration: '1 week'
pre_configured_consent_duration: "3 months"
## Requires the use of Pushed Authorization Requests for this client when set to true.
# require_pushed_authorization_requests: false
+12 -20
View File
@@ -16,16 +16,21 @@
thinking-mode gating, корректные reasoning-поля для v4-моделей issue #3139):
не нужны ни custom_provider_config, ни base_url (дефолт api.deepseek.com).
Роутинг декларативно через governance.routing_rules (требует включённого
config_store). tududi ходит на drop-in эндпоинт /openai и шлёт ЗАХАРДКОЖЕННОЕ
имя модели gpt-4o-mini «голым». Правило ловит его по cel и подменяет на
реальную модель deepseek/deepseek-v4-pro. Фолбэка нет единственный провайдер.
Чтобы сменить бэкенд-модель, правим ТОЛЬКО этот файл, конфиг tududi не трогаем.
Роутинг правилами больше не нужен: клиенты шлют имя модели с префиксом
провайдера (tududi deepseek/deepseek-v4-pro на drop-in эндпоинт /openai),
Bifrost резолвит провайдера сам. Раньше здесь жило правило, подменявшее
захардкоженное у tududi gpt-4o-mini; с tududi 1.3.1 модель задаётся переменной.
routing_rules оставлен ПУСТЫМ МАССИВОМ, а не удалён: при source_of_truth=config.json
авторитетно перезаписываются только присутствующие секции, а отсутствующие
оставляют строки в config.db нетронутыми удали ключ, и старое правило
продолжило бы жить в БД (config.go, isGovernanceSectionPresent).
Эти Jinja-комментарии в готовый config.json не попадают (на выходе чистый JSON). #}
{
"$schema": "https://www.getbifrost.ai/schema",
"encryption_key": {{ bifrost_encryption_key | to_json }},
"source_of_truth": "config.json",
"client": {
"enable_logging": true
},
@@ -40,8 +45,7 @@
"config_store": {
"enabled": true,
"type": "sqlite",
"config": { "path": "/app/store/config.db" },
"source_of_truth": "config.json"
"config": { "path": "/app/store/config.db" }
},
"logs_store": {
"enabled": true,
@@ -49,18 +53,6 @@
"config": { "path": "/app/store/logs.db" }
},
"governance": {
"routing_rules": [
{
"id": "tududi-gpt4omini-to-deepseek-pro",
"name": "Tududi gpt-4o-mini -> DeepSeek v4 Pro",
"enabled": true,
"cel_expression": "model == 'gpt-4o-mini'",
"targets": [
{ "provider": "deepseek", "model": "deepseek-v4-pro", "weight": 1 }
],
"scope": "global",
"priority": 0
}
]
"routing_rules": []
}
}
+5 -1
View File
@@ -3,6 +3,10 @@
# Клиенты ходят в него по имени bifrost_app:8080 в общей web_proxy_network и про
# провайдера/модель ничего не знают — роутинг решает config.json.
# See versions: https://hub.docker.com/r/maximhq/bifrost/tags
# Тег образа = GitHub-релиз transports/vX.Y.Z (там же changelog и миграции БД);
# теги 2.1.x в том же списке — это helm-chart, не образ. Конфиг после апдейта
# валидировать по transports/config.schema.json из нужного тега: Bifrost на Go
# молча игнорирует ключ, положенный не в ту секцию, в логах этого не видно.
#
# В контейнере Bifrost слушает 8080 (дефолт). Наружу host-порт НЕ пробрасываем:
# веб-UI (Dashboard + Logs) и API доступны только через Caddy
@@ -15,7 +19,7 @@
services:
bifrost_app:
image: maximhq/bifrost:v1.6.3 # проверь актуальный стабильный тег в releases
image: maximhq/bifrost:v1.6.8 # проверь актуальный стабильный тег в releases
container_name: bifrost_app
restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM allinurl/goaccess:1.10.2
FROM allinurl/goaccess:1.11
RUN apk add --no-cache jq
+1 -1
View File
@@ -1,7 +1,7 @@
services:
goaccess_processor:
build: .
image: local/goaccess-jq:1.10.2
image: local/goaccess-jq:1.11
container_name: goaccess_processor
restart: unless-stopped
init: true
+1 -1
View File
@@ -3,7 +3,7 @@
services:
miniflux_app:
image: miniflux/miniflux:2.3.2
image: miniflux/miniflux:2.3.3
container_name: miniflux_app
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on:
+3 -1
View File
@@ -35,7 +35,9 @@ services:
OIDC_USERINFO_URI: 'https://auth.vakhrushev.me/api/oidc/userinfo'
OIDC_LOGOUT_URI: 'https://auth.vakhrushev.me/logout'
OIDC_USERNAME_CLAIM: 'email'
OIDC_SCOPES: 'openid profile email'
# offline_access — чтобы Authelia выдала refresh token. Без него Outline не может продлить
# access token, ValidateSSOAccessTask признаёт вход невалидным и сбрасывает все сессии.
OIDC_SCOPES: 'openid profile email offline_access'
OIDC_DISPLAY_NAME: 'Authelia'
SMTP_HOST: '{{ postbox_host }}'
+11 -7
View File
@@ -12,7 +12,7 @@
services:
tududi_app:
image: chrisvel/tududi:1.2.4
image: chrisvel/tududi:1.3.1
container_name: tududi_app
restart: unless-stopped
networks:
@@ -31,7 +31,9 @@ services:
- BASE_URL=https://tududi.vakhrushev.me
- TUDUDI_ALLOWED_ORIGINS=https://tududi.vakhrushev.me
- TUDUDI_TRUST_PROXY=true
# Ровно один хоп (Caddy). `true` доверяет всей цепочке X-Forwarded-For и
# позволяет подделать IP клиента; tududi с 1.3.x сам приводит его к 1 и ругается.
- TUDUDI_TRUST_PROXY=1
- TUDUDI_SESSION_SECRET={{ tududi_session_secret }}
# Чистый OIDC: сид-админ не создаётся (TUDUDI_USER_EMAIL/PASSWORD не заданы),
@@ -67,11 +69,13 @@ services:
- FF_ENABLE_MCP=true
# AI-фичи через Bifrost (drop-in OpenAI-эндпоинт в общей сети — НЕ localhost,
# это сам контейнер). Модель у tududi захардкожена как gpt-4o-mini; Bifrost
# по governance-правилу роутит её на deepseek-v4-pro. openai-node сам
# подхватывает OPENAI_BASE_URL.
- OPENAI_API_KEY={{ tududi_openai_api_key }}
- OPENAI_BASE_URL=http://bifrost_app:8080/openai
# это сам контейнер). С 1.3.1 модель задаётся переменной, а не захардкожена,
# поэтому шлём deepseek/deepseek-v4-pro сразу: Bifrost резолвит провайдера по
# префиксу, и подменять модель governance-правилом больше не нужно.
# Пара LLM_* имеет приоритет над устаревшей OPENAI_*, значения те же.
- LLM_API_KEY={{ tududi_openai_api_key }}
- LLM_BASE_URL=http://bifrost_app:8080/openai
- LLM_MODEL=deepseek/deepseek-v4-pro
networks:
web_proxy_network:
+1 -1
View File
@@ -4,7 +4,7 @@
services:
tuwunel_app:
image: jevolk/tuwunel:v1.8.1
image: jevolk/tuwunel:v1.8.3
container_name: tuwunel_app
restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
+7 -7
View File
@@ -12,11 +12,11 @@
eget_install_path: '{{ (bin_prefix, "eget") | path_join }}'
eget_packages_to: "{{ bin_prefix }}"
eget_packages:
- {repo: "rclone/rclone", tag: "v1.73.4", asset: "zip"}
- {repo: "restic/restic", tag: "v0.18.1"}
- {repo: "creativeprojects/resticprofile", tag: "v0.32.0", asset: "^no_self_update"}
- {repo: "aristocratos/btop", tag: "v1.4.6"}
- {repo: "gobackup/gobackup", tag: "v2.17.0"}
- {repo: "go-task/task", tag: "v3.48.0", asset: "tar.gz"}
- {repo: "rclone/rclone", tag: "v1.75.0", asset: "zip"}
- {repo: "restic/restic", tag: "v0.19.1"}
- {repo: "creativeprojects/resticprofile", tag: "v0.33.1", asset: "^no_self_update"}
- {repo: "aristocratos/btop", tag: "v1.4.7"}
- {repo: "gobackup/gobackup", tag: "v3.1.1"}
- {repo: "go-task/task", tag: "v3.52.0", asset: "tar.gz"}
- {repo: "bootandy/dust", tag: "v1.2.4", asset: "gnu"}
- {repo: "zellij-org/zellij", tag: "v0.43.1", asset: "no-web"}
- {repo: "zellij-org/zellij", tag: "v0.44.3", asset: "no-web"}
+21 -5
View File
@@ -2,6 +2,7 @@
"""Invoke tasks — замена Taskfile.yml"""
import os
import re
import subprocess
import sys
@@ -11,7 +12,7 @@ from invoke.tasks import task
HOSTS_FILE = "production.yml"
VARS_FILE = "group_vars/all/main.yml"
AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia"
AUTHELIA_COMPOSE_FILE = "files/authelia/docker-compose.template.yml"
# --- Базовый набор: единый интерфейс команд во всех репозиториях ---
@@ -33,6 +34,17 @@ def _remote_host() -> str:
return _yq(".ungrouped.hosts.server.ansible_host")
def _authelia_docker() -> str:
"""Команда запуска authelia CLI на том же образе, что и задеплоенный сервис"""
pattern = re.compile(r"""image:\s*["']?(\S*authelia/authelia:[^"'\s]+)""")
with open(AUTHELIA_COMPOSE_FILE, encoding="utf-8") as compose:
for line in compose:
match = pattern.search(line)
if match:
return f"docker run --rm -v $PWD:/data {match.group(1)} authelia"
raise Exit(f"Образ authelia не найден в {AUTHELIA_COMPOSE_FILE}", code=1)
def _rest_args() -> list[str]:
"""Возвращает аргументы после '--' из sys.argv"""
try:
@@ -247,13 +259,15 @@ def login_as_app(ctx: Context, app: str) -> None:
@task
def authelia_cli(ctx: Context, args: str = "") -> None:
"""Запустить authelia CLI в docker"""
ctx.run(f"{AUTHELIA_DOCKER} {args}")
ctx.run(f"{_authelia_docker()} {args}")
@task
def authelia_validate_config(ctx: Context) -> None:
"""Отрендерить конфиг authelia из шаблона и проверить его"""
dest = "temp/configuration.yml"
# temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт.
os.makedirs(os.path.dirname(dest), exist_ok=True)
try:
ctx.run(
"uv run ansible localhost"
@@ -262,7 +276,7 @@ def authelia_validate_config(ctx: Context) -> None:
" --extra-vars @group_vars/all/secrets.yml"
" --extra-vars @files/authelia/secrets.yml"
)
ctx.run(f"{AUTHELIA_DOCKER} validate-config --config /data/{dest}")
ctx.run(f"{_authelia_docker()} validate-config --config /data/{dest}")
finally:
ctx.run(f"rm -f {dest}", warn=True)
@@ -270,14 +284,16 @@ def authelia_validate_config(ctx: Context) -> None:
@task
def authelia_gen_random_string(ctx: Context, length: int = 10) -> None:
"""Сгенерировать случайную alphanumeric-строку"""
ctx.run(f"{AUTHELIA_DOCKER} crypto rand --length {length} --charset alphanumeric")
ctx.run(
f"{_authelia_docker()} crypto rand --length {length} --charset alphanumeric"
)
@task
def authelia_gen_secret_and_hash(ctx: Context, length: int = 72) -> None:
"""Сгенерировать случайный секрет и его pbkdf2-sha512 хэш"""
ctx.run(
f"{AUTHELIA_DOCKER} crypto hash generate pbkdf2"
f"{_authelia_docker()} crypto hash generate pbkdf2"
f" --variant sha512 --random --random.length {length} --random.charset rfc3986"
)