Add docker admin bootstrap for clean release

This commit is contained in:
2026-03-13 11:41:44 +03:00
parent 2aea70a0f6
commit 152f19eba2
12 changed files with 254 additions and 25 deletions

View File

@@ -1,7 +1,7 @@
# Offline / air-gapped compose profile for enterprise clean release.
BACKEND_IMAGE=ss-tools-backend:v1.0.0-rc2
FRONTEND_IMAGE=ss-tools-frontend:v1.0.0-rc2
BACKEND_IMAGE=ss-tools-backend:v1.0.0-rc2-docker
FRONTEND_IMAGE=ss-tools-frontend:v1.0.0-rc2-docker
POSTGRES_IMAGE=postgres:16-alpine
POSTGRES_DB=ss_tools
@@ -17,5 +17,11 @@ TASK_LOG_LEVEL=INFO
STORAGE_ROOT=./storage
# Initial admin bootstrap. Set to true only for the first startup in a new environment.
INITIAL_ADMIN_CREATE=false
INITIAL_ADMIN_USERNAME=admin
INITIAL_ADMIN_PASSWORD=change-me
INITIAL_ADMIN_EMAIL=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=

View File

@@ -250,21 +250,32 @@ cd /home/busya/dev/ss-tools
```bash
# 1. Собрать образы в подключённом контуре
./scripts/build_offline_docker_bundle.sh v1.0.0-rc2
./scripts/build_offline_docker_bundle.sh v1.0.0-rc2-docker
# 2. Передать dist/docker/* в изолированный контур
# 3. Импортировать образы локально
docker load -i dist/docker/backend.v1.0.0-rc2.tar
docker load -i dist/docker/frontend.v1.0.0-rc2.tar
docker load -i dist/docker/postgres.v1.0.0-rc2.tar
docker load -i dist/docker/backend.v1.0.0-rc2-docker.tar
docker load -i dist/docker/frontend.v1.0.0-rc2-docker.tar
docker load -i dist/docker/postgres.v1.0.0-rc2-docker.tar
# 4. Подготовить env из шаблона
cp dist/docker/.env.enterprise-clean.example .env.enterprise-clean
# 4a. Для первого запуска задать bootstrap администратора
# INITIAL_ADMIN_CREATE=true
# INITIAL_ADMIN_USERNAME=<org-admin-login>
# INITIAL_ADMIN_PASSWORD=<temporary-strong-secret>
# 5. Запустить только локальные образы
docker compose --env-file .env.enterprise-clean -f dist/docker/docker-compose.enterprise-clean.yml up -d
```
Bootstrap администратора выполняется entrypoint-скриптом внутри backend container:
- если `INITIAL_ADMIN_CREATE=true`, контейнер вызывает [`create_admin.py`](backend/src/scripts/create_admin.py) перед стартом API;
- если администратор уже существует, учётная запись не меняется;
- теги в [`.env.enterprise-clean.example`](.env.enterprise-clean.example) должны совпадать с фактически загруженными образами `ss-tools-backend:v1.0.0-rc2-docker` и `ss-tools-frontend:v1.0.0-rc2-docker`;
- после первого входа пароль должен быть ротирован, а `INITIAL_ADMIN_CREATE` возвращён в `false`.
Ограничения для production-grade offline release:
- build не должен тянуть зависимости в изолированном контуре;
- все base images должны быть заранее зеркалированы во внутренний registry или поставляться как tar;

View File

