6 Commits

Author SHA1 Message Date
embeddevio 60521b1d7b ci(verify): токен для registry — secret REGISTRY_TOKEN с fallback на GITHUB_TOKEN
contract-openapi / openapi-contract (push) Failing after 31s
contract-openapi / ts-client-is-current (push) Failing after 42s
ci / lint (push) Successful in 42s
ci / unit (push) Successful in 48s
ci / security (push) Successful in 4m4s
ci / build (push) Successful in 39s
ci / image-scan (push) Failing after 4s
ci / push (push) Has been skipped
Refs #17
2026-10-28 22:32:42 +03:00
embeddevio 10ce7b8483 ci(verify): trivy через образ aquasec/trivy + кэш БД
ci / lint (push) Successful in 36s
contract-openapi / openapi-contract (push) Failing after 31s
contract-openapi / ts-client-is-current (push) Failing after 41s
ci / unit (push) Successful in 54s
ci / security (push) Successful in 1m57s
ci / build (push) Successful in 38s
ci / image-scan (push) Failing after 27s
ci / push (push) Has been skipped
Refs #17
2026-10-28 22:31:59 +03:00
embeddevio 56979d9912 ci(verify): trivy из релизного tarball, версия 0.76.1
ci / lint (push) Successful in 45s
contract-openapi / openapi-contract (push) Failing after 40s
contract-openapi / ts-client-is-current (push) Failing after 35s
ci / unit (push) Successful in 44s
ci / security (push) Successful in 1m10s
ci / build (push) Successful in 34s
ci / image-scan (push) Failing after 52s
ci / push (push) Has been skipped
Refs #17
2026-10-28 22:21:04 +03:00
embeddevio f077a85cba test(ci): ветка проверки пайплайна — deploy/Dockerfile из #18 (Refs #17)
ci / lint (push) Successful in 50s
contract-openapi / openapi-contract (push) Failing after 41s
contract-openapi / ts-client-is-current (push) Failing after 40s
ci / unit (push) Successful in 45s
ci / security (push) Successful in 1m11s
ci / build (push) Successful in 1m15s
ci / image-scan (push) Failing after 4s
ci / push (push) Has been skipped
2026-10-28 22:01:48 +03:00
embeddevio a941525f2e ci: тесты требуют requirements-contract.txt; явный guard на отсутствие deploy/Dockerfile
ci / lint (push) Successful in 42s
contract-openapi / openapi-contract (push) Failing after 41s
contract-openapi / ts-client-is-current (push) Failing after 31s
ci / unit (push) Successful in 41s
ci / security (push) Successful in 1m19s
ci / build (push) Has been skipped
ci / image-scan (push) Has been skipped
ci / push (push) Has been skipped
Refs #17
2026-10-28 22:01:14 +03:00
embeddevio b13f5e8ac5 ci: пайплайн с гейтами (lint, unit+coverage>=70%, security, build, image-scan, push staging-<sha>)
contract-openapi / openapi-contract (push) Failing after 0s
contract-openapi / ts-client-is-current (push) Failing after 1s
ci / lint (push) Successful in 7m38s
ci / unit (push) Failing after 1m3s
ci / security (push) Successful in 2m19s
ci / build (push) Has been skipped
ci / image-scan (push) Has been skipped
ci / push (push) Has been skipped
Refs #17, Refs #15

