Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e97f260113 | |||
| 6577c1fb39 | |||
| c7446a5d4c |
@@ -4,3 +4,6 @@ Expires: 2027-12-31T00:00:00.000Z
|
|||||||
Preferred-Languages: ru, en
|
Preferred-Languages: ru, en
|
||||||
Canonical: https://gl.mhspx.su/.well-known/security.txt
|
Canonical: https://gl.mhspx.su/.well-known/security.txt
|
||||||
Policy: https://gl.mhspx.su/security
|
Policy: https://gl.mhspx.su/security
|
||||||
|
|
||||||
|
# Этот файл отдаётся по адресу /.well-known/security.txt: копия для сборки
|
||||||
|
# лежит в web/public/.well-known/security.txt и должна совпадать с этой.
|
||||||
|
|||||||
+6
-3
@@ -1,7 +1,10 @@
|
|||||||
|
# Образы пинятся по digest (AGENT.md 11.3): тег может быть перезаписан в
|
||||||
|
# реестре, digest — нет. Обновление: получить новый digest
|
||||||
|
# (`docker buildx imagetools inspect <образ>` или Docker Hub API) и заменить.
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# ---------- web ----------
|
# ---------- web ----------
|
||||||
FROM node:24-bookworm-slim AS web
|
FROM node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 AS web
|
||||||
WORKDIR /src/web
|
WORKDIR /src/web
|
||||||
COPY web/package.json web/package-lock.json ./
|
COPY web/package.json web/package-lock.json ./
|
||||||
RUN npm ci --no-audit --no-fund
|
RUN npm ci --no-audit --no-fund
|
||||||
@@ -9,7 +12,7 @@ COPY web/ ./
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ---------- go ----------
|
# ---------- go ----------
|
||||||
FROM golang:1.27-bookworm AS gobuild
|
FROM golang:1.27-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195 AS gobuild
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG COMMIT=none
|
ARG COMMIT=none
|
||||||
@@ -36,7 +39,7 @@ RUN CC=musl-gcc CGO_ENABLED=1 go build \
|
|||||||
# ---------- runtime ----------
|
# ---------- runtime ----------
|
||||||
# База alpine + статический бинарник: образ укладывается в бюджет §9.1
|
# База alpine + статический бинарник: образ укладывается в бюджет §9.1
|
||||||
# (<50 MB). Проверка: docs/DECISIONS.md, решение D-007.
|
# (<50 MB). Проверка: docs/DECISIONS.md, решение D-007.
|
||||||
FROM alpine:3.20 AS runtime
|
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
|
||||||
|
|
||||||
RUN addgroup -S -g 10001 glchat \
|
RUN addgroup -S -g 10001 glchat \
|
||||||
&& adduser -S -u 10001 -G glchat -h /app -s /sbin/nologin glchat \
|
&& adduser -S -u 10001 -G glchat -h /app -s /sbin/nologin glchat \
|
||||||
|
|||||||
@@ -142,18 +142,29 @@ generate: tools ## Проверить сгенерированные артеф
|
|||||||
else echo "==> sqlc: запросов пока нет, пропускаем"; fi
|
else echo "==> sqlc: запросов пока нет, пропускаем"; fi
|
||||||
|
|
||||||
.PHONY: security
|
.PHONY: security
|
||||||
security: tools ## AppSec: govulncheck, gitleaks, trivy (по возможности)
|
security: tools ## AppSec: govulncheck, gitleaks, npm audit, trivy (по возможности)
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@echo "==> govulncheck"
|
@echo "==> govulncheck"
|
||||||
@govulncheck ./cmd/... ./internal/... || { echo "govulncheck нашёл уязвимости"; exit 1; }
|
@govulncheck ./cmd/... ./internal/... || { echo "govulncheck нашёл уязвимости"; exit 1; }
|
||||||
@echo "==> gitleaks"
|
@echo "==> gitleaks"
|
||||||
@$(ROOT)/scripts/gitleaks-scan.sh
|
@$(ROOT)/scripts/gitleaks-scan.sh
|
||||||
|
@echo "==> npm audit (web, только production-зависимости)"
|
||||||
|
@cd $(WEB_DIR) && npm audit --omit=dev --audit-level=high || { echo "npm audit нашёл уязвимости"; exit 1; }
|
||||||
@echo "==> trivy filesystem scan"
|
@echo "==> trivy filesystem scan"
|
||||||
@if command -v trivy >/dev/null 2>&1; then \
|
@if command -v trivy >/dev/null 2>&1; then \
|
||||||
trivy fs --quiet --scanners vuln,secret,misconfig --exit-code 1 \
|
trivy fs --quiet --scanners vuln,secret,misconfig --exit-code 1 \
|
||||||
--skip-dirs .cache --skip-dirs .git --skip-dirs web/node_modules .; \
|
--skip-dirs .cache --skip-dirs .git --skip-dirs web/node_modules .; \
|
||||||
else echo "trivy not installed — skipped"; fi
|
else echo "trivy not installed — skipped"; fi
|
||||||
|
|
||||||
|
.PHONY: sbom
|
||||||
|
sbom: ## SBOM зависимостей в build/sbom.cdx.json (trivy, если установлен)
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@if command -v trivy >/dev/null 2>&1; then \
|
||||||
|
trivy fs --quiet --format cyclonedx --output $(BUILD_DIR)/sbom.cdx.json \
|
||||||
|
--skip-dirs .cache --skip-dirs web/node_modules . && \
|
||||||
|
echo "SBOM: $(BUILD_DIR)/sbom.cdx.json"; \
|
||||||
|
else echo "trivy не установлен — SBOM не собран (запустите на сервере сборки)"; fi
|
||||||
|
|
||||||
.PHONY: secrets-scan
|
.PHONY: secrets-scan
|
||||||
secrets-scan: tools ## Только проверка на утечки секретов
|
secrets-scan: tools ## Только проверка на утечки секретов
|
||||||
@$(ROOT)/scripts/gitleaks-scan.sh
|
@$(ROOT)/scripts/gitleaks-scan.sh
|
||||||
|
|||||||
+14
-2
@@ -9,6 +9,16 @@ ENV_FILE="${GLCHAT_ETC_DIR}/.env"
|
|||||||
|
|
||||||
COMPOSE="docker compose -f ${COMPOSE_FILE}"
|
COMPOSE="docker compose -f ${COMPOSE_FILE}"
|
||||||
|
|
||||||
|
# app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем
|
||||||
|
# режиме оно слушает WireGuard-адрес, и проверка localhost всегда падала бы.
|
||||||
|
app_check_url() {
|
||||||
|
local port="$1" host="${2:-}"
|
||||||
|
case "$host" in
|
||||||
|
0.0.0.0 | "" | "::") host="127.0.0.1" ;;
|
||||||
|
esac
|
||||||
|
printf 'http://%s:%s/readyz' "$host" "$port"
|
||||||
|
}
|
||||||
|
|
||||||
# env_get читает значение из .env инстанса (пусто, если ключа нет).
|
# env_get читает значение из .env инстанса (пусто, если ключа нет).
|
||||||
env_get() {
|
env_get() {
|
||||||
[ -f "$ENV_FILE" ] || return 0
|
[ -f "$ENV_FILE" ] || return 0
|
||||||
@@ -56,9 +66,11 @@ cmd_status() {
|
|||||||
echo "== compose =="
|
echo "== compose =="
|
||||||
$COMPOSE ps
|
$COMPOSE ps
|
||||||
echo
|
echo
|
||||||
meta="$(curl -fsS "http://127.0.0.1:8080/api/v1/meta" 2>/dev/null || true)"
|
local check_url
|
||||||
|
check_url="$(app_check_url "${APP_PORT:-8080}" "$(env_get GLCHAT_APP_BIND)")"
|
||||||
|
meta="$(curl -fsS "${check_url%/readyz}/api/v1/meta" 2>/dev/null || true)"
|
||||||
version="$(printf '%s' "$meta" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')"
|
version="$(printf '%s' "$meta" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')"
|
||||||
health="$(curl -fsS "http://127.0.0.1:8080/readyz" 2>/dev/null || echo '{"status":"unreachable"}')"
|
health="$(curl -fsS "${check_url}" 2>/dev/null || echo '{"status":"unreachable"}')"
|
||||||
echo "== app =="
|
echo "== app =="
|
||||||
echo "version: ${version:-unknown}"
|
echo "version: ${version:-unknown}"
|
||||||
echo "readyz: ${health}"
|
echo "readyz: ${health}"
|
||||||
|
|||||||
+4
-2
@@ -106,8 +106,10 @@ ENABLE_NFTABLES="${ENABLE_NFTABLES:-true}"
|
|||||||
ENABLE_SSH_HARDENING="${ENABLE_SSH_HARDENING:-true}"
|
ENABLE_SSH_HARDENING="${ENABLE_SSH_HARDENING:-true}"
|
||||||
APP_IMAGE="${APP_IMAGE:-glchat:local}"
|
APP_IMAGE="${APP_IMAGE:-glchat:local}"
|
||||||
IMAGE_TAG="${IMAGE_TAG:-v0.1.0}"
|
IMAGE_TAG="${IMAGE_TAG:-v0.1.0}"
|
||||||
CADDY_IMAGE_TAG="${CADDY_IMAGE_TAG:-2.10-alpine}"
|
# Тег + digest: реестр может перезаписать тег, digest фиксирует содержимое
|
||||||
LIVEKIT_IMAGE_TAG="${LIVEKIT_IMAGE_TAG:-v1.9.7}"
|
# (AGENT.md 11.3). Обновление — заменить digest после проверки образа.
|
||||||
|
CADDY_IMAGE_TAG="${CADDY_IMAGE_TAG:-2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d}"
|
||||||
|
LIVEKIT_IMAGE_TAG="${LIVEKIT_IMAGE_TAG:-v1.9.7@sha256:ee33f59dc8d2fe176db7f2ef74e1b3ec754f29b50b0ae519b3fa893f7b015d07}"
|
||||||
MAX_UPLOAD_MB="${MAX_UPLOAD_MB:-}"
|
MAX_UPLOAD_MB="${MAX_UPLOAD_MB:-}"
|
||||||
LOG_LEVEL="${LOG_LEVEL:-info}"
|
LOG_LEVEL="${LOG_LEVEL:-info}"
|
||||||
SKIP_TLS="${SKIP_TLS:-0}"
|
SKIP_TLS="${SKIP_TLS:-0}"
|
||||||
|
|||||||
+18
-1
@@ -8,6 +8,23 @@ ENV_FILE="${GLCHAT_ETC_DIR}/.env"
|
|||||||
COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml"
|
COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml"
|
||||||
|
|
||||||
log() { printf '[restore] %s\n' "$*"; }
|
log() { printf '[restore] %s\n' "$*"; }
|
||||||
|
|
||||||
|
# env_get читает значение из .env инстанса (пусто, если ключа нет).
|
||||||
|
env_get() {
|
||||||
|
[ -f "$ENV_FILE" ] || return 0
|
||||||
|
sed -n "s/^$1=//p" "$ENV_FILE" | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем
|
||||||
|
# режиме приложение слушает WireGuard-адрес, и проверка localhost не работает.
|
||||||
|
app_check_url() {
|
||||||
|
local port="$1" host="${2:-}"
|
||||||
|
case "$host" in
|
||||||
|
0.0.0.0 | "" | "::") host="127.0.0.1" ;;
|
||||||
|
esac
|
||||||
|
printf 'http://%s:%s/readyz' "$host" "$port"
|
||||||
|
}
|
||||||
|
|
||||||
die() {
|
die() {
|
||||||
printf '[restore] ошибка: %s\n' "$*" >&2
|
printf '[restore] ошибка: %s\n' "$*" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -79,7 +96,7 @@ log "запускаем приложение"
|
|||||||
(cd "$GLCHAT_ETC_DIR" && docker compose start app) || (cd "$GLCHAT_ETC_DIR" && docker compose up -d app)
|
(cd "$GLCHAT_ETC_DIR" && docker compose start app) || (cd "$GLCHAT_ETC_DIR" && docker compose up -d app)
|
||||||
|
|
||||||
for _ in $(seq 1 30); do
|
for _ in $(seq 1 30); do
|
||||||
if curl -fsS "http://127.0.0.1:${APP_PORT:-8080}/readyz" >/dev/null 2>&1; then
|
if curl -fsS "$(app_check_url "${APP_PORT:-8080}" "$(env_get GLCHAT_APP_BIND)")" >/dev/null 2>&1; then
|
||||||
log "приложение готово"
|
log "приложение готово"
|
||||||
GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/smoke.sh" || true
|
GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/smoke.sh" || true
|
||||||
log "восстановление завершено"
|
log "восстановление завершено"
|
||||||
|
|||||||
+15
-3
@@ -7,6 +7,16 @@ ENV_FILE="${GLCHAT_ETC_DIR}/.env"
|
|||||||
COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml"
|
COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml"
|
||||||
REPO_DIR_DEFAULT="${GLCHAT_REPO_DIR:-/opt/glchat-src}"
|
REPO_DIR_DEFAULT="${GLCHAT_REPO_DIR:-/opt/glchat-src}"
|
||||||
|
|
||||||
|
# app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем
|
||||||
|
# режиме оно слушает WireGuard-адрес, и проверка localhost всегда падала бы.
|
||||||
|
app_check_url() {
|
||||||
|
local port="$1" host="${2:-}"
|
||||||
|
case "$host" in
|
||||||
|
0.0.0.0 | "" | "::") host="127.0.0.1" ;;
|
||||||
|
esac
|
||||||
|
printf 'http://%s:%s/readyz' "$host" "$port"
|
||||||
|
}
|
||||||
|
|
||||||
log() { printf '[update] %s\n' "$*"; }
|
log() { printf '[update] %s\n' "$*"; }
|
||||||
die() {
|
die() {
|
||||||
printf '[update] ошибка: %s\n' "$*" >&2
|
printf '[update] ошибка: %s\n' "$*" >&2
|
||||||
@@ -72,16 +82,18 @@ fi
|
|||||||
log "применяем новую версию"
|
log "применяем новую версию"
|
||||||
(cd "$GLCHAT_ETC_DIR" && docker compose "${compose_args[@]}" up -d --remove-orphans) || die "не удалось поднять стек"
|
(cd "$GLCHAT_ETC_DIR" && docker compose "${compose_args[@]}" up -d --remove-orphans) || die "не удалось поднять стек"
|
||||||
|
|
||||||
log "ожидаем готовности (миграции выполняются на старте приложения)"
|
READY_URL="$(app_check_url "${APP_PORT:-8080}" "${GLCHAT_APP_BIND:-}")"
|
||||||
|
|
||||||
|
log "ожидаем готовности ${READY_URL} (миграции выполняются на старте приложения)"
|
||||||
for _ in $(seq 1 45); do
|
for _ in $(seq 1 45); do
|
||||||
if curl -fsS "http://127.0.0.1:${APP_PORT:-8080}/readyz" >/dev/null 2>&1; then
|
if curl -fsS "$READY_URL" >/dev/null 2>&1; then
|
||||||
log "приложение готово"
|
log "приложение готово"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
if ! curl -fsS "http://127.0.0.1:${APP_PORT:-8080}/readyz" >/dev/null 2>&1; then
|
if ! curl -fsS "$READY_URL" >/dev/null 2>&1; then
|
||||||
die "приложение не поднялось; откат: glchat restore <бэкап>, затем docker compose up -d"
|
die "приложение не поднялось; откат: glchat restore <бэкап>, затем docker compose up -d"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ func staticContentType(rel string) string {
|
|||||||
return "text/css; charset=utf-8"
|
return "text/css; charset=utf-8"
|
||||||
case ".svg":
|
case ".svg":
|
||||||
return "image/svg+xml"
|
return "image/svg+xml"
|
||||||
|
case ".txt":
|
||||||
|
// security.txt (RFC 9116): в минимальном alpine-окружении mime может
|
||||||
|
// не знать расширение и отдать октет-поток.
|
||||||
|
return "text/plain; charset=utf-8"
|
||||||
}
|
}
|
||||||
return mime.TypeByExtension(filepath.Ext(rel))
|
return mime.TypeByExtension(filepath.Ext(rel))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Политика безопасности glchat (RFC 9116)
|
||||||
|
Contact: mailto:security@gl.mhspx.su
|
||||||
|
Expires: 2027-12-31T00:00:00.000Z
|
||||||
|
Preferred-Languages: ru, en
|
||||||
|
Canonical: https://gl.mhspx.su/.well-known/security.txt
|
||||||
|
Policy: https://gl.mhspx.su/security
|
||||||
|
|
||||||
|
# Этот файл отдаётся по адресу /.well-known/security.txt: копия для сборки
|
||||||
|
# лежит в web/public/.well-known/security.txt и должна совпадать с этой.
|
||||||
Reference in New Issue
Block a user