@@ -31,10 +31,13 @@ from src.core.logger import logger, belief_scope
#
# @PARAM: username (str) - Admin username.
# @PARAM: password (str) - Admin password.
def create_admin(username, password):
# @PARAM: email (str | None) - Optional admin email.
def create_admin(username, password, email=None):
with belief_scope("create_admin"):
db = AuthSessionLocal()
try:
normalized_email = email.strip() if isinstance(email, str) and email.strip() else None
# 1. Ensure Admin role exists
admin_role = db.query(Role).filter(Role.name == "Admin").first()
if not admin_role:
@@ -48,12 +51,13 @@ def create_admin(username, password):
existing_user = db.query(User).filter(User.username == username).first()
if existing_user:
logger.warning(f"User {username} already exists.")
return
return "exists"
# 3. Create Admin user
logger.info(f"Creating admin user: {username}")
new_user = User(
username=username,
email=normalized_email,
password_hash=get_password_hash(password),
auth_source="LOCAL",
is_active=True
@@ -62,10 +66,12 @@ def create_admin(username, password):
db.add(new_user)
db.commit()
logger.info(f"Admin user {username} created successfully.")
return "created"
except Exception as e:
logger.error(f"Failed to create admin user: {e}")
db.rollback()
raise
finally:
db.close()
# [/DEF:create_admin:Function]
@@ -74,10 +80,15 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Create initial admin user")
parser.add_argument("--username", required=True, help="Admin username")
parser.add_argument("--password", required=True, help="Admin password")
parser.add_argument("--email", required=False, help="Admin email")
args = parser.parse_args()
# Ensure DB is initialized before creating admin
init_db()
create_admin(args.username, args.password)
try:
# Ensure DB is initialized before creating admin
init_db()
create_admin(args.username, args.password, args.email)
sys.exit(0)
except Exception:
sys.exit(1)
# [/DEF:backend.src.scripts.create_admin:Module]

View File

@@ -12,6 +12,7 @@ from src.models.auth import User, Role, Permission, ADGroupMapping
from src.services.auth_service import AuthService
from src.core.auth.repository import AuthRepository
from src.core.auth.security import verify_password, get_password_hash
from src.scripts.create_admin import create_admin
# Create in-memory SQLite database for testing
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
@@ -159,3 +160,32 @@ def test_ad_group_mapping(auth_repo):
retrieved_mapping = auth_repo.db.query(ADGroupMapping).filter_by(ad_group="DOMAIN\\ADFS_Admins").first()
assert retrieved_mapping is not None
assert retrieved_mapping.role_id == role.id
def test_create_admin_creates_user_with_optional_email(monkeypatch, db_session):
"""Test bootstrap admin creation stores optional email and Admin role"""
monkeypatch.setattr("src.scripts.create_admin.AuthSessionLocal", lambda: db_session)
result = create_admin("bootstrap-admin", "bootstrap-pass", "admin@example.com")
created_user = db_session.query(User).filter(User.username == "bootstrap-admin").first()
assert result == "created"
assert created_user is not None
assert created_user.email == "admin@example.com"
assert created_user.roles[0].name == "Admin"
def test_create_admin_is_idempotent_for_existing_user(monkeypatch, db_session):
"""Test bootstrap admin creation preserves existing user on repeated runs"""
monkeypatch.setattr("src.scripts.create_admin.AuthSessionLocal", lambda: db_session)
first_result = create_admin("bootstrap-admin-2", "bootstrap-pass")
second_result = create_admin("bootstrap-admin-2", "new-password", "changed@example.com")
created_user = db_session.query(User).filter(User.username == "bootstrap-admin-2").first()
assert first_result == "created"
assert second_result == "exists"
assert created_user is not None
assert created_user.email is None
assert verify_password("bootstrap-pass", created_user.password_hash)
assert not verify_password("new-password", created_user.password_hash)

View File

@@ -33,6 +33,10 @@ services:
BACKEND_PORT: 8000
ENABLE_BELIEF_STATE_LOGGING: ${ENABLE_BELIEF_STATE_LOGGING:-true}
TASK_LOG_LEVEL: ${TASK_LOG_LEVEL:-INFO}
INITIAL_ADMIN_CREATE: ${INITIAL_ADMIN_CREATE:-false}
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-}
INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ports:

