Добавить воспроизводимую основу инфраструктуры
CI / tests-and-images (pull_request) Has been cancelled
CI / tests-and-images (pull_request) Has been cancelled
This commit is contained in:
1 parent
fb1a67044c
commit
d03da40eee
27 files changed
+664
-25
No files matched your search
@@ -0,0 +1,20 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tests-and-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run backend tests
|
||||
run: PYTHONPATH=backend python3 -m unittest discover -s backend -p 'test_*.py' -v
|
||||
- name: Run Telegram bot tests
|
||||
run: python3 -m unittest discover -s tools/telegram-bot -p 'test_*.py' -v
|
||||
- name: Build production images
|
||||
run: |
|
||||
docker build -f deploy/production/Dockerfile.backend -t akyldash-backend:ci .
|
||||
docker build -f deploy/production/Dockerfile.opensearch -t akyldash-opensearch:ci .
|
||||
@@ -0,0 +1,23 @@
|
||||
# Акылдаш — инструкции для агента
|
||||
|
||||
Проект: юридическая информационно-аналитическая платформа; Search API на
|
||||
Python, SQLite и OpenSearch. Production пока не развернут.
|
||||
|
||||
- Начинай с [инфраструктурного состояния](docs/operations/CURRENT_STATE.md) и
|
||||
[правил доступа](docs/operations/ACCESS.md). Не запрашивай доступы повторно.
|
||||
- Архитектура и процедуры: [инфраструктура](docs/operations/INFRASTRUCTURE.md),
|
||||
[деплой](docs/operations/DEPLOYMENT.md), [backup](docs/operations/BACKUP.md),
|
||||
[rollback](docs/operations/ROLLBACK.md), [восстановление](docs/operations/RECOVERY.md).
|
||||
- Работай в feature/fix/refactor-ветке. `main` защищена; изменения вноси через
|
||||
PR, не сливай его автоматически. Сохраняй незакоммиченные изменения.
|
||||
- SSH alias — `akyldash_vps`; используй только read-only диагностику. Не давай
|
||||
`codex` общий sudo или доступ к Docker socket. Root-действия требует владелец.
|
||||
Для Gitea используй `gitea-api`, не читай файлы с учётными данными.
|
||||
- Локальные проверки: `PYTHONPATH=backend python3 -m unittest discover -s backend -p 'test_*.py' -v` и
|
||||
`python3 -m unittest discover -s tools/telegram-bot -p 'test_*.py' -v`.
|
||||
Production images собираются командами из `.gitea/workflows/ci.yaml`.
|
||||
- Перед изменениями production необходим проверенный backup. Формального
|
||||
механизма миграций нет; проверяй совместимость SQLite/OpenSearch. Не меняй
|
||||
SSH, firewall или production вручную без владельца и последующего PR.
|
||||
- После каждого изменения инфраструктуры обновляй
|
||||
`docs/operations/CURRENT_STATE.md` по фактическим проверкам.
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Backend `0.11.3`: ключ подписи поисковых снимков читается из Docker secret-файла, а не из аргументов процесса. Добавлена базовая инфраструктура VPS, drift-проверка и CI для тестов и сборки образов; production-деплой остаётся выключен до проверки runner, backup/restore и ограниченного deploy-helper.
|
||||
- Frontend `0.1.2`: на планшетной ширине экран поиска переходит в одну колонку, чтобы фильтры, результаты и документ помещались в окно.
|
||||
- Backend `0.11.2`: смена пароля требует текущий пароль и запрещает оставить прежний, включая временный пароль при первом входе.
|
||||
- Backend `0.11.1` и frontend `0.1.1`: в управлении пользователями показываются даты первого и последнего входа, а также последнего сброса пароля.
|
||||
|
||||
@@ -10,7 +10,7 @@ Telegram-бот — только часть рабочего окружения
|
||||
## Текущее состояние
|
||||
|
||||
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2`, backend версии
|
||||
`0.11.2` и внутренний frontend лаборатории `0.1.2`. Авторизация хранится в
|
||||
`0.11.3` и внутренний frontend лаборатории `0.1.2`. Авторизация хранится в
|
||||
SQLite; первый пользователь `admin` создаётся интерактивной CLI-командой.
|
||||
Поиск расширяет аббревиатуры организационно-правовых форм и поднимает
|
||||
действующий Налоговый кодекс для запросов о налоге на прибыль.
|
||||
@@ -18,7 +18,7 @@ SQLite; первый пользователь `admin` создаётся инт
|
||||
| Компонент | Версия | Состояние |
|
||||
|---|---:|---|
|
||||
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
|
||||
| Backend | `0.11.2` | авторизация и поиск с фильтрами; тематический поиск ОсОО/ЖЧК |
|
||||
| Backend | `0.11.3` | авторизация и поиск с фильтрами; тематический поиск ОсОО/ЖЧК |
|
||||
| Frontend | `0.1.2` | лаборатория поиска, личный кабинет и управление учетными записями |
|
||||
| Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи |
|
||||
| RAG и база знаний | — | ещё не созданы |
|
||||
@@ -62,4 +62,4 @@ python3 -m unittest discover -s tools/telegram-bot -v
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.2
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Backend Акылдаш
|
||||
|
||||
Версия: `0.11.2`
|
||||
Версия: `0.11.3`
|
||||
|
||||
Первая backend-область проекта — загрузка правовых документов из официального
|
||||
Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в
|
||||
@@ -253,4 +253,4 @@ PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --la
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Backend v0.11.2 · Frontend v0.1.2
|
||||
Акылдаш · Backend v0.11.3 · Frontend v0.1.2
|
||||
@@ -451,6 +451,7 @@ def main() -> int:
|
||||
parser.add_argument("--reviews-db", type=Path, default=Path("data/search-reviews.sqlite3"))
|
||||
parser.add_argument("--users-db", type=Path, default=Path("data/search-users.sqlite3"))
|
||||
parser.add_argument("--review-secret", default=None)
|
||||
parser.add_argument("--review-secret-file", type=Path)
|
||||
parser.add_argument("--cookie-secure", action=argparse.BooleanOptionalAction, default=os.environ.get("COOKIE_SECURE", "false").lower() == "true")
|
||||
parser.add_argument("--create-admin", action="store_true", help="create the initial admin account and exit")
|
||||
parser.add_argument("--reset-admin-password", action="store_true", help="set a temporary password for admin and exit")
|
||||
@@ -458,7 +459,14 @@ def main() -> int:
|
||||
parser.add_argument("--port", type=int, default=8080)
|
||||
parser.add_argument("--version", action="version", version=APP_VERSION)
|
||||
arguments = parser.parse_args()
|
||||
secret = arguments.review_secret.encode() if arguments.review_secret else None
|
||||
if arguments.review_secret and arguments.review_secret_file:
|
||||
parser.error("choose either --review-secret or --review-secret-file")
|
||||
try:
|
||||
secret = arguments.review_secret.encode() if arguments.review_secret else arguments.review_secret_file.read_bytes().strip() if arguments.review_secret_file else None
|
||||
except OSError as error:
|
||||
parser.error(f"cannot read review secret file: {error}")
|
||||
if arguments.review_secret_file and not secret:
|
||||
parser.error("review secret file is empty")
|
||||
api = Api(arguments.url, arguments.index, arguments.data, arguments.reviews_db, secret, arguments.users_db, arguments.cookie_secure)
|
||||
if arguments.create_admin or arguments.reset_admin_password:
|
||||
users = api.auth_store.list_users()
|
||||
|
||||
@@ -17,7 +17,7 @@ from typing import Iterator
|
||||
|
||||
from search.catalog import authority_codes, legal_force_code, source_code
|
||||
|
||||
APP_VERSION = "0.11.2"
|
||||
APP_VERSION = "0.11.3"
|
||||
LANGUAGES = {"ru", "ky"}
|
||||
DEFAULT_MAPPING = Path(__file__).with_name("minjust-fragments-index.json")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Оценка поисковой выдачи · Акылдаш</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font: 16px/1.5 "Noto Sans", "Segoe UI", sans-serif; color: #38262A; background: #F7F2E8; --ink: #38262A; --muted: #786469; --line: #DED3CE; --surface: #fff; --surface-muted: #FBF8F4; --brand: #9D2633; --brand-dark: #84202B; --accent: #A37B32; --focus: #245C78; --subtle: #F8EBED; --serif: "Noto Serif", Georgia, serif; }
|
||||
:root { color-scheme: light; font: 16px/1.5 "Noto Sans", sans-serif; color: #38262A; background: #F7F2E8; --ink: #38262A; --muted: #786469; --line: #DED3CE; --surface: #fff; --surface-muted: #FBF8F4; --brand: #9D2633; --brand-dark: #84202B; --accent: #A37B32; --focus: #245C78; --subtle: #F8EBED; --serif: "Noto Serif", serif; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: var(--ink); background: #F7F2E8; }
|
||||
.review__skip { position: absolute; inset-block-start: 0; inset-inline-start: -10000px; padding: .5rem 1rem; background: var(--surface); }
|
||||
@@ -53,7 +53,7 @@
|
||||
.review__comment { min-height: 4rem; resize: vertical; background: var(--surface); }
|
||||
.review__document { position: sticky; inset-block-start: 1rem; min-height: 24rem; }
|
||||
.review__document-meta { margin-block-end: 1rem; padding-block-end: 1rem; border-block-end: 1px solid var(--line); color: var(--muted); }
|
||||
.review__document-body { max-width: 75ch; overflow-wrap: anywhere; font: 400 1.125rem/1.65 "Noto Sans", "Segoe UI", sans-serif; }
|
||||
.review__document-body { max-width: 75ch; overflow-wrap: anywhere; font: 400 1.125rem/1.65 "Noto Sans", sans-serif; }
|
||||
.review__document-body img { max-width: 100%; height: auto; }
|
||||
.review__actions { max-width: 1320px; margin: 1.5rem auto 0; padding-inline: clamp(1rem, 3vw, 3rem); }
|
||||
.review__actions-inner { display: flex; align-items: end; gap: .75rem; padding: 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; }
|
||||
@@ -115,7 +115,7 @@
|
||||
.search__document { min-height: 60vh; }
|
||||
.search__document-controls { display: flex; flex-wrap: wrap; gap: .75rem; }
|
||||
.search__document-controls label { display: grid; gap: .25rem; min-width: 11rem; color: var(--muted); font-size: .875rem; }
|
||||
.search__document-body { max-width: 75ch; margin: auto; font: 400 1.125rem/1.65 "Noto Sans", "Segoe UI", sans-serif; }
|
||||
.search__document-body { max-width: 75ch; margin: auto; font: 400 1.125rem/1.65 "Noto Sans", sans-serif; }
|
||||
.search__document-body h1, .search__document-body h2, .search__document-body h3 { font-family: var(--serif); }
|
||||
.search__document-body img { max-width: 100%; height: auto; }
|
||||
.search__sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
@@ -176,7 +176,7 @@
|
||||
<dialog id="users-dialog" aria-labelledby="users-heading"><section class="review__page"><h2 id="users-heading">Управление пользователями</h2><form id="create-user-form"><label>Логин<input id="new-username" autocomplete="off" minlength="3" maxlength="64" required></label><label>Временный пароль<input id="temporary-password" type="password" autocomplete="new-password" minlength="12" maxlength="256" required></label><p>Пароль — от 12 символов. Новый пользователь сменит его при первом входе. Передайте временный пароль отдельно.</p><p id="users-error" role="alert" class="review__status review__status--error"></p><button class="review__button--primary" type="submit">Создать пользователя</button></form><ul id="user-list" class="review__user-list"></ul><button type="button" data-close="users-dialog">Закрыть</button></section></dialog>
|
||||
<dialog id="document-dialog"><button class="review__dialog-close" id="dialog-close" type="button">Закрыть</button><h2 id="dialog-heading">Документ</h2><div class="review__document-body" id="dialog-body"></div></dialog>
|
||||
<div class="review__tour-spotlight" id="tutorial-spotlight" hidden aria-hidden="true"></div><dialog class="review__tour" id="tutorial-dialog" aria-labelledby="tutorial-heading" aria-describedby="tutorial-text"><div class="review__tour-content"><p class="review__tour-kicker">Короткое знакомство</p><h2 id="tutorial-heading"></h2><p id="tutorial-text"></p><div class="review__tour-footer"><span class="review__tour-progress" id="tutorial-progress"></span><div class="review__tour-actions"><button id="tutorial-skip" type="button">Пропустить</button><button class="review__button--primary" id="tutorial-next" type="button">Далее</button></div></div></div></dialog>
|
||||
<footer class="review__header"><div class="review__header-content">Акылдаш · Frontend v0.1.2 · Backend v0.11.2 · Внутренняя лаборатория релевантности</div></footer>
|
||||
<footer class="review__header"><div class="review__header-content">Акылдаш · Frontend v0.1.2 · Backend v0.11.3 · Внутренняя лаборатория релевантности</div></footer>
|
||||
<script>
|
||||
document.body.classList.add('review__locked');
|
||||
const DRAFT_KEY = 'akyldash-search-review-draft';
|
||||
@@ -193,7 +193,7 @@
|
||||
let account = null;
|
||||
const reviewRoute = location.pathname === '/review';
|
||||
$('review').hidden = !reviewRoute; $('search-page').hidden = reviewRoute; $('review-nav').classList.toggle('is-active', reviewRoute); $('search-nav').classList.toggle('is-active', !reviewRoute);
|
||||
if (!reviewRoute) { document.title = 'Поиск по правовой базе · Акылдаш'; document.querySelector('.review__header-content').hidden = true; $('tutorial-open').hidden = true; document.querySelector('.review__skip').href = '#public-results'; document.querySelector('footer .review__header-content').textContent = 'Акылдаш · Frontend v0.1.2 · Backend v0.11.2'; }
|
||||
if (!reviewRoute) { document.title = 'Поиск по правовой базе · Акылдаш'; document.querySelector('.review__header-content').hidden = true; $('tutorial-open').hidden = true; document.querySelector('.review__skip').href = '#public-results'; document.querySelector('footer .review__header-content').textContent = 'Акылдаш · Frontend v0.1.2 · Backend v0.11.3'; }
|
||||
async function apiFetch(path, options = {}) { const response = await fetch(path, {credentials: 'same-origin', ...options}); if (response.status === 401 && path !== '/auth/login') { document.body.classList.add('review__locked'); if (!$('login-dialog').open) $('login-dialog').showModal(); $('login-username').focus(); } return response; }
|
||||
const showLogin = () => { document.body.classList.add('review__locked'); if (!$('login-dialog').open) $('login-dialog').showModal(); $('login-username').focus(); };
|
||||
const enterWorkspace = (user) => { account = user; $('signed-in-user').textContent = `${user.username} · ${user.role}`; $('reviewer').value = user.username; $('profile-username').textContent = user.username; $('profile-role').textContent = user.role; $('profile-created').textContent = new Date(user.created_at * 1000).toLocaleDateString('ru-RU'); $('users-open').hidden = user.username.toLowerCase() !== 'admin'; document.body.classList.remove('review__locked'); if (!reviewRoute) initSearchPage(); else if (!hasSeenTour()) setTimeout(openTutorial, 350); };
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -36,6 +38,27 @@ class SearchApiTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ApiError, "within 10000 results"):
|
||||
api.handle("GET", "/search?q=x&page=100&page_size=100")
|
||||
|
||||
def test_review_secret_file_is_read_without_command_line_value(self):
|
||||
from search.api import main
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
secret_file = Path(temporary) / "secret"
|
||||
secret_file.write_text("secret-value\n", encoding="utf-8")
|
||||
with patch("sys.argv", ["search.api", "--review-secret-file", str(secret_file)]), patch("search.api.Api") as api, patch("search.api.ThreadingHTTPServer"):
|
||||
main()
|
||||
self.assertEqual(api.call_args.args[4], b"secret-value")
|
||||
|
||||
def test_review_secret_file_rejects_empty_content(self):
|
||||
from search.api import main
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
secret_file = Path(temporary) / "secret"
|
||||
secret_file.touch()
|
||||
with patch("sys.argv", ["search.api", "--review-secret-file", str(secret_file)]), contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as result:
|
||||
main()
|
||||
self.assertEqual(result.exception.code, 2)
|
||||
|
||||
def test_production_api_uses_registration_intent_boosts(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
api = self.make_api(Path(temporary))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Absolute path on the production host containing minjust-normalized/.
|
||||
DATA_ROOT=/volume1/docker/akyldash/data
|
||||
DATA_ROOT=/var/lib/akyldash/data
|
||||
REVIEW_SECRET_FILE=/etc/akyldash/review_secret
|
||||
BACKEND_PORT=8080
|
||||
SEARCH_INDEX=akyldash-fragments-current
|
||||
OPENSEARCH_MEM_LIMIT=4g
|
||||
OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g
|
||||
# Generate with: openssl rand -hex 32
|
||||
REVIEW_SECRET=replace-with-a-random-secret
|
||||
COOKIE_SECURE=true
|
||||
@@ -12,8 +12,15 @@ workspace; it does not replace host-level network controls or TLS.
|
||||
## First deployment
|
||||
|
||||
1. Copy this directory to the host with the repository source.
|
||||
2. Copy `.env.example` to `.env`, set an absolute `DATA_ROOT`, and replace
|
||||
`REVIEW_SECRET` with a random value. Do not commit `.env`.
|
||||
2. Copy `.env.example` to `.env`, set an absolute `DATA_ROOT`, and create the
|
||||
secret file referenced by `REVIEW_SECRET_FILE` outside the repository:
|
||||
|
||||
```bash
|
||||
sudo install -d -o root -g root -m 0700 /etc/akyldash
|
||||
sudo sh -c 'umask 077; openssl rand -hex 32 > /etc/akyldash/review_secret'
|
||||
```
|
||||
|
||||
Do not commit `.env` or the secret file.
|
||||
3. Put the normalized dataset under `$DATA_ROOT/minjust-normalized/`.
|
||||
4. Check the rendered configuration:
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
name: akyldash
|
||||
|
||||
services:
|
||||
opensearch:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/production/Dockerfile.opensearch
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: local
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
bootstrap.memory_lock: "true"
|
||||
@@ -32,8 +39,12 @@ services:
|
||||
context: ../..
|
||||
dockerfile: deploy/production/Dockerfile.backend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: local
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
environment:
|
||||
REVIEW_SECRET: ${REVIEW_SECRET:?set REVIEW_SECRET in .env}
|
||||
COOKIE_SECURE: ${COOKIE_SECURE:-true}
|
||||
command:
|
||||
- python
|
||||
@@ -53,8 +64,10 @@ services:
|
||||
- /app/data/search-reviews.sqlite3
|
||||
- --users-db
|
||||
- /app/data/search-users.sqlite3
|
||||
- --review-secret
|
||||
- ${REVIEW_SECRET}
|
||||
- --review-secret-file
|
||||
- /run/secrets/review_secret
|
||||
secrets:
|
||||
- review_secret
|
||||
ports:
|
||||
- "127.0.0.1:${BACKEND_PORT:-8080}:8080"
|
||||
depends_on:
|
||||
@@ -65,3 +78,7 @@ services:
|
||||
|
||||
volumes:
|
||||
opensearch-data:
|
||||
|
||||
secrets:
|
||||
review_secret:
|
||||
file: ${REVIEW_SECRET_FILE:?set REVIEW_SECRET_FILE in .env}
|
||||
+5
-1
@@ -26,6 +26,10 @@
|
||||
|
||||
- [Текущий статус](operations/project-status.md) — выполненные шаги, риски и
|
||||
ближайшие действия.
|
||||
- [Состояние инфраструктуры](operations/CURRENT_STATE.md) — подтверждённое
|
||||
состояние production и оставшиеся проверки.
|
||||
- [Инфраструктура и эксплуатация](operations/INFRASTRUCTURE.md) — схема,
|
||||
доступы, deployment, backup, rollback и recovery.
|
||||
- [План Telegram-пространства](operations/telegram-workspace-plan.md) — темы,
|
||||
роли и этапы развития командного окружения.
|
||||
|
||||
@@ -41,4 +45,4 @@
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.2
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2
|
||||
@@ -0,0 +1,58 @@
|
||||
# Infrastructure access
|
||||
|
||||
## Discover existing access first
|
||||
|
||||
In a new Codex session, inspect the SSH alias and key file metadata without
|
||||
printing key contents, then check the connection:
|
||||
|
||||
```bash
|
||||
ssh -F ~/.ssh/config -G akyldash_vps | awk '$1 == "hostname" || $1 == "user" || $1 == "port" || $1 == "identityfile" { print }'
|
||||
test -r ~/.ssh/akyldash_codex && echo 'SSH key is available'
|
||||
ssh -F ~/.ssh/config -o BatchMode=yes akyldash_vps 'id; hostname'
|
||||
gitea-api GET /version
|
||||
gitea-api GET /repos/admin/akyldash
|
||||
```
|
||||
|
||||
Do not ask for the host, login, password, token, or key again before these
|
||||
checks. Never print a private key, token, or complete environment file.
|
||||
|
||||
## VPS
|
||||
|
||||
- SSH alias: `akyldash_vps`, user `codex`, host `206.223.249.118`, port `22`.
|
||||
- SSH configuration: `~/.ssh/config`.
|
||||
- Identity file: `~/.ssh/akyldash_codex` (path only; never copy its contents).
|
||||
- `codex` can run unprivileged read-only diagnostics. The audited account has no
|
||||
non-interactive sudo and must not be added to `sudo` or the `docker` group.
|
||||
- Host package installation, service setup, firewall changes, secret creation,
|
||||
backup restore, and production changes require the VPS owner to run a reviewed
|
||||
root command. No restricted deploy-helper is installed yet.
|
||||
- Never connect to other VPS hosts or Synology as part of this deployment.
|
||||
|
||||
This execution environment's system SSH config include had unsafe file
|
||||
permissions, so plain `ssh akyldash_vps` failed. `ssh -F ~/.ssh/config ...`
|
||||
worked. Network policy may also require an approved SSH command. If the key is
|
||||
missing in a future execution environment, report that limitation; do not
|
||||
retrieve it from another machine.
|
||||
|
||||
## Gitea
|
||||
|
||||
- Repository: `https://git.vakanaut.ru/admin/akyldash`.
|
||||
- Use the installed `gitea-api` helper; it reads credentials from the
|
||||
environment. Never inspect local credential files, print environment values,
|
||||
or construct HTTP authorization headers.
|
||||
- The audited API token can read/write this repository and create issues, but
|
||||
does not have `read:user` or `read:package`. An API `403` caused by a missing
|
||||
scope is not evidence that the account itself lacks the permission.
|
||||
- Protected `main` requires one approval. Do not push directly to `main`, merge
|
||||
automatically, or start a review. Work on a feature branch and use a PR.
|
||||
- Gitea Actions and package features are enabled for the repository, but runner
|
||||
availability and package publishing permissions have not been confirmed.
|
||||
|
||||
## Who can do what
|
||||
|
||||
| Role | Operations |
|
||||
|---|---|
|
||||
| `codex` over SSH | Read-only host diagnostics; inspect service health and public listeners |
|
||||
| Restricted deploy-helper | Intended future allowlisted backup/deploy/status actions; not installed |
|
||||
| VPS owner/root | Bootstrap, package installation, SSH/firewall, secret setup, recovery, and approval of production changes |
|
||||
| Gitea repository writer | Feature branch push and PR creation; protected-main merge needs an approval |
|
||||
@@ -0,0 +1,28 @@
|
||||
# Backup and restore
|
||||
|
||||
## Current status
|
||||
|
||||
No production data or backup mechanism existed during the 2026-10-11 audit.
|
||||
`/var/backups/akyldash` is a planned root-only location, not an active backup.
|
||||
Do not report backup protection as enabled until an archive and restore have
|
||||
both been checked.
|
||||
|
||||
## Required release policy
|
||||
|
||||
- Create a backup before every production change, including any future schema
|
||||
or index migration.
|
||||
- Include the normalized corpus, `search-users.sqlite3`,
|
||||
`search-reviews.sqlite3`, the OpenSearch index/snapshot, deployment digest,
|
||||
and required secret file in the protected backup set. Store secrets separately
|
||||
with equivalent access controls.
|
||||
- Verify archive readability, checksums, and SQLite integrity before
|
||||
replacing the previous known-good backup.
|
||||
- Keep one current pre-release backup. If deployment fails, retain the backup
|
||||
required to restore the previous state.
|
||||
- Test restore into an isolated target before relying on the backup. Do not
|
||||
restore over production as a test.
|
||||
|
||||
The data is not yet managed by a tested backup script. Until that exists, the
|
||||
owner must pause deployment and approve a maintenance window for a reviewed
|
||||
backup/restore procedure. Copying live SQLite files with `cp` or an unchecked
|
||||
tar archive is not an approved backup.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Infrastructure current state
|
||||
|
||||
Last audit: 2026-10-11. Source: read-only SSH session as `codex`, local
|
||||
repository inspection, and Gitea API. This document separates measured facts
|
||||
from planned configuration.
|
||||
|
||||
## VPS facts
|
||||
|
||||
- Host: `avprogs.hlab.kz`, `206.223.249.118`, SSH port `22`, user `codex`.
|
||||
- OS/kernel: Ubuntu Server `24.04.5 LTS`, `6.8.0-142-generic`.
|
||||
- Resources: 4 vCPU, 7.7 GiB RAM, 3.8 GiB swap, 99 GiB disk with 93 GiB free.
|
||||
- Network: interface `ens3`, IPv4 `206.223.249.118/32`.
|
||||
- `codex` is UID/GID 1000, groups `codex` and `users`; non-interactive sudo is
|
||||
unavailable.
|
||||
- SSH service is running. The inspected sshd configuration has public-key and
|
||||
password authentication enabled and `PermitRootLogin yes`; no SSH changes
|
||||
were made.
|
||||
- Docker, Docker Compose, Git, Nginx, Caddy, Certbot, application paths, and
|
||||
application services are absent. Only base OS services and SSH were running.
|
||||
- Public listeners observed: TCP `22`; local resolver listeners on loopback.
|
||||
- UFW is reported active in the supplied VPS notes, but the audit account could
|
||||
not independently read its status. Treat firewall state and rules as
|
||||
unverified.
|
||||
|
||||
## Application and Gitea facts
|
||||
|
||||
- Repository: `admin/akyldash`, default branch `main`; Gitea version `1.25.5`.
|
||||
- The repository API reports push permission, Actions and packages enabled, and
|
||||
PRs enabled. `main` is protected and requires one approval; the current API
|
||||
principal cannot push or merge directly to `main`.
|
||||
- The Gitea token lacks `read:user` and `read:package`; runner status and package
|
||||
publishing permissions could not be checked. No workflow existed at audit.
|
||||
- Production Compose describes Python Search API `0.11.3`, SQLite account and
|
||||
review data, and OpenSearch `3.7.0` with `analysis-icu`. Nothing is running.
|
||||
- Compose binds the API to `127.0.0.1:8080`, leaves OpenSearch private, uses a
|
||||
named OpenSearch volume, and a host data bind mount. TLS/reverse proxy,
|
||||
registry digest deployment, backup, rollback helper, and systemd deployment
|
||||
are not configured.
|
||||
- The app has no migration framework or SMTP setup. Current app versions:
|
||||
backend `0.11.3`, frontend `0.1.2`, Telegram bot `0.2.2` (the bot is not
|
||||
deployed on this VPS).
|
||||
|
||||
## Checks not completed
|
||||
|
||||
- SSH completed with the user config explicitly selected; plain SSH was blocked
|
||||
by permissions on a system config include. The private key was not read.
|
||||
- UFW rules require root and remain unverified.
|
||||
- DNS delegation, HTTPS, Cloudflare Full (strict), public application health,
|
||||
Actions runner availability, registry publishing, and production deployment
|
||||
have not been tested.
|
||||
- No packages or services were installed or changed on the VPS.
|
||||
|
||||
## Next owner actions
|
||||
|
||||
1. Review and run the root bootstrap on the dedicated host.
|
||||
2. Review SSH/password/root policy and firewall rules separately; this change
|
||||
did not touch them.
|
||||
3. Set up DNS, origin certificate, reverse proxy, protected secret files, and
|
||||
initial corpus/index.
|
||||
4. Confirm a Gitea Actions runner and package permissions, then test CI and
|
||||
backup/restore before considering automated production deploy.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Infrastructure decisions
|
||||
|
||||
| Date | Decision | Reason and alternatives | Status / limits |
|
||||
|---|---|---|---|
|
||||
| 2026-10-11 | Use Docker Compose on the dedicated Ubuntu VPS | Matches the existing production Compose baseline and workload; Kubernetes adds no demonstrated need | Planned; Docker is not installed |
|
||||
| 2026-10-11 | Keep `codex` out of sudo and the Docker group | Docker socket access is effectively root; use an owner-run bootstrap and a narrowly reviewed deploy-helper | No helper installed yet |
|
||||
| 2026-10-11 | Keep Search API on loopback and OpenSearch on the private Compose network | Minimizes host exposure; HTTPS terminates at a reviewed reverse proxy | Compose target only; no proxy or firewall audit privileges |
|
||||
| 2026-10-11 | Mount the review signing key as a Compose secret file | Avoids putting its value in command-line arguments; an environment variable or CLI value is easier to inspect in process metadata | Implemented in repository Compose; not deployed |
|
||||
| 2026-10-11 | Do not enable production deployment from Gitea Actions yet | Runner, package publishing scope, deploy-helper, backup/restore, and production health are not verified | Revisit after controlled end-to-end test |
|
||||
@@ -0,0 +1,37 @@
|
||||
# Deployment
|
||||
|
||||
## Current status
|
||||
|
||||
Production deployment is not enabled or tested. The VPS has no Docker, Git,
|
||||
application data, proxy, registry pull configuration, backup process, or
|
||||
restricted deploy-helper. Do not deploy until the owner has completed the root
|
||||
bootstrap, provided a protected secret file and a reviewed proxy/firewall
|
||||
configuration, and a backup and restore have been tested.
|
||||
|
||||
## First host preparation
|
||||
|
||||
1. Review the current inventory and state, then have the owner run
|
||||
`sudo bash infra/bootstrap/bootstrap-ubuntu-24.04.sh` from a verified copy
|
||||
of this branch on the VPS. The script installs Docker Engine/Compose and Git,
|
||||
sets Docker log rotation, and creates root-owned paths. It refuses other OS
|
||||
versions and does not change SSH, UFW, sudoers, or user groups.
|
||||
2. Install the repository checkout at `/opt/akyldash/current` and configure
|
||||
`/etc/akyldash/production.env` plus `/etc/akyldash/review_secret` outside
|
||||
Git. The secret file must be root-readable only.
|
||||
3. Review DNS and certificate issuance, choose and configure the reverse proxy,
|
||||
then ask the owner to open only the required ports after confirming current
|
||||
firewall rules.
|
||||
4. Create and verify the first backup before starting services. Load and
|
||||
validate the OpenSearch index, create the initial admin interactively, and
|
||||
verify `/review` over HTTPS.
|
||||
|
||||
The remaining steps are intentionally manual until a restricted deploy-helper,
|
||||
registry permissions, a live Gitea runner, and the backup path are tested.
|
||||
|
||||
## CI
|
||||
|
||||
`.gitea/workflows/ci.yaml` runs the Python unit suites and builds both
|
||||
production images on pull requests and pushes to `main`. It does not publish an
|
||||
image or deploy production. Gitea Actions is enabled at repository level, but a
|
||||
successful workflow run has not yet confirmed that a compatible runner is
|
||||
online.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Infrastructure
|
||||
|
||||
## Intended layout
|
||||
|
||||
Production is planned for the dedicated Ubuntu VPS at `206.223.249.118`.
|
||||
`deploy/production/compose.yaml` defines a Python Search API and private
|
||||
OpenSearch 3.7 node. The API binds to loopback port `8080`; OpenSearch is only
|
||||
exposed to the Compose network. The host reverse proxy should terminate HTTPS
|
||||
for `akyldash.kg` and proxy to the API. Cloudflare should use Full (strict) only
|
||||
after the origin certificate and DNS path are verified.
|
||||
|
||||
The API stores its account and review SQLite files beside the normalized corpus
|
||||
under the persistent data directory. OpenSearch uses a named volume. The API's
|
||||
review signing key is mounted from a host file; it is not passed in process
|
||||
arguments or committed to Git. Docker logs are bounded to 10 MiB per file and
|
||||
three files per service.
|
||||
|
||||
## Actual state
|
||||
|
||||
The 2026-10-11 read-only audit found a fresh Ubuntu 24.04.5 host with SSH only.
|
||||
Docker, Git, a reverse proxy, application directories, Compose services, TLS,
|
||||
and backups are not installed/configured. The `codex` account has no
|
||||
non-interactive sudo. See [CURRENT_STATE.md](CURRENT_STATE.md) and the
|
||||
[production inventory](../../infra/inventory/production.yml).
|
||||
|
||||
The Compose file is a repository baseline, not proof of deployment. It still
|
||||
builds images locally; immutable registry images and automated deploy are not
|
||||
configured. No migration framework exists. The application uses SQLite and
|
||||
OpenSearch; SMTP registration/recovery is not implemented.
|
||||
|
||||
## Repository paths and target host paths
|
||||
|
||||
- Compose and Dockerfiles: `deploy/production/`.
|
||||
- Host checkout target: `/opt/akyldash/current`.
|
||||
- Environment configuration: `/etc/akyldash/production.env`.
|
||||
- Secret file: `/etc/akyldash/review_secret`, mode `0600`.
|
||||
- Application data: `/var/lib/akyldash/data`.
|
||||
- Intended backups: `/var/backups/akyldash`.
|
||||
|
||||
All host paths above are targets; none existed at audit time. Root bootstrap is
|
||||
prepared at [bootstrap-ubuntu-24.04.sh](../../infra/bootstrap/bootstrap-ubuntu-24.04.sh).
|
||||
It does not edit SSH or firewall configuration, create credentials, deploy the
|
||||
application, or grant `codex` Docker/root access.
|
||||
|
||||
## Network and operations
|
||||
|
||||
Target public TCP listeners are SSH `22` and HTTPS proxy `80`/`443`. Backend
|
||||
`8080` must remain loopback-only and OpenSearch `9200` private to Compose. The
|
||||
firewall state could not be independently verified because the SSH account has
|
||||
no sudo; do not change it without owner approval. DNS, HTTPS, Cloudflare origin
|
||||
mode, and production health have not been verified.
|
||||
|
||||
Use [DEPLOYMENT.md](DEPLOYMENT.md), [BACKUP.md](BACKUP.md),
|
||||
[ROLLBACK.md](ROLLBACK.md), and [RECOVERY.md](RECOVERY.md) for operational
|
||||
limits and procedures. Configuration changes must go through Git and review;
|
||||
do not reconcile a Git/server difference by overwriting the VPS automatically.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Recovery on a clean VPS
|
||||
|
||||
## Scope and safety
|
||||
|
||||
The bootstrap below prepares Ubuntu packages and directories only. It is not a
|
||||
complete recovery: production secrets, data, reverse proxy, TLS, registry
|
||||
credentials, backup restore, and deploy-helper still require owner-controlled
|
||||
setup. Run only on the new recovery host after verifying its identity. Never
|
||||
run recovery commands against the existing production by accident.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. Provision a clean Ubuntu Server 24.04 host and confirm hostname, IP, console
|
||||
access, and a tested owner recovery path.
|
||||
2. Create the SSH-authorized `codex` account without sudo or Docker group access.
|
||||
Preserve the host's existing root SSH and firewall policy until an owner has
|
||||
reviewed the change.
|
||||
3. Check out the approved Git revision and run
|
||||
`sudo bash infra/bootstrap/bootstrap-ubuntu-24.04.sh`. It installs Docker
|
||||
Engine, Compose, Git, Docker log limits, and target directories.
|
||||
4. Install the reviewed reverse proxy and its HTTPS configuration after DNS
|
||||
points to the recovery host. Verify Cloudflare Full (strict) end to end.
|
||||
5. Provide production configuration and secret files through the owner's
|
||||
protected channel; never commit or paste secret values into chat.
|
||||
6. Restore the latest verified backup to the new host. Validate SQLite
|
||||
databases, corpus checksums, and the OpenSearch snapshot/index before serving
|
||||
traffic.
|
||||
7. Start the approved image digest, verify container health and
|
||||
`https://akyldash.kg/review`, then record actual versions and checks in
|
||||
[CURRENT_STATE.md](CURRENT_STATE.md).
|
||||
|
||||
Do not repoint DNS or retire the old host until the owner verifies recovery and
|
||||
approves the cutover. The restore and cutover have not been practiced yet.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Rollback
|
||||
|
||||
## Current status
|
||||
|
||||
There is no production release, pinned image digest, backup, or deploy-helper
|
||||
to roll back. Automatic rollback is not configured.
|
||||
|
||||
After deployment automation is reviewed, rollback may switch the backend to a
|
||||
previous immutable image digest only after health checks fail and only when the
|
||||
data format remains compatible. This application currently has no formal
|
||||
migration framework, but its SQLite files and OpenSearch indexes are persistent;
|
||||
compatibility must be checked for each change. If data compatibility is
|
||||
uncertain, stop and report rather than restoring or deleting production data.
|
||||
|
||||
Any data restore requires the owner, a verified pre-change backup, and a
|
||||
maintenance window. Preserve the failed release and its backup until recovery
|
||||
is confirmed.
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
- Telegram-бот: `0.2.2`
|
||||
- Telegram-бот на Synology: `0.2.1`
|
||||
- Backend: `0.11.2`
|
||||
- Backend: `0.11.3`
|
||||
- Frontend: `0.1.2` — поиск, лаборатория релевантности, аккаунты
|
||||
|
||||
## Краткий итог
|
||||
@@ -281,4 +281,4 @@
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.2
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2
|
||||
@@ -262,4 +262,4 @@ runtime-зависимостями frontend. Регистрация в стор
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.1
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2
|
||||
@@ -44,4 +44,4 @@
|
||||
|
||||
---
|
||||
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.2 · Frontend v0.1.2
|
||||
Акылдаш · Telegram-бот v0.2.2 · Backend v0.11.3 · Frontend v0.1.2
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "Run this script as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. /etc/os-release
|
||||
if [[ ${ID:-} != ubuntu || ${VERSION_ID:-} != 24.04 ]]; then
|
||||
echo "Expected Ubuntu 24.04; found ${PRETTY_NAME:-unknown}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! id codex >/dev/null 2>&1; then
|
||||
echo "Create the SSH-authorized codex account before running bootstrap." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -e /etc/docker/daemon.json ]] && ! python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); assert d.get("log-driver") == "local" and d.get("log-opts",{}).get("max-size") == "10m" and d.get("log-opts",{}).get("max-file") == "3"' /etc/docker/daemon.json; then
|
||||
echo "/etc/docker/daemon.json exists with different log settings; review it manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 0755 /etc/docker
|
||||
if [[ ! -e /etc/docker/daemon.json ]]; then
|
||||
cat > /etc/docker/daemon.json <<'EOF'
|
||||
{
|
||||
"log-driver": "local",
|
||||
"log-opts": {"max-size": "10m", "max-file": "3"}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl git
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' \
|
||||
"$(dpkg --print-architecture)" "$VERSION_CODENAME" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
|
||||
install -d -o root -g root -m 0755 /opt/akyldash
|
||||
install -d -o root -g root -m 0700 /etc/akyldash
|
||||
install -d -o root -g root -m 0750 /var/lib/akyldash/data
|
||||
install -d -o root -g root -m 0700 /var/backups/akyldash
|
||||
systemctl enable --now docker
|
||||
docker compose version
|
||||
echo "Bootstrap complete. codex was not added to sudo or the docker group; SSH and firewall settings were not changed."
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ${1:-} != --remote ]]; then
|
||||
ssh_alias=${1:-akyldash_vps}
|
||||
exec ssh -F "${HOME}/.ssh/config" -o BatchMode=yes "$ssh_alias" 'sudo -n bash -s -- --remote' < "$0"
|
||||
fi
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "Run the remote check as root; it is read-only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for package in git docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; do
|
||||
if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -qx 'install ok installed'; then
|
||||
printf 'PASS package %s\n' "$package"
|
||||
else
|
||||
printf 'FAIL package %s missing\n' "$package"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if systemctl is-active --quiet docker; then
|
||||
echo 'PASS Docker service active'
|
||||
else
|
||||
echo 'FAIL Docker service inactive'
|
||||
failed=1
|
||||
fi
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
echo 'PASS Docker Compose responds'
|
||||
else
|
||||
echo 'FAIL Docker Compose unavailable'
|
||||
failed=1
|
||||
fi
|
||||
|
||||
for path in /opt/akyldash /etc/akyldash /var/lib/akyldash/data /var/backups/akyldash; do
|
||||
if [[ -d $path ]]; then
|
||||
printf 'PASS directory %s\n' "$path"
|
||||
else
|
||||
printf 'FAIL directory %s missing\n' "$path"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
for path in /etc/akyldash /var/backups/akyldash; do
|
||||
if [[ -d $path && $(stat -c %a "$path") == 700 ]]; then
|
||||
printf 'PASS permissions %s mode 0700\n' "$path"
|
||||
elif [[ -d $path ]]; then
|
||||
printf 'FAIL permissions %s mode %s, expected 0700\n' "$path" "$(stat -c %a "$path")"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if ufw status | grep -q '^Status: active$'; then
|
||||
echo 'PASS UFW active'
|
||||
if ! ufw status verbose | grep -q '^Default: deny (incoming)'; then
|
||||
echo 'FAIL UFW default incoming policy is not deny'
|
||||
failed=1
|
||||
fi
|
||||
while read -r rule action direction; do
|
||||
[[ $action == ALLOW && $direction == IN ]] || continue
|
||||
ports=${rule//\/tcp/}
|
||||
ports=${ports//\/udp/}
|
||||
IFS=, read -ra allowed <<< "$ports"
|
||||
for port in "${allowed[@]}"; do
|
||||
case "$port" in
|
||||
22|80|443) ;;
|
||||
*) printf 'FAIL unexpected UFW allow rule %s\n' "$rule"; failed=1 ;;
|
||||
esac
|
||||
done
|
||||
done < <(ufw status | awk 'NR > 3 && NF >= 3 {print $1, $2, $3}')
|
||||
else
|
||||
echo 'FAIL UFW inactive or unavailable'
|
||||
failed=1
|
||||
fi
|
||||
|
||||
tcp_listeners=$(ss -H -lnt | awk '{print $4}')
|
||||
for endpoint in $tcp_listeners; do
|
||||
address=${endpoint%:*}
|
||||
port=${endpoint##*:}
|
||||
case "$address" in 127.*|::1|\[::1\]) continue ;; esac
|
||||
case "$port" in 22|80|443) ;; *) printf 'FAIL unexpected non-loopback TCP listener %s\n' "$endpoint"; failed=1 ;; esac
|
||||
done
|
||||
printf 'INFO non-loopback TCP listeners: %s\n' "${tcp_listeners//$'\n'/,}"
|
||||
|
||||
udp_listeners=$(ss -H -lnu | awk '{print $5}')
|
||||
for endpoint in $udp_listeners; do
|
||||
address=${endpoint%:*}
|
||||
case "$address" in 127.*|::1|\[::1\]) continue ;; esac
|
||||
printf 'FAIL unexpected non-loopback UDP listener %s\n' "$endpoint"
|
||||
failed=1
|
||||
done
|
||||
|
||||
if [[ -x /usr/local/sbin/akyldash-deploy ]]; then
|
||||
echo 'PASS restricted deploy helper present'
|
||||
else
|
||||
echo 'FAIL restricted deploy helper missing'
|
||||
failed=1
|
||||
fi
|
||||
if [[ -f /opt/akyldash/deployed-image.txt ]]; then
|
||||
expected=$(cat /opt/akyldash/deployed-image.txt)
|
||||
container=$(docker ps -aq --filter label=com.docker.compose.project=akyldash --filter label=com.docker.compose.service=backend | head -n1)
|
||||
actual=$(docker inspect --format '{{.Config.Image}}' "$container" 2>/dev/null || true)
|
||||
if [[ $expected == *@sha256:* && $actual == "$expected" ]]; then
|
||||
printf 'PASS running backend image matches %s\n' "$expected"
|
||||
else
|
||||
echo 'FAIL running backend image does not match recorded digest'
|
||||
failed=1
|
||||
fi
|
||||
else
|
||||
echo 'FAIL deployed image digest record missing'
|
||||
failed=1
|
||||
fi
|
||||
exit "$failed"
|
||||
@@ -0,0 +1,72 @@
|
||||
project: akyldash
|
||||
environment: production
|
||||
ssh:
|
||||
alias: akyldash_vps
|
||||
host: 206.223.249.118
|
||||
port: 22
|
||||
user: codex
|
||||
identity_file: ~/.ssh/akyldash_codex
|
||||
domain: akyldash.kg
|
||||
facts:
|
||||
hostname: avprogs.hlab.kz
|
||||
os: Ubuntu Server 24.04.5 LTS
|
||||
kernel: 6.8.0-142-generic
|
||||
cpu_vcpus: 4
|
||||
memory_gib: 7.7
|
||||
swap_gib: 3.8
|
||||
disk_gib: 99
|
||||
disk_free_gib: 93
|
||||
network_interface: ens3
|
||||
ipv4: 206.223.249.118/32
|
||||
user_groups: [codex, users]
|
||||
sudo_noninteractive: false
|
||||
installed:
|
||||
- OpenSSH server
|
||||
- systemd-resolved
|
||||
- unattended-upgrades
|
||||
absent:
|
||||
- Docker Engine
|
||||
- Docker Compose plugin
|
||||
- Git
|
||||
- reverse proxy
|
||||
access_audit_date: 2026-10-11
|
||||
target:
|
||||
compose_file_in_repository: deploy/production/compose.yaml
|
||||
compose_file_on_host: /opt/akyldash/current/deploy/production/compose.yaml
|
||||
production_env_file: /etc/akyldash/production.env
|
||||
secrets_directory: /etc/akyldash
|
||||
data_directory: /var/lib/akyldash/data
|
||||
backup_directory: /var/backups/akyldash
|
||||
services:
|
||||
backend:
|
||||
container: compose project service backend
|
||||
image: built from deploy/production/Dockerfile.backend; digest deployment not configured
|
||||
host_bind: 127.0.0.1:8080
|
||||
healthcheck: HTTP GET /review on container port 8080
|
||||
persistent_data: /var/lib/akyldash/data (bind mount)
|
||||
opensearch:
|
||||
container: compose project service opensearch
|
||||
image: opensearchproject/opensearch:3.7.0 plus analysis-icu plugin
|
||||
port: 9200 on private Compose network only
|
||||
persistent_data: named Compose volume opensearch-data
|
||||
reverse_proxy:
|
||||
target: Caddy or another reviewed HTTPS proxy on TCP 80/443
|
||||
status: not installed or configured
|
||||
external_ports_target: [22, 80, 443]
|
||||
compose_network: default private Compose network
|
||||
secrets:
|
||||
- REVIEW_SECRET (host file mounted as /run/secrets/review_secret)
|
||||
- Gitea credentials from environment-backed gitea-api helper
|
||||
- SSH key at ~/.ssh/akyldash_codex
|
||||
checks:
|
||||
- infra/bootstrap/verify.sh akyldash_vps
|
||||
- docker compose -f deploy/production/compose.yaml ps
|
||||
- curl -fsS https://akyldash.kg/review
|
||||
backup_path: /var/backups/akyldash (target; mechanism not configured)
|
||||
versions:
|
||||
application:
|
||||
backend: 0.11.3
|
||||
frontend: 0.1.2
|
||||
telegram_bot: 0.2.2
|
||||
infrastructure: unknown; no production install or release exists
|
||||
docker_engine: not installed
|
||||
Reference in new issue
Block a user