git init
Some checks failed
CI / containers (push) Has been cancelled
CI / agent (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

This commit is contained in:
2026-09-19 19:31:36 +03:00
commit c4dc19cd41
78 changed files with 13045 additions and 0 deletions

38
scripts/README.md Normal file
View File

@@ -0,0 +1,38 @@
# Agent lifecycle script
`vps-agentctl.sh` installs signed/checksummed release artifacts without piping a
network response into a shell. Run a locally inspected copy as root.
```bash
./vps-agentctl.sh install --version v0.1.0
./vps-agentctl.sh update --version v0.1.1
./vps-agentctl.sh status
./vps-agentctl.sh restart
./vps-agentctl.sh version
./vps-agentctl.sh rollback
./vps-agentctl.sh remove
./vps-agentctl.sh remove --purge --yes
```
Set `VPS_AGENT_RELEASES_URL` or pass `--releases-url` for a GitHub-compatible
releases root. Set `VPS_AGENT_MINISIGN_PUBLIC_KEY` to require verification of
`SHA256SUMS.minisig`; checksums are always mandatory. Supported artifact names
are `vps-agent-linux-amd64` and `vps-agent-linux-arm64`.
Maintainers can create that release layout from two cross-compiled binaries:
```bash
./package-agent-release.sh path/to/x86_64/vps-agent path/to/aarch64/vps-agent dist
```
This produces both canonical names and `SHA256SUMS`; when
`VPS_AGENT_MINISIGN_SECRET_KEY` is set it also creates `SHA256SUMS.minisig`.
Install checks root, Linux/systemd and architecture, creates the locked service
user/directories, validates the binary before installation, writes safe config
and a random token, installs hardening, and starts the unit. Update stages the
new binary and automatically restores the previous one when the service health
check fails. `rollback` swaps current/previous versions.
Remove preserves `/etc/vps-agent` by default. `--purge` removes credentials and
requires a terminal confirmation or the explicit `--yes` flag.

58
scripts/README_RU.md Normal file
View File

@@ -0,0 +1,58 @@
# Скрипт управления жизненным циклом агента
`vps-agentctl.sh` устанавливает подписанные и проверенные по контрольной сумме артефакты релизов, не передавая сетевой ответ напрямую в shell. Запускайте локально проверенную копию скрипта от имени `root`.
```bash
./vps-agentctl.sh install --version v0.1.0
./vps-agentctl.sh update --version v0.1.1
./vps-agentctl.sh status
./vps-agentctl.sh restart
./vps-agentctl.sh version
./vps-agentctl.sh rollback
./vps-agentctl.sh remove
./vps-agentctl.sh remove --purge --yes
```
Укажите переменную окружения `VPS_AGENT_RELEASES_URL` или передайте параметр `--releases-url`, чтобы задать корневой URL репозитория релизов, совместимого с GitHub Releases.
Чтобы сделать обязательной проверку файла `SHA256SUMS.minisig`, задайте переменную `VPS_AGENT_MINISIGN_PUBLIC_KEY`. Проверка контрольных сумм обязательна всегда.
Поддерживаются следующие имена артефактов:
- `vps-agent-linux-amd64`
- `vps-agent-linux-arm64`
Сопровождающие проект разработчики могут сформировать необходимую структуру релиза из двух заранее скомпилированных бинарных файлов:
```bash
./package-agent-release.sh path/to/x86_64/vps-agent path/to/aarch64/vps-agent dist
```
Этот скрипт создаёт файлы с каноническими именами, а также `SHA256SUMS`. Если задана переменная `VPS_AGENT_MINISIGN_SECRET_KEY`, дополнительно создаётся файл подписи `SHA256SUMS.minisig`.
Во время установки скрипт:
- проверяет наличие прав `root`;
- проверяет, что система работает под Linux и использует `systemd`;
- определяет и проверяет архитектуру процессора;
- создаёт изолированного системного пользователя и необходимые директории;
- проверяет бинарный файл перед установкой;
- создаёт безопасную конфигурацию и случайный токен;
- устанавливает настройки усиления безопасности (`hardening`);
- запускает systemd-службу агента.
При обновлении новая версия бинарного файла сначала устанавливается во временное состояние. После этого выполняется проверка работоспособности службы. Если проверка завершается неудачно, автоматически восстанавливается предыдущая версия.
Команда `rollback` меняет местами текущую и предыдущую версии агента.
По умолчанию команда удаления сохраняет каталог:
```text
/etc/vps-agent
```
Параметр `--purge` дополнительно удаляет конфигурацию и учётные данные агента. Для этого требуется подтверждение через терминал либо явное указание флага:
```bash
--yes
```

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
if [[ $# -ne 3 ]]; then
printf 'Usage: %s X86_64_BINARY AARCH64_BINARY OUTPUT_DIRECTORY\n' "$0" >&2
exit 2
fi
AMD64_BINARY="$1"
ARM64_BINARY="$2"
OUTPUT_DIRECTORY="$3"
[[ -x "$AMD64_BINARY" ]] || { printf 'amd64 binary is not executable\n' >&2; exit 1; }
[[ -x "$ARM64_BINARY" ]] || { printf 'arm64 binary is not executable\n' >&2; exit 1; }
command -v sha256sum >/dev/null || { printf 'sha256sum is required\n' >&2; exit 1; }
command -v file >/dev/null || { printf 'file is required\n' >&2; exit 1; }
AMD64_DESCRIPTION="$(file -b "$AMD64_BINARY")"
ARM64_DESCRIPTION="$(file -b "$ARM64_BINARY")"
[[ "$AMD64_DESCRIPTION" == *ELF* && "$AMD64_DESCRIPTION" == *x86-64* ]] || {
printf 'amd64 artifact is not an x86-64 Linux ELF binary\n' >&2; exit 1;
}
[[ "$ARM64_DESCRIPTION" == *ELF* && ("$ARM64_DESCRIPTION" == *aarch64* || "$ARM64_DESCRIPTION" == *ARM64*) ]] || {
printf 'arm64 artifact is not an aarch64 Linux ELF binary\n' >&2; exit 1;
}
install -d -m 0755 "$OUTPUT_DIRECTORY"
install -m 0755 "$AMD64_BINARY" "$OUTPUT_DIRECTORY/vps-agent-linux-amd64"
install -m 0755 "$ARM64_BINARY" "$OUTPUT_DIRECTORY/vps-agent-linux-arm64"
(
cd "$OUTPUT_DIRECTORY"
sha256sum vps-agent-linux-amd64 vps-agent-linux-arm64 > SHA256SUMS
if [[ -n "${VPS_AGENT_MINISIGN_SECRET_KEY:-}" ]]; then
command -v minisign >/dev/null || { printf 'minisign is required for signing\n' >&2; exit 1; }
minisign -Sm SHA256SUMS -s "$VPS_AGENT_MINISIGN_SECRET_KEY"
fi
)

307
scripts/vps-agentctl.sh Executable file
View File

@@ -0,0 +1,307 @@
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
readonly PROGRAM="vps-agent"
readonly BINARY="/usr/local/bin/vps-agent"
readonly PREVIOUS="/usr/local/lib/vps-agent/vps-agent.previous"
readonly CONFIG_DIR="/etc/vps-agent"
readonly CREDENTIAL_DIR="${CONFIG_DIR}/credentials"
readonly STATE_DIR="/var/lib/vps-agent"
readonly UNIT="/etc/systemd/system/vps-agent.service"
readonly DEFAULT_RELEASES="https://github.com/vps-control/vps-control/releases"
VERSION="latest"
RELEASES_URL="${VPS_AGENT_RELEASES_URL:-$DEFAULT_RELEASES}"
PURGE=false
ASSUME_YES=false
TMP_DIR=""
DOWNLOADED_BINARY=""
usage() {
printf '%s\n' "Usage: $0 <install|update|remove|rollback|status|restart|version> [options]" \
" --version VERSION release tag, or latest" \
" --releases-url URL releases root (default: VPS_AGENT_RELEASES_URL)" \
" --purge remove configuration and credentials" \
" --yes confirm destructive purge"
}
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
info() { printf '==> %s\n' "$*"; }
cleanup() {
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
rm -rf -- "$TMP_DIR"
fi
}
trap cleanup EXIT
require_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || die "run this action as root"; }
check_host() {
[[ "$(uname -s)" == "Linux" ]] || die "only Linux is supported"
command -v systemctl >/dev/null || die "systemd is required"
[[ -d /run/systemd/system ]] || die "systemd is not running"
}
release_arch() {
case "$(uname -m)" in
x86_64|amd64) printf 'amd64\n' ;;
aarch64|arm64) printf 'arm64\n' ;;
*) die "unsupported architecture: $(uname -m)" ;;
esac
}
download_release() {
command -v curl >/dev/null || die "curl is required"
command -v sha256sum >/dev/null || die "sha256sum is required"
TMP_DIR="$(mktemp -d -t vps-agent.XXXXXXXX)"
chmod 700 "$TMP_DIR"
local artifact="vps-agent-linux-$(release_arch)"
local release_url
if [[ "$VERSION" == "latest" ]]; then
release_url="${RELEASES_URL%/}/latest/download"
else
[[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]] || die "invalid release version"
release_url="${RELEASES_URL%/}/download/${VERSION}"
fi
info "downloading ${artifact} (${VERSION})"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
--output "$TMP_DIR/$artifact" "$release_url/$artifact"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
--output "$TMP_DIR/SHA256SUMS" "$release_url/SHA256SUMS"
local expected actual
expected="$(awk -v name="$artifact" '$2 == name || $2 == "*" name { print $1; exit }' "$TMP_DIR/SHA256SUMS")"
[[ "$expected" =~ ^[a-fA-F0-9]{64}$ ]] || die "artifact checksum is absent or malformed"
actual="$(sha256sum "$TMP_DIR/$artifact" | awk '{print $1}')"
[[ "$actual" == "$expected" ]] || die "checksum verification failed"
if [[ -n "${VPS_AGENT_MINISIGN_PUBLIC_KEY:-}" ]]; then
command -v minisign >/dev/null || die "minisign is required when a public key is configured"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
--output "$TMP_DIR/SHA256SUMS.minisig" "$release_url/SHA256SUMS.minisig"
minisign -Vm "$TMP_DIR/SHA256SUMS" -P "$VPS_AGENT_MINISIGN_PUBLIC_KEY"
fi
chmod 0755 "$TMP_DIR/$artifact"
"$TMP_DIR/$artifact" --version >/dev/null || die "downloaded binary self-check failed"
DOWNLOADED_BINARY="$TMP_DIR/$artifact"
}
install_unit() {
if [[ -f "$(cd "$(dirname "$0")" && pwd)/../agent/packaging/vps-agent.service" ]]; then
install -o root -g root -m 0644 \
"$(cd "$(dirname "$0")" && pwd)/../agent/packaging/vps-agent.service" "$UNIT"
return
fi
install -o root -g root -m 0644 /dev/null "$UNIT"
tee "$UNIT" >/dev/null <<'EOF'
[Unit]
Description=VPS Control monitoring agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=vps-agent
Group=vps-agent
ExecStart=/usr/local/bin/vps-agent --config /etc/vps-agent/config.toml
Restart=on-failure
RestartSec=5s
TimeoutStopSec=15s
MemoryHigh=48M
MemoryMax=64M
CPUQuota=10%
TasksMax=64
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictRealtime=true
RestrictNamespaces=true
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
UMask=0077
ProcSubset=all
ProtectProc=invisible
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
[Install]
WantedBy=multi-user.target
EOF
}
create_config() {
[[ -f "$CONFIG_DIR/config.toml" ]] && return
install -o root -g vps-agent -m 0640 /dev/null "$CONFIG_DIR/config.toml"
tee "$CONFIG_DIR/config.toml" >/dev/null <<'EOF'
[server]
listen = "127.0.0.1:9105"
[auth]
mode = "token"
token_file = "/etc/vps-agent/credentials/control-plane.token"
[limits]
max_concurrent_requests = 32
request_timeout_seconds = 5
max_body_bytes = 65536
read_requests_per_second = 10.0
write_requests_per_second = 2.0
burst = 20.0
command_timeout_seconds = 4
[services]
allowed = []
[firewall]
management_enabled = false
rollback_timeout_seconds = 60
max_snapshots = 5
[logging]
level = "info"
[telemetry]
connections_enabled = true
process_info_enabled = true
[logs]
enabled = false
allowed_units = ["vps-agent.service"]
max_entries_per_request = 200
max_message_bytes = 16384
[tls]
enabled = false
certificate_file = ""
private_key_file = ""
EOF
}
create_token() {
local token_file="$CREDENTIAL_DIR/control-plane.token"
[[ -f "$token_file" ]] && return
if command -v openssl >/dev/null; then
umask 077
openssl rand -hex 32 > "$token_file"
else
umask 077
od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > "$token_file"
printf '\n' >> "$token_file"
fi
chown vps-agent:vps-agent "$token_file"
chmod 0600 "$token_file"
info "created agent token at $token_file; copy it through a secure channel"
}
wait_healthy() {
local attempts=15
while (( attempts > 0 )); do
if systemctl is-active --quiet vps-agent.service; then
return 0
fi
sleep 1
attempts=$((attempts - 1))
done
systemctl status --no-pager vps-agent.service || true
return 1
}
do_install() {
require_root; check_host
[[ ! -e "$BINARY" ]] || die "$BINARY already exists; use update"
local downloaded
download_release
downloaded="$DOWNLOADED_BINARY"
getent group vps-agent >/dev/null || groupadd --system vps-agent
id -u vps-agent >/dev/null 2>&1 || useradd --system --gid vps-agent --home-dir /nonexistent --shell /usr/sbin/nologin vps-agent
install -d -o root -g vps-agent -m 0750 "$CONFIG_DIR" "$CREDENTIAL_DIR"
install -d -o vps-agent -g vps-agent -m 0700 "$STATE_DIR"
install -d -o root -g root -m 0755 "$(dirname "$PREVIOUS")"
install -o root -g root -m 0755 "$downloaded" "$BINARY"
create_config
create_token
install_unit
systemctl daemon-reload
systemctl enable --now vps-agent.service
wait_healthy || die "agent failed to become healthy"
systemctl status --no-pager vps-agent.service
}
do_update() {
require_root; check_host
[[ -x "$BINARY" ]] || die "agent is not installed"
local downloaded staged
download_release
downloaded="$DOWNLOADED_BINARY"
staged="$(dirname "$BINARY")/.vps-agent.new"
install -o root -g root -m 0755 "$downloaded" "$staged"
install -o root -g root -m 0755 "$BINARY" "$PREVIOUS"
mv -f -- "$staged" "$BINARY"
systemctl restart vps-agent.service
if ! wait_healthy; then
info "health check failed; restoring previous binary"
install -o root -g root -m 0755 "$PREVIOUS" "$BINARY"
systemctl restart vps-agent.service
wait_healthy || die "rollback also failed; inspect systemctl status"
die "update rolled back"
fi
info "updated to $($BINARY --version)"
}
do_rollback() {
require_root; check_host
[[ -x "$PREVIOUS" ]] || die "no previous binary is available"
local current="$(dirname "$BINARY")/.vps-agent.current"
install -o root -g root -m 0755 "$BINARY" "$current"
install -o root -g root -m 0755 "$PREVIOUS" "$BINARY"
mv -f -- "$current" "$PREVIOUS"
systemctl restart vps-agent.service
wait_healthy || die "rolled-back version did not become healthy"
}
do_remove() {
require_root; check_host
if [[ "$PURGE" == true && "$ASSUME_YES" != true ]]; then
[[ -t 0 ]] || die "--purge requires an interactive terminal or --yes"
read -r -p "Permanently delete $CONFIG_DIR and credentials? [y/N] " answer
[[ "$answer" == "y" || "$answer" == "Y" ]] || die "purge cancelled"
fi
systemctl disable --now vps-agent.service 2>/dev/null || true
rm -f -- "$UNIT" "$BINARY" "$PREVIOUS"
rm -rf -- "$STATE_DIR"
systemctl daemon-reload
if [[ "$PURGE" == true ]]; then
rm -rf -- "$CONFIG_DIR"
info "removed configuration and credentials; this cannot be recovered"
else
info "preserved $CONFIG_DIR (use remove --purge to delete it)"
fi
userdel vps-agent 2>/dev/null || true
groupdel vps-agent 2>/dev/null || true
}
[[ $# -ge 1 ]] || { usage; exit 2; }
ACTION="$1"; shift
while [[ $# -gt 0 ]]; do
case "$1" in
--version) [[ $# -ge 2 ]] || die "--version needs a value"; VERSION="$2"; shift 2 ;;
--releases-url) [[ $# -ge 2 ]] || die "--releases-url needs a value"; RELEASES_URL="$2"; shift 2 ;;
--purge) PURGE=true; shift ;;
--yes) ASSUME_YES=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
case "$ACTION" in
install) do_install ;;
update) do_update ;;
remove) do_remove ;;
rollback) do_rollback ;;
status) systemctl status --no-pager vps-agent.service ;;
restart) require_root; systemctl restart vps-agent.service; wait_healthy ;;
version) [[ -x "$BINARY" ]] && "$BINARY" --version || die "agent is not installed" ;;
*) usage; exit 2 ;;
esac