57
docker/backend.entrypoint.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
# [DEF:docker.backend.entrypoint:Module]
# @TIER: STANDARD
# @SEMANTICS: docker, entrypoint, admin-bootstrap, runtime, backend
# @PURPOSE: Container entrypoint that performs optional idempotent admin bootstrap before starting backend runtime.
# @LAYER: Infra
# @RELATION: DEPENDS_ON -> backend/src/scripts/create_admin.py
# @INVARIANT: Existing admin account must never be overwritten during container restarts.
# [/DEF:docker.backend.entrypoint:Module]
# [DEF:docker.backend.entrypoint.bootstrap_admin:Function]
# @PURPOSE: Execute optional initial admin bootstrap from runtime environment variables.
# @PRE: Python runtime and backend sources are available inside /app/backend.
# @POST: Admin is created only when INITIAL_ADMIN_CREATE=true and required credentials are present.
bootstrap_admin() {
local create_flag="${INITIAL_ADMIN_CREATE:-false}"
local username="${INITIAL_ADMIN_USERNAME:-}"
local password="${INITIAL_ADMIN_PASSWORD:-}"
local email="${INITIAL_ADMIN_EMAIL:-}"
case "${create_flag,,}" in
true|1|yes|y)
;;
*)
echo "[entrypoint] INITIAL_ADMIN_CREATE is disabled; skipping admin bootstrap"
return 0
;;
esac
if [[ -z "${username}" ]]; then
echo "[entrypoint] INITIAL_ADMIN_USERNAME is required when INITIAL_ADMIN_CREATE=true" >&2
return 1
fi
if [[ -z "${password}" ]]; then
echo "[entrypoint] INITIAL_ADMIN_PASSWORD is required when INITIAL_ADMIN_CREATE=true" >&2
return 1
fi
echo "[entrypoint] initializing auth database"
python3 src/scripts/init_auth_db.py
echo "[entrypoint] running idempotent admin bootstrap for user '${username}'"
if [[ -n "${email}" ]]; then
python3 src/scripts/create_admin.py --username "${username}" --password "${password}" --email "${email}"
else
python3 src/scripts/create_admin.py --username "${username}" --password "${password}"
fi
}
# [/DEF:docker.backend.entrypoint.bootstrap_admin:Function]
bootstrap_admin
echo "[entrypoint] starting backend: $*"
exec "$@"

View File

