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 ## 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 ## 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. ## 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 ## The time before the session cookie expires and the session is destroyed if remember me IS NOT selected by the
## user. ## 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 ## 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 ## 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. ## 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. ## Cookie Session Domain default 'name' value.
# name: 'authelia_session' # 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 ## 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. ## syntax the lifespans can be customized per-client.
# lifespans: ## Дефолты: access_token '1 hour', authorize_code '1 minute', id_token '1 hour', refresh_token '90 minutes'.
## 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. ## Outline раз в час (после каждого auth.info, если lastSignedInAt старше часа) запускает
# access_token: '1 hour' ## ValidateSSOAccessTask: обновляет access token по refresh token и дёргает userinfo. Если обновить
# authorize_code: '1 minute' ## не удалось — ротирует jwtSecret пользователя и убивает ВСЕ свои сессии, то есть выкидывает на
# id_token: '1 hour' ## повторный вход. С дефолтным refresh token в 90 минут это происходит почти при каждом визите.
# refresh_token: '90 minutes' lifespans:
custom:
outline:
access_token: "1 hour"
refresh_token: "3 months"
## Cross-Origin Resource Sharing (CORS) settings. ## Cross-Origin Resource Sharing (CORS) settings.
# cors: # cors:
@@ -1486,20 +1492,25 @@ identity_providers:
# audience: [] # audience: []
## Scopes this client is allowed to request. ## Scopes this client is allowed to request.
## offline_access нужен, чтобы Authelia выдала refresh token: без него Outline не может
## продлить access token и убивает свою сессию (см. кастомный lifespan 'outline' выше).
scopes: scopes:
- "openid" - "openid"
- "profile" - "profile"
- "email" - "email"
- "offline_access"
## Grant Types configures which grants this client can obtain. ## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing. ## It's not recommended to define this unless you know what you're doing.
# grant_types: grant_types:
# - 'authorization_code' - "authorization_code"
- "refresh_token"
## Response Types configures which responses this client can be sent. ## Response Types configures which responses this client can be sent.
## It's not recommended to define this unless you know what you're doing. ## It's not recommended to define this unless you know what you're doing.
# response_types: ## Задан явно: без этого Authelia ругается на scope 'offline_access'.
# - 'code' response_types:
- "code"
## Response Modes configures which response modes this client supports. ## Response Modes configures which response modes this client supports.
# response_modes: # 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 ## 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. ## utilization. Custom lifespans are reusable similar to authorization policies.
# lifespan: '' lifespan: "outline"
## The consent mode controls how consent is obtained. ## 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 ## 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. ## 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. ## Requires the use of Pushed Authorization Requests for this client when set to true.
# require_pushed_authorization_requests: false # require_pushed_authorization_requests: false
+12 -20
View File
@@ -16,16 +16,21 @@
thinking-mode gating, корректные reasoning-поля для v4-моделей issue #3139): thinking-mode gating, корректные reasoning-поля для v4-моделей issue #3139):
не нужны ни custom_provider_config, ни base_url (дефолт api.deepseek.com). не нужны ни custom_provider_config, ни base_url (дефолт api.deepseek.com).
Роутинг декларативно через governance.routing_rules (требует включённого Роутинг правилами больше не нужен: клиенты шлют имя модели с префиксом
config_store). tududi ходит на drop-in эндпоинт /openai и шлёт ЗАХАРДКОЖЕННОЕ провайдера (tududi deepseek/deepseek-v4-pro на drop-in эндпоинт /openai),
имя модели gpt-4o-mini «голым». Правило ловит его по cel и подменяет на Bifrost резолвит провайдера сам. Раньше здесь жило правило, подменявшее
реальную модель deepseek/deepseek-v4-pro. Фолбэка нет единственный провайдер. захардкоженное у tududi gpt-4o-mini; с tududi 1.3.1 модель задаётся переменной.
Чтобы сменить бэкенд-модель, правим ТОЛЬКО этот файл, конфиг tududi не трогаем.
routing_rules оставлен ПУСТЫМ МАССИВОМ, а не удалён: при source_of_truth=config.json
авторитетно перезаписываются только присутствующие секции, а отсутствующие
оставляют строки в config.db нетронутыми удали ключ, и старое правило
продолжило бы жить в БД (config.go, isGovernanceSectionPresent).
Эти Jinja-комментарии в готовый config.json не попадают (на выходе чистый JSON). #} Эти Jinja-комментарии в готовый config.json не попадают (на выходе чистый JSON). #}
{ {
"$schema": "https://www.getbifrost.ai/schema", "$schema": "https://www.getbifrost.ai/schema",
"encryption_key": {{ bifrost_encryption_key | to_json }}, "encryption_key": {{ bifrost_encryption_key | to_json }},
"source_of_truth": "config.json",
"client": { "client": {
"enable_logging": true "enable_logging": true
}, },
@@ -40,8 +45,7 @@
"config_store": { "config_store": {
"enabled": true, "enabled": true,
"type": "sqlite", "type": "sqlite",
"config": { "path": "/app/store/config.db" }, "config": { "path": "/app/store/config.db" }
"source_of_truth": "config.json"
}, },
"logs_store": { "logs_store": {
"enabled": true, "enabled": true,
@@ -49,18 +53,6 @@
"config": { "path": "/app/store/logs.db" } "config": { "path": "/app/store/logs.db" }
}, },
"governance": { "governance": {
"routing_rules": [ "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
}
]
} }
} }
+5 -1
View File
@@ -3,6 +3,10 @@
# Клиенты ходят в него по имени bifrost_app:8080 в общей web_proxy_network и про # Клиенты ходят в него по имени bifrost_app:8080 в общей web_proxy_network и про
# провайдера/модель ничего не знают — роутинг решает config.json. # провайдера/модель ничего не знают — роутинг решает config.json.
# See versions: https://hub.docker.com/r/maximhq/bifrost/tags # 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-порт НЕ пробрасываем: # В контейнере Bifrost слушает 8080 (дефолт). Наружу host-порт НЕ пробрасываем:
# веб-UI (Dashboard + Logs) и API доступны только через Caddy # веб-UI (Dashboard + Logs) и API доступны только через Caddy
@@ -15,7 +19,7 @@
services: services:
bifrost_app: bifrost_app:
image: maximhq/bifrost:v1.6.3 # проверь актуальный стабильный тег в releases image: maximhq/bifrost:v1.6.8 # проверь актуальный стабильный тег в releases
container_name: bifrost_app container_name: bifrost_app
restart: unless-stopped restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" 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 RUN apk add --no-cache jq
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
goaccess_processor: goaccess_processor:
build: . build: .
image: local/goaccess-jq:1.10.2 image: local/goaccess-jq:1.11
container_name: goaccess_processor container_name: goaccess_processor
restart: unless-stopped restart: unless-stopped
init: true init: true
+1 -1
View File
@@ -3,7 +3,7 @@
services: services:
miniflux_app: miniflux_app:
image: miniflux/miniflux:2.3.2 image: miniflux/miniflux:2.3.3
container_name: miniflux_app container_name: miniflux_app
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
depends_on: depends_on:
+3 -1
View File
@@ -35,7 +35,9 @@ services:
OIDC_USERINFO_URI: 'https://auth.vakhrushev.me/api/oidc/userinfo' OIDC_USERINFO_URI: 'https://auth.vakhrushev.me/api/oidc/userinfo'
OIDC_LOGOUT_URI: 'https://auth.vakhrushev.me/logout' OIDC_LOGOUT_URI: 'https://auth.vakhrushev.me/logout'
OIDC_USERNAME_CLAIM: 'email' 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' OIDC_DISPLAY_NAME: 'Authelia'
SMTP_HOST: '{{ postbox_host }}' SMTP_HOST: '{{ postbox_host }}'
+11 -7
View File
@@ -12,7 +12,7 @@
services: services:
tududi_app: tududi_app:
image: chrisvel/tududi:1.2.4 image: chrisvel/tududi:1.3.1
container_name: tududi_app container_name: tududi_app
restart: unless-stopped restart: unless-stopped
networks: networks:
@@ -31,7 +31,9 @@ services:
- BASE_URL=https://tududi.vakhrushev.me - BASE_URL=https://tududi.vakhrushev.me
- TUDUDI_ALLOWED_ORIGINS=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 }} - TUDUDI_SESSION_SECRET={{ tududi_session_secret }}
# Чистый OIDC: сид-админ не создаётся (TUDUDI_USER_EMAIL/PASSWORD не заданы), # Чистый OIDC: сид-админ не создаётся (TUDUDI_USER_EMAIL/PASSWORD не заданы),
@@ -67,11 +69,13 @@ services:
- FF_ENABLE_MCP=true - FF_ENABLE_MCP=true
# AI-фичи через Bifrost (drop-in OpenAI-эндпоинт в общей сети — НЕ localhost, # AI-фичи через Bifrost (drop-in OpenAI-эндпоинт в общей сети — НЕ localhost,
# это сам контейнер). Модель у tududi захардкожена как gpt-4o-mini; Bifrost # это сам контейнер). С 1.3.1 модель задаётся переменной, а не захардкожена,
# по governance-правилу роутит её на deepseek-v4-pro. openai-node сам # поэтому шлём deepseek/deepseek-v4-pro сразу: Bifrost резолвит провайдера по
# подхватывает OPENAI_BASE_URL. # префиксу, и подменять модель governance-правилом больше не нужно.
- OPENAI_API_KEY={{ tududi_openai_api_key }} # Пара LLM_* имеет приоритет над устаревшей OPENAI_*, значения те же.
- OPENAI_BASE_URL=http://bifrost_app:8080/openai - LLM_API_KEY={{ tududi_openai_api_key }}
- LLM_BASE_URL=http://bifrost_app:8080/openai
- LLM_MODEL=deepseek/deepseek-v4-pro
networks: networks:
web_proxy_network: web_proxy_network:
+1 -1
View File
@@ -4,7 +4,7 @@
services: services:
tuwunel_app: tuwunel_app:
image: jevolk/tuwunel:v1.8.1 image: jevolk/tuwunel:v1.8.3
container_name: tuwunel_app container_name: tuwunel_app
restart: unless-stopped restart: unless-stopped
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" 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_install_path: '{{ (bin_prefix, "eget") | path_join }}'
eget_packages_to: "{{ bin_prefix }}" eget_packages_to: "{{ bin_prefix }}"
eget_packages: eget_packages:
- {repo: "rclone/rclone", tag: "v1.73.4", asset: "zip"} - {repo: "rclone/rclone", tag: "v1.75.0", asset: "zip"}
- {repo: "restic/restic", tag: "v0.18.1"} - {repo: "restic/restic", tag: "v0.19.1"}
- {repo: "creativeprojects/resticprofile", tag: "v0.32.0", asset: "^no_self_update"} - {repo: "creativeprojects/resticprofile", tag: "v0.33.1", asset: "^no_self_update"}
- {repo: "aristocratos/btop", tag: "v1.4.6"} - {repo: "aristocratos/btop", tag: "v1.4.7"}
- {repo: "gobackup/gobackup", tag: "v2.17.0"} - {repo: "gobackup/gobackup", tag: "v3.1.1"}
- {repo: "go-task/task", tag: "v3.48.0", asset: "tar.gz"} - {repo: "go-task/task", tag: "v3.52.0", asset: "tar.gz"}
- {repo: "bootandy/dust", tag: "v1.2.4", asset: "gnu"} - {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""" """Invoke tasks — замена Taskfile.yml"""
import os import os
import re
import subprocess import subprocess
import sys import sys
@@ -11,7 +12,7 @@ from invoke.tasks import task
HOSTS_FILE = "production.yml" HOSTS_FILE = "production.yml"
VARS_FILE = "group_vars/all/main.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") 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]: def _rest_args() -> list[str]:
"""Возвращает аргументы после '--' из sys.argv""" """Возвращает аргументы после '--' из sys.argv"""
try: try:
@@ -247,13 +259,15 @@ def login_as_app(ctx: Context, app: str) -> None:
@task @task
def authelia_cli(ctx: Context, args: str = "") -> None: def authelia_cli(ctx: Context, args: str = "") -> None:
"""Запустить authelia CLI в docker""" """Запустить authelia CLI в docker"""
ctx.run(f"{AUTHELIA_DOCKER} {args}") ctx.run(f"{_authelia_docker()} {args}")
@task @task
def authelia_validate_config(ctx: Context) -> None: def authelia_validate_config(ctx: Context) -> None:
"""Отрендерить конфиг authelia из шаблона и проверить его""" """Отрендерить конфиг authelia из шаблона и проверить его"""
dest = "temp/configuration.yml" dest = "temp/configuration.yml"
# temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт.
os.makedirs(os.path.dirname(dest), exist_ok=True)
try: try:
ctx.run( ctx.run(
"uv run ansible localhost" "uv run ansible localhost"
@@ -262,7 +276,7 @@ def authelia_validate_config(ctx: Context) -> None:
" --extra-vars @group_vars/all/secrets.yml" " --extra-vars @group_vars/all/secrets.yml"
" --extra-vars @files/authelia/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: finally:
ctx.run(f"rm -f {dest}", warn=True) ctx.run(f"rm -f {dest}", warn=True)
@@ -270,14 +284,16 @@ def authelia_validate_config(ctx: Context) -> None:
@task @task
def authelia_gen_random_string(ctx: Context, length: int = 10) -> None: def authelia_gen_random_string(ctx: Context, length: int = 10) -> None:
"""Сгенерировать случайную alphanumeric-строку""" """Сгенерировать случайную alphanumeric-строку"""
ctx.run(f"{AUTHELIA_DOCKER} crypto rand --length {length} --charset alphanumeric") ctx.run(
f"{_authelia_docker()} crypto rand --length {length} --charset alphanumeric"
)
@task @task
def authelia_gen_secret_and_hash(ctx: Context, length: int = 72) -> None: def authelia_gen_secret_and_hash(ctx: Context, length: int = 72) -> None:
"""Сгенерировать случайный секрет и его pbkdf2-sha512 хэш""" """Сгенерировать случайный секрет и его pbkdf2-sha512 хэш"""
ctx.run( 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" f" --variant sha512 --random --random.length {length} --random.charset rfc3986"
) )