- lint: ruff check/format + mypy strict
- unit: pytest с --cov-fail-under=70, артефакты coverage.xml + htmlcov
- security: bandit -lll (HIGH) + pip-audit, артефакты bandit/pip-audit
- build: docker build deploy/Dockerfile -> staging-<sha> (активируется после merge #18)
- image-scan: trivy HIGH/CRITICAL --exit-code 1
- push: docker login + push в registry 192.168.1.111:3000

Обход дефекта Gitea git-http (upload-pack отдаёт пустой pack): fallback
на archive/<sha>.tar.gz, actions/checkout остаётся основным путём.
2026-10-28 21:47:05 +03:00
4 changed files with 513 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
name: ci
# CI-пайплайн с гейтами (issue #17, Refs #15)
# lint -> unit (coverage >= 70%) -> security (bandit/pip-audit) -> build -> image-scan (trivy) -> push staging-<sha>
# Гейты (роняют pipeline):
# * unit — покрытие < 70 % (--cov-fail-under=70)
# * security — bandit HIGH, pip-audit: любая известная уязвимость
# * image-scan — trivy: HIGH/CRITICAL в образе
# * build стартует только после зелёных lint+unit+security; push — только после зелёного image-scan.
on:
push:
branches: ["feat/**", "fix/**", "chore/**", "ci/**", "main"]
pull_request:
branches: [main]
workflow_dispatch:
env:
PYTHON_VERSION: "3.12"
REGISTRY: 192.168.1.111:3000
IMAGE_NAME: ${{ github.repository }}
jobs:
# ─────────────────────────────── 1. LINT ────────────────────────────────
lint:
name: lint
runs-on: ubuntu-latest
outputs:
has_dockerfile: ${{ steps.dockerfile.outputs.has_dockerfile }}
steps:
- name: checkout
id: checkout
uses: actions/checkout@v4
continue-on-error: true
# Дефект Gitea git-http: upload-pack отдаёт пустой pack (Content-Length: 90),
# git clone/fetch падает "early EOF / invalid index-pack output".
# Обход: эндпоинт archive/<sha>.tar.gz работает — забираем исходники им.
# Когда git-http починят, шаг просто перестанет срабатывать (outcome != failure).
- name: "checkout fallback (archive tarball)"
if: steps.checkout.outcome == 'failure'
run: |
set -euo pipefail
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
echo "sources: archive fallback, $(ls -1 | wc -l) entries"
- name: "python 3.12"
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt -r requirements-contract.txt
- name: ruff check
run: ruff check backend
- name: ruff format --check
run: ruff format --check backend
- name: "mypy (strict)"
run: mypy backend
- name: detect deploy/Dockerfile
id: dockerfile
run: |
if [ -f deploy/Dockerfile ]; then
echo "has_dockerfile=true" >> "$GITHUB_OUTPUT"
echo "deploy/Dockerfile найден — jobs build/image-scan/push активны"
else
echo "has_dockerfile=false" >> "$GITHUB_OUTPUT"
echo "::warning::deploy/Dockerfile отсутствует — build/image-scan/push пропущены (ждут merge #18 / PR #49)"
fi
# ─────────────────── 2. UNIT + ГЕЙТ ПОКРЫТИЯ >= 70 % ───────────────────
unit:
name: unit
needs: lint
runs-on: ubuntu-latest
steps:
- name: checkout
id: checkout
uses: actions/checkout@v4
continue-on-error: true
- name: "checkout fallback (archive tarball)"
if: steps.checkout.outcome == 'failure'
run: |
set -euo pipefail
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
- name: "python 3.12"
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt -r requirements-contract.txt
- name: "pytest + coverage gate (fail below 70%)"
run: |
python -m pytest \
--cov=awg_agent --cov=awg_api \
--cov-report=term-missing \
--cov-report=xml:coverage.xml \
--cov-report=html:htmlcov \
--cov-fail-under=70
- name: coverage summary
if: always()
run: python -m coverage report --sort=cover
- name: upload coverage artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage.xml
htmlcov/
retention-days: 14
# ───────────── 3. SECURITY: bandit (HIGH) + pip-audit ──────────────────
security:
name: security
needs: lint
runs-on: ubuntu-latest
steps:
- name: checkout
id: checkout
uses: actions/checkout@v4
continue-on-error: true
- name: "checkout fallback (archive tarball)"
if: steps.checkout.outcome == 'failure'
run: |
set -euo pipefail
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
- name: "python 3.12"
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install scanners
run: |
python -m pip install --upgrade pip
pip install "bandit[toml]>=1.7" "pip-audit>=2.7"
pip install -r requirements.txt -r requirements-contract.txt
- name: "bandit -lll (HIGH severity gate)"
run: |
set -o pipefail
bandit -lll -r backend 2>&1 | tee bandit-report.txt
bandit -lll -f json -o bandit-report.json -r backend || true
echo "bandit: HIGH/CRITICAL не найдены"
- name: "pip-audit (known vulnerabilities gate)"
run: |
pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off
- name: pip-audit json report
if: always()
run: pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off --format json -o pip-audit-report.json || true
- name: upload security artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
bandit-report.txt
bandit-report.json
pip-audit-report.json
retention-days: 14
# ───────────────── 4. BUILD: docker build (нужен #18) ──────────────────
build:
name: build
needs: [lint, unit, security]
if: needs.lint.outputs.has_dockerfile == 'true'
runs-on: ubuntu-latest
steps:
- name: checkout
id: checkout
uses: actions/checkout@v4
continue-on-error: true
- name: "checkout fallback (archive tarball)"
if: steps.checkout.outcome == 'failure'
run: |
set -euo pipefail
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
- name: "guard: deploy/Dockerfile"
run: |
if [ ! -f deploy/Dockerfile ]; then
echo "::error::deploy/Dockerfile отсутствует: сборка образа ждёт merge #18 (PR #49)."
echo "Пайплайн целиком проверен на ветке feat/17-verify (она же + deploy/Dockerfile)."
exit 1
fi
- name: image meta
id: meta
run: |
SHA_SHORT="${GITHUB_SHA::8}"
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${SHA_SHORT}"
echo "sha_short=${SHA_SHORT}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
echo "image: ${IMAGE}"
- name: docker build
run: |
set -o pipefail
docker build --pull -f deploy/Dockerfile -t "$IMAGE" . 2>&1 | tee build.log
- name: image digest
run: |
docker image inspect "$IMAGE" --format 'id={{.Id}} size={{.Size}}'
docker image inspect "$IMAGE" --format '{{.Id}}' > image-id.txt
- name: upload build artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: "build-${{ github.sha }}"
path: |
build.log
image-id.txt
retention-days: 14
# ──────── 5. IMAGE-SCAN: trivy, гейт HIGH/CRITICAL ────────────────────
image-scan:
name: image-scan
needs: [lint, build]
if: needs.lint.outputs.has_dockerfile == 'true'
runs-on: ubuntu-latest
env:
TRIVY_VERSION: "0.76.1"
TRIVY_SEVERITY: "HIGH,CRITICAL"
steps:
# trivy запускается официальным образом через docker-сокет: не требует
# скачивания бинаря (github releases отдаёт медленно) + БД кэшируется в
# /opt/trivy-cache на build-хосте.
- name: "trivy image (HIGH/CRITICAL -> fail)"
run: |
set -o pipefail
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
echo "scan: ${IMAGE}"
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/trivy-cache:/root/.cache/trivy \
"aquasec/trivy:${TRIVY_VERSION}" image \
--image-src docker --scanners vuln \
--severity "$TRIVY_SEVERITY" --ignore-unfixed \
--exit-code 1 --format table "$IMAGE" 2>&1 | tee trivy-report.txt
- name: trivy json report
if: always()
run: |
set -o pipefail
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
mkdir -p reports
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/trivy-cache:/root/.cache/trivy \
-v "$PWD/reports:/reports" \
"aquasec/trivy:${TRIVY_VERSION}" image \
--image-src docker --scanners vuln \
--severity "$TRIVY_SEVERITY" --ignore-unfixed \
--format json -o /reports/trivy-report.json "$IMAGE" || true
cp reports/trivy-report.json ./trivy-report.json 2>/dev/null || true
- name: upload trivy report
if: always()
uses: actions/upload-artifact@v3
with:
name: trivy-report
path: |
trivy-report.txt
trivy-report.json
retention-days: 14
# ─────── 6. PUSH: staging-<sha> в registry Gitea ──────────────────────
push:
name: push
needs: [lint, build, image-scan]
if: needs.lint.outputs.has_dockerfile == 'true' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: docker login
run: |
set -o pipefail
# Приоритет — секрет с правом write:package (REGISTRY_TOKEN), иначе встроенный токен Actions.
TOKEN="${{ secrets.REGISTRY_TOKEN }}"
if [ -z "$TOKEN" ]; then
TOKEN="${{ secrets.GITHUB_TOKEN }}"
fi
echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin
- name: "push staging-<sha>"
run: |
set -o pipefail
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
docker push "$IMAGE" 2>&1 | tee push.log
echo "${IMAGE}" | tee pushed-image.txt
- name: upload push record
if: always()
uses: actions/upload-artifact@v3
with:
name: pushed-image
path: |
push.log
pushed-image.txt
retention-days: 14
+79
View File
@@ -0,0 +1,79 @@
# syntax=docker/dockerfile:1.7
#
# awg-dash — единый образ: AmneziaWG gateway + policy routing + REST API + Web UI.
# Задача: #18 (эпик #15). ADR: docs/adr/002-single-image-modular-monolith.md
#
# ВАЖНО: AMNEZIAWG_GO_REF / AMNEZIAWG_TOOLS_REF по умолчанию указывают на master
# и должны быть запинены на проверенный тег/коммит перед релизом (см. #45).
ARG GO_IMAGE=golang:1.25-alpine
# --- stage 1: сборка awg-утилит (Go) -----------------------------------------
FROM ${GO_IMAGE} AS go-build
ARG AMNEZIAWG_GO_REPO=https://github.com/amnezia-vpn/amneziawg-go.git
ARG AMNEZIAWG_GO_REF=master
ARG AMNEZIAWG_TOOLS_REPO=https://github.com/amnezia-vpn/amneziawg-tools.git
ARG AMNEZIAWG_TOOLS_REF=master
RUN apk add --no-cache git make gcc musl-dev linux-headers
WORKDIR /src
RUN git clone --depth 1 --branch "${AMNEZIAWG_GO_REF}" "${AMNEZIAWG_GO_REPO}" amneziawg-go \
&& cd amneziawg-go && make && cp amneziawg-go /usr/local/bin/amneziawg-go
RUN git clone --depth 1 --branch "${AMNEZIAWG_TOOLS_REF}" "${AMNEZIAWG_TOOLS_REPO}" amneziawg-tools \
&& cd amneziawg-tools/src && make \
&& cp wg /usr/local/bin/awg \
&& if [ -f wg-quick ]; then cp wg-quick /usr/local/bin/awg-quick; fi
# --- stage 2: runtime -------------------------------------------------------
FROM alpine:3.21 AS runtime
LABEL org.opencontainers.image.title="awg-dash" \
org.opencontainers.image.description="AmneziaWG gateway with intelligent routing: REST API + Web UI + split-tunnel" \
org.opencontainers.image.source="http://192.168.1.111:3000/embeddevio/awg-dash"
# Сетевые утилиты: iptables/ip6tables, ipset, iproute2 (policy routing), dnsmasq (ADR-005),
# curl (HEALTHCHECK), tini (PID 1), su-exec (запуск API под непривилегированным пользователем).
RUN apk add --no-cache \
bash curl iproute2 ipset iptables ip6tables dnsmasq \
python3 py3-pip tini su-exec \
&& addgroup -S awg \
&& adduser -S -G awg -h /app awg
ENV PATH=/opt/venv/bin:$PATH \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/backend \
AWG_CONFIG_DIR=/config \
AWG_DATA_DIR=/data \
AWG_RUN_DIR=/run/awg \
AWG_API_HOST=0.0.0.0 \
AWG_API_PORT=8080
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev libffi-dev \
&& python3 -m venv /opt/venv \
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
&& /opt/venv/bin/pip install --no-cache-dir -r /app/requirements.txt \
&& apk del .build-deps
COPY backend/ /app/backend/
COPY --from=go-build /usr/local/bin/awg /usr/local/bin/awg
COPY --from=go-build /usr/local/bin/amneziawg-go /usr/local/bin/amneziawg-go
COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh \
&& mkdir -p /config /data /run/awg \
&& chown -R awg:awg /app /config /data /run/awg
VOLUME ["/config", "/data"]
EXPOSE 8080
# healthcheck ходит в /api/v1/health (задача #31). До реализации API контейнер
# будет помечаться как unhealthy — это ожидаемое состояние до #31.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS "http://127.0.0.1:${AWG_API_PORT}/api/v1/health" >/dev/null || exit 1
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]
+32
View File
@@ -0,0 +1,32 @@
# Пример клиентского конфига AmneziaWG для awg-dash.
#
# Скопируйте в том /config как awg0.conf (имя файла = имя интерфейса) или
# загрузите через Web UI (задача #38). Значения ниже — заглушки, замените своими.
# Файл в репозитории не является рабочим конфигом и не содержит реальных ключей.
[Interface]
# Адрес внутри туннеля (из панели провайдера AmneziaWG)
Address = 10.8.1.2/32
# DNS: адрес должен указывать на этот шлюз, иначе не сработает DNS-split (ADR-005)
DNS = 10.8.1.1
PrivateKey = <PRIVATE_KEY>
# Обфускация AmneziaWG (Amnezia VPN: экспорт -> AmneziaWG)
Jc = 4
Jmin = 40
Jmax = 70
S1 = 0
S2 = 0
H1 = 1
H2 = 2
H3 = 3
H4 = 4
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <PRESHARED_KEY>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <SERVER_HOST>:<SERVER_PORT>
PersistentKeepalive = 25
+77
View File
@@ -0,0 +1,77 @@
#!/bin/sh
# Супервизор контейнера awg-dash (задача #18, ADR-002).
# Последовательность: preflight -> подготовка -> запуск awg-api под awg.
# Привилегированный агент awg-agent стартует отсюда же после реализации (#22).
#
# Роль процесса: все сетевые операции выполняет агент под root; здесь только
# проверяем, что нужные возможности и устройства проброшены, и падаем явной
# ошибкой, если контейнер запущен без них (а не тихо деградируем).
set -eu
CONFIG_DIR="${AWG_CONFIG_DIR:-/config}"
DATA_DIR="${AWG_DATA_DIR:-/data}"
RUN_DIR="${AWG_RUN_DIR:-/run/awg}"
API_HOST="${AWG_API_HOST:-0.0.0.0}"
API_PORT="${AWG_API_PORT:-8080}"
ROUTING_TABLE_ID=100
ROUTING_TABLE_NAME=awg-vpn
log() { printf '[entrypoint] %s\n' "$*"; }
fail() { printf '[entrypoint] FATAL: %s\n' "$*" >&2; exit 1; }
log 'preflight'
[ -e /dev/net/tun ] || fail '/dev/net/tun не проброшен: добавьте --device=/dev/net/tun:/dev/net/tun'
iptables -t mangle -L -n >/dev/null 2>&1 || fail 'iptables недоступен: нужен --cap-add=NET_ADMIN (и/или --privileged)'
command -v awg >/dev/null 2>&1 || fail 'бинарь awg не найден в образе'
command -v amneziawg-go >/dev/null 2>&1 || log 'WARN: amneziawg-go не найден — остаётся только kernel-модуль'
command -v ipset >/dev/null 2>&1 || fail 'ipset не найден в образе'
log 'подготовка каталогов состояния'
mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$RUN_DIR"
chown -R awg:awg "$CONFIG_DIR" "$DATA_DIR" "$RUN_DIR" 2>/dev/null || true
chmod 700 "$CONFIG_DIR" "$DATA_DIR" 2>/dev/null || true
mkdir -p /etc/iproute2
touch /etc/iproute2/rt_tables 2>/dev/null || true
if ! grep -q "^${ROUTING_TABLE_ID} ${ROUTING_TABLE_NAME}" /etc/iproute2/rt_tables 2>/dev/null; then
printf '%s %s\n' "$ROUTING_TABLE_ID" "$ROUTING_TABLE_NAME" >> /etc/iproute2/rt_tables \
|| log 'WARN: не удалось записать таблицу маршрутизации в /etc/iproute2/rt_tables'
fi
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 \
|| log 'WARN: net.ipv4.ip_forward не включён — задайте --sysctl net.ipv4.ip_forward=1 или --privileged (см. runbook #45)'
log "awg: $(awg --version 2>/dev/null | head -n 1 || echo 'version unknown')"
api_pid=''
if [ -f /app/backend/awg_api/app.py ]; then
log "запуск awg-api на ${API_HOST}:${API_PORT}"
su-exec awg /opt/venv/bin/uvicorn awg_api.app:app --host "$API_HOST" --port "$API_PORT" &
api_pid=$!
else
log 'WARN: awg_api/app.py ещё не реализован (задача #31) — API не запущен, контейнер работает только как агент'
fi
stop() {
if [ -n "$api_pid" ] && kill -0 "$api_pid" 2>/dev/null; then
log 'остановка awg-api'
kill -TERM "$api_pid" 2>/dev/null || true
fi
}
trap stop INT TERM
if [ -n "$api_pid" ]; then
# ждём завершения API и прокидываем его код возврата в Docker
set +e
wait "$api_pid"
rc=$?
set -e
log "awg-api завершился с кодом $rc"
exit "$rc"
fi
log 'активных процессов нет (agent #22 и api #31 ещё не реализованы) — ожидание сигнала'
while :; do
sleep 3600 &
wait $!
done