@@ -152,15 +152,15 @@ psql -U postgres -d ss_tools
```bash
cd /home/busya/dev/ss-tools
./scripts/build_offline_docker_bundle.sh v1.0.0-rc2
./scripts/build_offline_docker_bundle.sh v1.0.0-rc2-docker
```
Результат появится в `dist/docker/`:
- `backend.v1.0.0-rc2.tar`
- `frontend.v1.0.0-rc2.tar`
- `postgres.v1.0.0-rc2.tar`
- `sha256sums.v1.0.0-rc2.txt`
- `manifest.v1.0.0-rc2.txt`
- `backend.v1.0.0-rc2-docker.tar`
- `frontend.v1.0.0-rc2-docker.tar`
- `postgres.v1.0.0-rc2-docker.tar`
- `sha256sums.v1.0.0-rc2-docker.txt`
- `manifest.v1.0.0-rc2-docker.txt`
- `docker-compose.enterprise-clean.yml`
- `.env.enterprise-clean.example`
@@ -171,9 +171,9 @@ cd /home/busya/dev/ss-tools
### 3. Импорт образов
```bash
docker load -i backend.v1.0.0-rc2.tar
docker load -i frontend.v1.0.0-rc2.tar
docker load -i postgres.v1.0.0-rc2.tar
docker load -i backend.v1.0.0-rc2-docker.tar
docker load -i frontend.v1.0.0-rc2-docker.tar
docker load -i postgres.v1.0.0-rc2-docker.tar
```
### 4. Подготовка конфигурации
@@ -189,6 +189,16 @@ cp .env.enterprise-clean.example .env.enterprise-clean
- `POSTGRES_PASSWORD`
- `STORAGE_ROOT`
Для первого запуска в новом контуре дополнительно задайте:
- `INITIAL_ADMIN_CREATE=true`
- `INITIAL_ADMIN_USERNAME=<org-admin-login>`
- `INITIAL_ADMIN_PASSWORD=<temporary-strong-secret>`
- `INITIAL_ADMIN_EMAIL=<optional>`
Также проверьте, что теги образов в [`.env.enterprise-clean`](.env.enterprise-clean.example) совпадают с реально загруженными:
- `BACKEND_IMAGE=ss-tools-backend:v1.0.0-rc2-docker`
- `FRONTEND_IMAGE=ss-tools-frontend:v1.0.0-rc2-docker`
### 5. Запуск в offline-контуре
```bash
@@ -197,6 +207,21 @@ docker compose --env-file .env.enterprise-clean -f docker-compose.enterprise-cle
Compose-файл использует `pull_policy: never`, поэтому runtime не должен обращаться к внешним registry.
### 6. Bootstrap администратора в контейнере
При `INITIAL_ADMIN_CREATE=true` backend container автоматически:
1. инициализирует auth DB;
2. запускает [`create_admin.py`](../backend/src/scripts/create_admin.py) с runtime-параметрами;
3. создаёт пользователя только если его ещё нет;
4. при повторном старте не изменяет существующего администратора.
После первого успешного входа обязательно:
- смените bootstrap-пароль на постоянный организационный секрет;
- установите `INITIAL_ADMIN_CREATE=false`;
- перезапустите stack с обновлённым `.env.enterprise-clean`.
Если bootstrap завершается ошибкой, backend не стартует — это ожидаемый fail-fast режим для безопасного ввода в эксплуатацию.
## Первая настройка
### 1. Инициализация базы данных
@@ -450,6 +475,24 @@ export CLEAN_TUI_ARTIFACTS_JSON=/absolute/path/artifacts.json
}
```
Минимальный пример `artifacts.json`:
```json
{
"artifacts": [
{
"id": "artifact-backend-dist",
"path": "backend/dist/package.tar.gz",
"sha256": "deadbeef",
"size": 1024,
"category": "core",
"source_uri": "https://repo.intra.company.local/releases/backend/dist/package.tar.gz",
"source_host": "repo.intra.company.local"
}
]
}
```
### Политика источников (internal-only)
Разрешены только хосты из внутреннего реестра компании, например:

View File

@@ -73,6 +73,7 @@ postgres_image_id=${POSTGRES_IMAGE_ID}
postgres_repo_digest=${POSTGRES_REPO_DIGEST}
compose_file=docker-compose.enterprise-clean.yml
env_template=.env.enterprise-clean.example
env_bootstrap_fields=INITIAL_ADMIN_CREATE,INITIAL_ADMIN_USERNAME,INITIAL_ADMIN_PASSWORD,INITIAL_ADMIN_EMAIL
checksums_file=sha256sums.${TAG}.txt
generated_at_utc=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EOF
@@ -105,6 +106,12 @@ cat > "${DIST_ROOT}/manifest.${TAG}.json" <<EOF
],
"compose_file": "docker-compose.enterprise-clean.yml",
"env_template": ".env.enterprise-clean.example",
"env_bootstrap_fields": [
"INITIAL_ADMIN_CREATE",
"INITIAL_ADMIN_USERNAME",
"INITIAL_ADMIN_PASSWORD",
"INITIAL_ADMIN_EMAIL"
],
"checksums_file": "sha256sums.${TAG}.txt"
}
EOF

View File

@@ -34,7 +34,8 @@
- 1 enterprise release flow;
- 1 TUI сценарий подготовки/проверки;
- 69 новых/обновлённых модулей (+config_loader, filesystem_scanner, db_cleanup_executor);
- документация и контракты в пределах feature-папки.
- документация и контракты в пределах feature-папки;
- runtime bootstrap администратора через переменные `.env.enterprise-clean` без hardcode секретов в образах.
## Constitution Check
@@ -167,7 +168,8 @@ frontend/
## Implementation Traceability & Final Notes
- Статус реализации: Phase 17 завершены (T001T043).
- Статус реализации: Phase 17 завершены (T001T044).
- Новое расширение (post-release hardening, 2026-03-13): добавлен scope на управляемый bootstrap администратора через `.env.enterprise-clean` и docker startup flow.
- Ключевые подтверждения polish-фазы:
- T039: smoke TUI сценария зафиксирован в [`quickstart.md`](./quickstart.md).
- T040: контрактная проверка API подтверждена тестом [`backend/tests/api/routes/test_clean_release_api.py`](../../backend/tests/api/routes/test_clean_release_api.py).
@@ -175,7 +177,30 @@ frontend/
- T042: governance conflict по префиксу закрыт и задокументирован.
- T043: добавлена итоговая traceability-нотация в текущем плане.
Итог: feature готова к финальному релизному циклу с обязательным CI gate (`COMPLIANT` only) и операционной доказательной базой для аудита.
Итог: базовая feature готова к финальному релизному циклу с обязательным CI gate (`COMPLIANT` only) и операционной доказательной базой для аудита.
## Post-Release Hardening Addendum — Admin Bootstrap via `.env.enterprise-clean`
Цель addendum: убрать ручной шаг создания initial admin после доставки offline bundle и сделать это управляемой частью container startup.
Архитектурные решения:
1. В `.env.enterprise-clean.example` добавить параметры:
- `INITIAL_ADMIN_CREATE=false` (default-safe),
- `INITIAL_ADMIN_USERNAME=admin`,
- `INITIAL_ADMIN_PASSWORD=change-me`,
- `INITIAL_ADMIN_EMAIL=` (optional).
2. В backend image добавить entrypoint-скрипт:
- запускает проверку флага `INITIAL_ADMIN_CREATE`,
- при `true` вызывает существующий скрипт создания администратора,
- обрабатывает idempotency (существующий пользователь => без ошибки, лог `already exists`).
3. В `docker-compose.enterprise-clean.yml` прокинуть переменные bootstrap администратора только в `backend` service.
4. В операционном runbook зафиксировать обязательную ротацию bootstrap-пароля после первого входа.
5. В offline bundle manifest оставить ссылку на `.env.enterprise-clean.example` как source-of-truth для параметров запуска.
Нефункциональные ограничения:
- Никаких default production секретов в Git.
- Повторный restart контейнера не должен менять существующего admin.
- Ошибка bootstrap не должна маскироваться: должна логироваться и приводить к fail-fast старта backend (чтобы оператор устранил причину до ввода в эксплуатацию).
## Complexity Tracking

View File

@@ -73,6 +73,23 @@ cd /home/busya/dev/ss-tools
2. Повторно запустить проверку из того же TUI экрана (`F5`).
3. Повторять до статуса `COMPLIANT`.
## Admin Bootstrap via `.env.enterprise-clean` (Docker Runtime)
Для offline bundle deployment в контейнерах используйте runtime файл `.env.enterprise-clean` на базе шаблона `.env.enterprise-clean.example`.
Обязательные параметры bootstrap:
- `INITIAL_ADMIN_CREATE=true` для первого запуска;
- `INITIAL_ADMIN_USERNAME=<org-admin-login>`;
- `INITIAL_ADMIN_PASSWORD=<strong-temporary-secret>`;
- `INITIAL_ADMIN_EMAIL=<optional>`.
Ожидаемое поведение:
1. Backend container стартует.
2. EntryPoint проверяет флаг `INITIAL_ADMIN_CREATE`.
3. Если пользователь не существует — создаётся Admin user.
4. Если пользователь уже существует — bootstrap шага пропускается без изменения учётной записи.
5. После первого входа оператор выполняет обязательную ротацию bootstrap-пароля.
## CI Gate (обязательный)
После операторского прогона TUI та же политика должна быть проверена в CI.

View File

@@ -76,6 +76,7 @@
- Что происходит, если внутренний сервер ресурсов временно недоступен во время развёртывания в изолированном контуре?
- Как система реагирует, если в конфигурации присутствует косвенная ссылка на внешний интернет-источник (например, зеркальный URL вне корпоративного домена)?
- Что происходит при повторной подготовке clean-дистрибутива для уже очищенного релиз-кандидата?
- Что происходит, если контейнер перезапускается с `INITIAL_ADMIN_CREATE=true` и администратор уже существует?
## Requirements *(mandatory)*
@@ -101,6 +102,9 @@
- **FR-018**: Стадия `NO_EXTERNAL_ENDPOINTS` MUST сканировать все текстовые файлы (включая код, конфиги, скрипты) на наличие URL/хостов и сверять каждый найденный endpoint с `allowed_sources`.
- **FR-019**: Процесс clean-подготовки MUST включать стадию очистки БД от тестовых пользователей и демо-данных. Правила очистки (таблицы, условия, исключения) задаются в секции `database_cleanup` файла `.clean-release.yaml`.
- **FR-020**: Структура `.clean-release.yaml` MUST включать секции: `profile`, `scan_mode`, `prohibited_categories`, `prohibited_paths`, `allowed_sources`, `ignore_paths`, `database_cleanup` (с подсекциями `tables` и `preserve`).
- **FR-021**: Enterprise clean release bundle MUST включать `.env.enterprise-clean.example` с параметрами bootstrap администратора (`INITIAL_ADMIN_USERNAME`, `INITIAL_ADMIN_PASSWORD`, `INITIAL_ADMIN_EMAIL` optional, `INITIAL_ADMIN_CREATE`) и безопасными комментариями по ротации пароля.
- **FR-022**: Docker startup flow backend MUST поддерживать idempotent bootstrap администратора при запуске контейнера: при `INITIAL_ADMIN_CREATE=true` система создаёт администратора только если пользователь отсутствует и не перезаписывает существующие credentials/roles.
- **FR-023**: Процесс публикации clean bundle MUST включать операторский сценарий заполнения runtime `.env.enterprise-clean` из шаблона и подтверждение, что bootstrap-пароль заменён на организационный секрет до первого production запуска.
### Key Entities *(include if feature involves data)*

View File

@@ -141,6 +141,20 @@
---
## Phase 8: Post-Release Hardening — Admin Bootstrap in Docker
**Purpose**: Автоматизировать первичное создание администратора через runtime `.env.enterprise-clean` в offline/enterprise deployment.
- [X] T045 Add admin bootstrap env contract to `.env.enterprise-clean.example` (`INITIAL_ADMIN_CREATE`, `INITIAL_ADMIN_USERNAME`, `INITIAL_ADMIN_PASSWORD`, optional `INITIAL_ADMIN_EMAIL`)
- [X] T046 Wire admin bootstrap envs to backend runtime in `docker-compose.enterprise-clean.yml`
- [X] T047 Add backend entrypoint flow that performs idempotent admin bootstrap before app start in `docker/backend.Dockerfile` and new entrypoint script
- [X] T048 Extend admin creation script for optional email and deterministic exit behavior for existing user in `backend/src/scripts/create_admin.py`
- [X] T049 Update offline bundle packaging metadata to preserve new env contract in `scripts/build_offline_docker_bundle.sh` and bundle docs
- [X] T050 Add deployment runbook section for secure admin bootstrap and mandatory password rotation in `README.md` and `docs/installation.md`
- [X] T051 Add regression tests for container bootstrap path and create-admin idempotency in `backend/tests/scripts/` and/or service tests
---
## Dependencies & Execution Order
### Phase Dependencies