Add restricted provisioning preflight helper

This commit is contained in:
Ochenstarik 2026-07-19 22:59:41 +07:00
parent a15b0dd7b3
commit 4d08b93d23
21 changed files with 504 additions and 12 deletions

View file

@ -57,6 +57,12 @@ jobs:
- name: Publish agent arm64 - name: Publish agent arm64
run: dotnet publish src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj --configuration Release --runtime linux-arm64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true run: dotnet publish src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj --configuration Release --runtime linux-arm64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
- name: Publish provisioning helper amd64
run: dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj --configuration Release --runtime linux-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
- name: Publish provisioning helper arm64
run: dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj --configuration Release --runtime linux-arm64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
- name: Build systemd smoke release - name: Build systemd smoke release
run: | run: |
dotnet publish src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj \ dotnet publish src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj \
@ -65,14 +71,18 @@ jobs:
dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj \ dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj \
--configuration Release --runtime linux-x64 --self-contained true \ --configuration Release --runtime linux-x64 --self-contained true \
-p:PublishSingleFile=true -p:PublishTrimmed=true -o smoke/control -p:PublishSingleFile=true -p:PublishTrimmed=true -o smoke/control
dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj \
--configuration Release --runtime linux-x64 --self-contained true \
-p:PublishSingleFile=true -p:PublishTrimmed=true -o smoke/provisioning-helper
install -d smoke/deploy smoke/bootstrap install -d smoke/deploy smoke/bootstrap
install -m 0644 deploy/ochenstarik-smm-control.service smoke/deploy/ install -m 0644 deploy/ochenstarik-smm-control.service smoke/deploy/
install -m 0644 deploy/ochenstarik-smm-agent.service smoke/deploy/ install -m 0644 deploy/ochenstarik-smm-agent.service smoke/deploy/
install -m 0644 deploy/ochenstarik-smm-provisioning-helper.service smoke/deploy/
install -m 0644 deploy/ochenstarik-smm-firewall.service smoke/deploy/ install -m 0644 deploy/ochenstarik-smm-firewall.service smoke/deploy/
install -m 0755 deploy/ochenstarik-smm-policy-apply smoke/deploy/ install -m 0755 deploy/ochenstarik-smm-policy-apply smoke/deploy/
install -m 0755 deploy/ochenstarik-smm-emergency smoke/deploy/ install -m 0755 deploy/ochenstarik-smm-emergency smoke/deploy/
install -m 0755 deploy/ochenstarik-server-monitor-manager.sh smoke/bootstrap/ install -m 0755 deploy/ochenstarik-server-monitor-manager.sh smoke/bootstrap/
tar -C smoke -czf smoke-release.tar.gz agent control deploy bootstrap tar -C smoke -czf smoke-release.tar.gz agent control provisioning-helper deploy bootstrap
sha256sum smoke-release.tar.gz > smoke-release.tar.gz.sha256 sha256sum smoke-release.tar.gz > smoke-release.tar.gz.sha256
- name: Verify repeated systemd installation - name: Verify repeated systemd installation

View file

@ -52,15 +52,19 @@ jobs:
dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj \ dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj \
--configuration Release --runtime '${{ matrix.runtime }}' --self-contained true \ --configuration Release --runtime '${{ matrix.runtime }}' --self-contained true \
-p:PublishSingleFile=true -p:PublishTrimmed=true -o out/control -p:PublishSingleFile=true -p:PublishTrimmed=true -o out/control
dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj \
--configuration Release --runtime '${{ matrix.runtime }}' --self-contained true \
-p:PublishSingleFile=true -p:PublishTrimmed=true -o out/provisioning-helper
install -d out/deploy out/bootstrap install -d out/deploy out/bootstrap
install -m 0644 deploy/ochenstarik-smm-control.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-control.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-agent.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-agent.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-provisioning-helper.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-firewall.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-firewall.service out/deploy/
install -m 0755 deploy/ochenstarik-smm-policy-apply out/deploy/ install -m 0755 deploy/ochenstarik-smm-policy-apply out/deploy/
install -m 0755 deploy/ochenstarik-smm-emergency out/deploy/ install -m 0755 deploy/ochenstarik-smm-emergency out/deploy/
install -m 0755 deploy/ochenstarik-server-monitor-manager.sh out/bootstrap/ install -m 0755 deploy/ochenstarik-server-monitor-manager.sh out/bootstrap/
archive='server-monitor-manager-${{ matrix.runtime }}.tar.gz' archive='server-monitor-manager-${{ matrix.runtime }}.tar.gz'
tar -C out -czf "$archive" agent control deploy bootstrap tar -C out -czf "$archive" agent control provisioning-helper deploy bootstrap
sha256sum "$archive" >"$archive.sha256" sha256sum "$archive" >"$archive.sha256"
- name: Upload release archive - name: Upload release archive

View file

@ -84,6 +84,9 @@ jobs:
- name: Publish control - name: Publish control
run: dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -o out/control run: dotnet publish src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -o out/control
- name: Publish provisioning helper
run: dotnet publish src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj --configuration Release --runtime ${{ matrix.runtime }} --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true -o out/provisioning-helper
- name: Package - name: Package
shell: bash shell: bash
run: | run: |
@ -91,12 +94,13 @@ jobs:
install -d out/deploy out/bootstrap install -d out/deploy out/bootstrap
install -m 0644 deploy/ochenstarik-smm-control.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-control.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-agent.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-agent.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-provisioning-helper.service out/deploy/
install -m 0644 deploy/ochenstarik-smm-firewall.service out/deploy/ install -m 0644 deploy/ochenstarik-smm-firewall.service out/deploy/
install -m 0755 deploy/ochenstarik-smm-policy-apply out/deploy/ install -m 0755 deploy/ochenstarik-smm-policy-apply out/deploy/
install -m 0755 deploy/ochenstarik-smm-emergency out/deploy/ install -m 0755 deploy/ochenstarik-smm-emergency out/deploy/
install -m 0755 deploy/ochenstarik-server-monitor-manager.sh out/bootstrap/ install -m 0755 deploy/ochenstarik-server-monitor-manager.sh out/bootstrap/
archive="server-monitor-manager-${{ matrix.runtime }}.tar.gz" archive="server-monitor-manager-${{ matrix.runtime }}.tar.gz"
tar -C out -czf "$archive" agent control deploy bootstrap tar -C out -czf "$archive" agent control provisioning-helper deploy bootstrap
sha256sum "$archive" > "$archive.sha256" sha256sum "$archive" > "$archive.sha256"
- name: Upload artifact - name: Upload artifact

View file

@ -100,7 +100,7 @@ In the application, generate or copy the monitoring SSH key, add the Hub profile
The current development branch implements dedicated Windows pages for Servers, Links, Sessions, and Settings; SSH monitoring; directional Links; one-time enrollment; separate mTLS Agent, Operator, and source-scoped Automation identities; certificate revocation/re-enrollment; SQLite control state; audit; authenticated event streaming; Windows Control API integration; and a bounded durable Agent buffer with downsampling. The current development branch implements dedicated Windows pages for Servers, Links, Sessions, and Settings; SSH monitoring; directional Links; one-time enrollment; separate mTLS Agent, Operator, and source-scoped Automation identities; certificate revocation/re-enrollment; SQLite control state; audit; authenticated event streaming; Windows Control API integration; and a bounded durable Agent buffer with downsampling.
Reconnect reconciliation is implemented with a durable SQLite marker: after a Node returns, the Hub reapplies the latest effective disabled policies and clears the marker only after the firewall confirms success. Control also expires TTL Links through the firewall helper, prunes bounded operational data, versions its SQLite schema, and creates verified backups of SQLite state and the Control CA. The first Provisioning control-plane slice now persists versioned jobs, enforces TTL/audit/idempotency and one active job per Node, and exposes Operator-only create/read/confirm/cancel endpoints. CI exercises the Control-to-helper process boundary, HTTP authorization, Agent parsing, Desktop contracts, and a 100-Node concurrent heartbeat/replay scenario. Still required are Provisioning execution/retry/rollback and the restricted helper channel, physical WireGuard/nftables/reboot acceptance, trusted public code signing, Xray, and clients for additional platforms. Reconnect reconciliation is implemented with a durable SQLite marker: after a Node returns, the Hub reapplies the latest effective disabled policies and clears the marker only after the firewall confirms success. Control also expires TTL Links through the firewall helper, prunes bounded operational data, versions its SQLite schema, and creates verified backups of SQLite state and the Control CA. The Provisioning control plane persists versioned jobs, enforces TTL/audit/idempotency and one active job per Node, and supports confirmation, progress, reconciliation, retry, and rollback states. A restricted root helper now accepts only versioned, module-hashed allowlisted requests through a local Unix socket; the first executable action is read-only Linux `preflight`. CI exercises process boundaries, authorization, Agent parsing, Desktop contracts, and concurrent heartbeat/replay. Still required are mutating Provisioning actions with factual-state verification, physical WireGuard/nftables/reboot acceptance, trusted public code signing, Xray, and clients for additional platforms.
## License and project policy ## License and project policy

View file

@ -2,5 +2,6 @@
<Project Path="src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj" /> <Project Path="src/ServerMonitorManager.Agent/ServerMonitorManager.Agent.csproj" />
<Project Path="src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj" /> <Project Path="src/ServerMonitorManager.Control/ServerMonitorManager.Control.csproj" />
<Project Path="src/ServerMonitorManager.Core/ServerMonitorManager.Core.csproj" /> <Project Path="src/ServerMonitorManager.Core/ServerMonitorManager.Core.csproj" />
<Project Path="src/ServerMonitorManager.Provisioning.Helper/ServerMonitorManager.Provisioning.Helper.csproj" />
<Project Path="tests/ServerMonitorManager.Control.Tests/ServerMonitorManager.Control.Tests.csproj" /> <Project Path="tests/ServerMonitorManager.Control.Tests/ServerMonitorManager.Control.Tests.csproj" />
</Solution> </Solution>

View file

@ -12,6 +12,7 @@ readonly CONTROL_USER="ochenstarik-smm-control"
readonly AGENT_USER="ochenstarik-smm-agent" readonly AGENT_USER="ochenstarik-smm-agent"
readonly CONTROL_UNIT="ochenstarik-smm-control.service" readonly CONTROL_UNIT="ochenstarik-smm-control.service"
readonly AGENT_UNIT="ochenstarik-smm-agent.service" readonly AGENT_UNIT="ochenstarik-smm-agent.service"
readonly PROVISIONING_HELPER_UNIT="ochenstarik-smm-provisioning-helper.service"
readonly POLICY_HELPER="/usr/local/libexec/ochenstarik-smm-policy-apply" readonly POLICY_HELPER="/usr/local/libexec/ochenstarik-smm-policy-apply"
readonly EMERGENCY_COMMAND="/usr/local/sbin/ochenstarik-smm-emergency" readonly EMERGENCY_COMMAND="/usr/local/sbin/ochenstarik-smm-emergency"
readonly SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control" readonly SUDOERS_FILE="/etc/sudoers.d/ochenstarik-smm-control"
@ -138,7 +139,7 @@ verify_archive() {
[[ -n "$entry" ]] || continue [[ -n "$entry" ]] || continue
[[ "$entry" != /* && "$entry" != *".."* ]] || fail "Unsafe archive entry: $entry" [[ "$entry" != /* && "$entry" != *".."* ]] || fail "Unsafe archive entry: $entry"
case "$entry" in case "$entry" in
agent|agent/*|control|control/*|deploy|deploy/*|bootstrap|bootstrap/*) ;; agent|agent/*|control|control/*|provisioning-helper|provisioning-helper/*|deploy|deploy/*|bootstrap|bootstrap/*) ;;
*) fail "Unexpected archive entry: $entry" ;; *) fail "Unexpected archive entry: $entry" ;;
esac esac
done < <(tar -tzf "$archive") done < <(tar -tzf "$archive")
@ -152,6 +153,7 @@ extract_archive() {
tar -xzf "$archive" -C "$TEMP_DIR" --no-same-owner --no-same-permissions tar -xzf "$archive" -C "$TEMP_DIR" --no-same-owner --no-same-permissions
[[ -f "$TEMP_DIR/deploy/$CONTROL_UNIT" ]] || fail "Control systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$CONTROL_UNIT" ]] || fail "Control systemd unit is missing from archive."
[[ -f "$TEMP_DIR/deploy/$AGENT_UNIT" ]] || fail "Agent systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$AGENT_UNIT" ]] || fail "Agent systemd unit is missing from archive."
[[ -f "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" ]] || fail "Provisioning helper systemd unit is missing from archive."
[[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall systemd unit is missing from archive." [[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall systemd unit is missing from archive."
[[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing from archive." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing from archive."
[[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency command is missing from archive." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency command is missing from archive."
@ -164,6 +166,7 @@ verify_release_payload() {
extract_archive "$archive" extract_archive "$archive"
[[ -x "$TEMP_DIR/control/ochenstarik-smm-control" ]] || fail "Control binary is missing." [[ -x "$TEMP_DIR/control/ochenstarik-smm-control" ]] || fail "Control binary is missing."
[[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing." [[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing."
[[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] || fail "Provisioning helper binary is missing."
[[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-policy-apply" ]] || fail "Policy helper is missing."
[[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency recovery command is missing." [[ -x "$TEMP_DIR/deploy/ochenstarik-smm-emergency" ]] || fail "Emergency recovery command is missing."
[[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall unit is missing." [[ -f "$TEMP_DIR/deploy/$FIREWALL_UNIT" ]] || fail "Mesh firewall unit is missing."
@ -229,9 +232,11 @@ create_backup() {
agent) agent)
list=( list=(
"usr/local/lib/ochenstarik-server-monitor-manager/agent" "usr/local/lib/ochenstarik-server-monitor-manager/agent"
"usr/local/lib/ochenstarik-server-monitor-manager/provisioning-helper"
"etc/ochenstarik-server-monitor-manager/agent.env" "etc/ochenstarik-server-monitor-manager/agent.env"
"etc/ochenstarik-server-monitor-manager/control-ca.crt" "etc/ochenstarik-server-monitor-manager/control-ca.crt"
"etc/systemd/system/$AGENT_UNIT" "etc/systemd/system/$AGENT_UNIT"
"etc/systemd/system/$PROVISIONING_HELPER_UNIT"
) )
;; ;;
*) fail "Unknown backup role: $role" ;; *) fail "Unknown backup role: $role" ;;
@ -531,11 +536,13 @@ install_agent() {
openssl x509 -in "$ca_cert" -noout >/dev/null 2>&1 || fail "Invalid Control CA certificate." openssl x509 -in "$ca_cert" -noout >/dev/null 2>&1 || fail "Invalid Control CA certificate."
extract_archive "$archive" extract_archive "$archive"
[[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing." [[ -x "$TEMP_DIR/agent/ochenstarik-smm-agent" ]] || fail "Agent binary is missing."
[[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] || fail "Provisioning helper binary is missing."
backup_id="$(create_backup agent)" backup_id="$(create_backup agent)"
ensure_system_user "$AGENT_USER" ensure_system_user "$AGENT_USER"
install -d -m 0750 -o root -g "$AGENT_USER" "$ETC_DIR" install -d -m 0750 -o root -g "$AGENT_USER" "$ETC_DIR"
install -d -m 0700 -o "$AGENT_USER" -g "$AGENT_USER" "$STATE_DIR/agent" install -d -m 0700 -o "$AGENT_USER" -g "$AGENT_USER" "$STATE_DIR/agent"
install_tree_atomic "$TEMP_DIR/agent" "$LIB_DIR/agent" "root:root" install_tree_atomic "$TEMP_DIR/agent" "$LIB_DIR/agent" "root:root"
install_tree_atomic "$TEMP_DIR/provisioning-helper" "$LIB_DIR/provisioning-helper" "root:root"
install -d -m 0755 "$(dirname "$EMERGENCY_COMMAND")" install -d -m 0755 "$(dirname "$EMERGENCY_COMMAND")"
install -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-emergency" "$EMERGENCY_COMMAND" install -m 0755 "$TEMP_DIR/deploy/ochenstarik-smm-emergency" "$EMERGENCY_COMMAND"
if [[ "$(realpath "$ca_cert")" != "$(realpath -m "$ETC_DIR/control-ca.crt")" ]]; then if [[ "$(realpath "$ca_cert")" != "$(realpath -m "$ETC_DIR/control-ca.crt")" ]]; then
@ -564,6 +571,8 @@ EOF
chown root:"$AGENT_USER" "$ETC_DIR/control-ca.crt" chown root:"$AGENT_USER" "$ETC_DIR/control-ca.crt"
chmod 0640 "$ETC_DIR/control-ca.crt" chmod 0640 "$ETC_DIR/control-ca.crt"
install_unit "$TEMP_DIR/deploy/$AGENT_UNIT" "$AGENT_UNIT" install_unit "$TEMP_DIR/deploy/$AGENT_UNIT" "$AGENT_UNIT"
install_unit "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" "$PROVISIONING_HELPER_UNIT"
systemctl enable --now "$PROVISIONING_HELPER_UNIT"
systemctl enable --now "$AGENT_UNIT" systemctl enable --now "$AGENT_UNIT"
systemctl is-active --quiet "$AGENT_UNIT" || { systemctl is-active --quiet "$AGENT_UNIT" || {
systemctl status --no-pager "$AGENT_UNIT" >&2 || true systemctl status --no-pager "$AGENT_UNIT" >&2 || true
@ -661,8 +670,19 @@ update_role() {
esac esac
[[ -x "$TEMP_DIR/$role/$binary" ]] || fail "$role binary is missing." [[ -x "$TEMP_DIR/$role/$binary" ]] || fail "$role binary is missing."
systemctl stop "$unit" systemctl stop "$unit"
if [[ "$role" == "agent" ]]; then
systemctl stop "$PROVISIONING_HELPER_UNIT" 2>/dev/null || true
fi
backup_id="$(create_backup "$role")" backup_id="$(create_backup "$role")"
install_tree_atomic "$TEMP_DIR/$role" "$LIB_DIR/$role" "$user" install_tree_atomic "$TEMP_DIR/$role" "$LIB_DIR/$role" "$user"
if [[ "$role" == "agent" ]]; then
[[ -x "$TEMP_DIR/provisioning-helper/ochenstarik-smm-provisioning-helper" ]] \
|| fail "Provisioning helper binary is missing."
install_tree_atomic "$TEMP_DIR/provisioning-helper" "$LIB_DIR/provisioning-helper" "root:root"
install_unit "$TEMP_DIR/deploy/$AGENT_UNIT" "$AGENT_UNIT"
install_unit "$TEMP_DIR/deploy/$PROVISIONING_HELPER_UNIT" "$PROVISIONING_HELPER_UNIT"
systemctl enable --now "$PROVISIONING_HELPER_UNIT"
fi
systemctl start "$unit" systemctl start "$unit"
if ! systemctl is-active --quiet "$unit"; then if ! systemctl is-active --quiet "$unit"; then
log "Update failed; restoring backup $backup_id" log "Update failed; restoring backup $backup_id"
@ -684,8 +704,14 @@ restore_backup() {
case "$role" in control) unit="$CONTROL_UNIT" ;; agent) unit="$AGENT_UNIT" ;; *) fail "Unknown role: $role" ;; esac case "$role" in control) unit="$CONTROL_UNIT" ;; agent) unit="$AGENT_UNIT" ;; *) fail "Unknown role: $role" ;; esac
[[ -f "$archive" ]] || fail "Backup archive not found: $backup_id" [[ -f "$archive" ]] || fail "Backup archive not found: $backup_id"
systemctl stop "$unit" || true systemctl stop "$unit" || true
if [[ "$role" == "agent" ]]; then
systemctl stop "$PROVISIONING_HELPER_UNIT" || true
fi
tar -C / -xzf "$archive" tar -C / -xzf "$archive"
systemctl daemon-reload systemctl daemon-reload
if [[ "$role" == "agent" ]]; then
systemctl start "$PROVISIONING_HELPER_UNIT"
fi
systemctl start "$unit" systemctl start "$unit"
systemctl is-active --quiet "$unit" || fail "Rollback restored files but service is not active." systemctl is-active --quiet "$unit" || fail "Rollback restored files but service is not active."
log "$role restored from $backup_id" log "$role restored from $backup_id"
@ -825,8 +851,9 @@ uninstall_agent() {
local purge="${1:-}" local purge="${1:-}"
require_root require_root
systemctl disable --now "$AGENT_UNIT" 2>/dev/null || true systemctl disable --now "$AGENT_UNIT" 2>/dev/null || true
rm -f -- "/etc/systemd/system/$AGENT_UNIT" "$ETC_DIR/agent.env" systemctl disable --now "$PROVISIONING_HELPER_UNIT" 2>/dev/null || true
rm -rf -- "$LIB_DIR/agent" rm -f -- "/etc/systemd/system/$AGENT_UNIT" "/etc/systemd/system/$PROVISIONING_HELPER_UNIT" "$ETC_DIR/agent.env"
rm -rf -- "$LIB_DIR/agent" "$LIB_DIR/provisioning-helper"
[[ "$purge" == "--purge" ]] && rm -rf -- "$STATE_DIR/agent" "$ETC_DIR/control-ca.crt" [[ "$purge" == "--purge" ]] && rm -rf -- "$STATE_DIR/agent" "$ETC_DIR/control-ca.crt"
systemctl daemon-reload systemctl daemon-reload
log "Agent removed${purge:+ ($purge)}." log "Agent removed${purge:+ ($purge)}."

View file

@ -1,7 +1,8 @@
[Unit] [Unit]
Description=Ochenstarik Server Monitor Manager Agent Description=Ochenstarik Server Monitor Manager Agent
After=network-online.target After=network-online.target ochenstarik-smm-provisioning-helper.service
Wants=network-online.target Wants=network-online.target
Requires=ochenstarik-smm-provisioning-helper.service
[Service] [Service]
Type=simple Type=simple

View file

@ -0,0 +1,31 @@
[Unit]
Description=Ochenstarik Server Monitor Manager restricted provisioning helper
After=local-fs.target
[Service]
Type=simple
User=root
Group=ochenstarik-smm-agent
UMask=0007
RuntimeDirectory=ochenstarik-server-monitor-manager
RuntimeDirectoryMode=0750
WorkingDirectory=/usr/local/lib/ochenstarik-server-monitor-manager/provisioning-helper
ExecStart=/usr/local/lib/ochenstarik-server-monitor-manager/provisioning-helper/ochenstarik-smm-provisioning-helper
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target

View file

@ -116,7 +116,7 @@
- [x] атомарный Agent job channel только для собственного `node_id`; - [x] атомарный Agent job channel только для собственного `node_id`;
- [x] начальные строгие JSON schemas v1 для `preflight` и `system.base-install`; - [x] начальные строгие JSON schemas v1 для `preflight` и `system.base-install`;
- [ ] versioned JSON schemas для остальных action type; - [ ] versioned JSON schemas для остальных action type;
- [ ] restricted root helper через Unix socket; - [x] restricted root helper через Unix socket (первый allowlisted action `preflight`);
- [x] structured redacted events, bounded Operator history и progress; - [x] structured redacted events, bounded Operator history и progress;
- [x] `NeedsReconciliation` после истечения execution TTL и неопределённого результата; - [x] `NeedsReconciliation` после истечения execution TTL и неопределённого результата;
- [ ] desired/factual configuration и drift; - [ ] desired/factual configuration и drift;

View file

@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using ServerMonitorManager.Core; using ServerMonitorManager.Core;
namespace ServerMonitorManager.Agent; namespace ServerMonitorManager.Agent;
@ -105,12 +106,99 @@ internal sealed class AgentClient(AgentOptions options)
} }
} }
try
{
await ExecuteNextProvisioningJobAsync(client, cancellationToken);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
Console.Error.WriteLine($"Provisioning poll failed: {exception.Message}");
}
var elapsed = DateTimeOffset.UtcNow - iterationStartedAt; var elapsed = DateTimeOffset.UtcNow - iterationStartedAt;
var remaining = collectionDelay - elapsed; var remaining = collectionDelay - elapsed;
await Task.Delay(remaining > TimeSpan.Zero ? remaining : TimeSpan.FromSeconds(1), cancellationToken); await Task.Delay(remaining > TimeSpan.Zero ? remaining : TimeSpan.FromSeconds(1), cancellationToken);
} }
} }
private async Task ExecuteNextProvisioningJobAsync(
HttpClient client,
CancellationToken cancellationToken)
{
using var response = await client.GetAsync("api/v1/agents/provisioning/jobs/next", cancellationToken);
if (response.StatusCode == HttpStatusCode.NoContent)
{
return;
}
response.EnsureSuccessStatusCode();
var job = await response.Content.ReadFromJsonAsync(
SmmJsonContext.Default.ProvisioningJob,
cancellationToken)
?? throw new InvalidOperationException("Control service returned an empty provisioning job.");
if (job.ActionType != "preflight" || job.SchemaVersion != 1)
{
await ReportProvisioningAsync(
client, job, ProvisioningJobStates.Failed, job.ProgressPercent,
"dispatch", "action.unsupported", "Unsupported provisioning action.", cancellationToken);
return;
}
var currentProgress = job.ProgressPercent;
try
{
var helper = new ProvisioningHelperClient(options.ProvisioningSocketPath);
var result = await helper.RunPreflightAsync(job, cancellationToken);
await ReportProvisioningAsync(
client, job, ProvisioningJobStates.Running, 40,
"inspect-host", "preflight.inspected", "Host inspection completed.", cancellationToken);
currentProgress = 40;
await ReportProvisioningAsync(
client, job, ProvisioningJobStates.Verifying, 80,
"verify-host", "preflight.verifying", "Verifying preflight result.", cancellationToken);
currentProgress = 80;
await ReportProvisioningAsync(
client, job, ProvisioningJobStates.Completed, 100,
"completed", "preflight.completed", "Preflight completed.", cancellationToken);
Console.WriteLine(
$"Preflight {job.Id} completed: {result.OperatingSystem} "
+ $"{result.OperatingSystemVersion} {result.Architecture}.");
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
await ReportProvisioningAsync(
client, job, ProvisioningJobStates.Failed, currentProgress,
"preflight", "preflight.failed", "Preflight helper failed.", cancellationToken);
Console.Error.WriteLine($"Preflight {job.Id} failed: {exception.Message}");
}
}
private static async Task ReportProvisioningAsync(
HttpClient client,
ProvisioningJob job,
string state,
int progress,
string step,
string eventCode,
string message,
CancellationToken cancellationToken)
{
var request = new ProvisioningJobProgressRequest(
state, progress, step, eventCode, message, CreateOperationId(job.Id, state));
using var response = await client.PostAsJsonAsync(
$"api/v1/agents/provisioning/jobs/{job.Id}/progress",
request,
SmmJsonContext.Default.ProvisioningJobProgressRequest,
cancellationToken);
response.EnsureSuccessStatusCode();
}
private static string CreateOperationId(string jobId, string state)
{
var digest = SHA256.HashData(Encoding.UTF8.GetBytes($"{jobId}:{state}"));
return new Guid(digest.AsSpan(0, 16)).ToString();
}
private static async Task<AgentHeartbeatResponse> SendHeartbeatAsync( private static async Task<AgentHeartbeatResponse> SendHeartbeatAsync(
HttpClient client, HttpClient client,
AgentHeartbeat heartbeat, AgentHeartbeat heartbeat,

View file

@ -6,6 +6,7 @@ public sealed class AgentOptions
public Uri ControlUrl { get; init; } = new("https://127.0.0.1:7443"); public Uri ControlUrl { get; init; } = new("https://127.0.0.1:7443");
public string StateDirectory { get; init; } = "/var/lib/ochenstarik-server-monitor-manager/agent"; public string StateDirectory { get; init; } = "/var/lib/ochenstarik-server-monitor-manager/agent";
public string CertificateAuthorityPath { get; init; } = "/etc/ochenstarik-server-monitor-manager/control-ca.crt"; public string CertificateAuthorityPath { get; init; } = "/etc/ochenstarik-server-monitor-manager/control-ca.crt";
public string ProvisioningSocketPath { get; init; } = "/run/ochenstarik-server-monitor-manager/provisioning.sock";
public int HeartbeatSeconds { get; init; } = 30; public int HeartbeatSeconds { get; init; } = 30;
public int BufferMaxSamples { get; init; } = 720; public int BufferMaxSamples { get; init; } = 720;
public int BufferRecentSamples { get; init; } = 120; public int BufferRecentSamples { get; init; } = 120;

View file

@ -19,7 +19,8 @@ if (options.HeartbeatSeconds is < 10 or > 300
|| options.BufferRecentSamples >= options.BufferMaxSamples || options.BufferRecentSamples >= options.BufferMaxSamples
|| options.BufferDownsampleFactor is < 2 or > 100 || options.BufferDownsampleFactor is < 2 or > 100
|| options.UploadBatchSize is < 1 or > 100 || options.UploadBatchSize is < 1 or > 100
|| options.MaxRetrySeconds is < 10 or > 3600) || options.MaxRetrySeconds is < 10 or > 3600
|| !Path.IsPathFullyQualified(options.ProvisioningSocketPath))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
"Invalid buffer settings: heartbeat 10-300s, max samples 10-10000, recent samples below max, " "Invalid buffer settings: heartbeat 10-300s, max samples 10-10000, recent samples below max, "

View file

@ -0,0 +1,53 @@
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using ServerMonitorManager.Core;
namespace ServerMonitorManager.Agent;
public sealed class ProvisioningHelperClient(string socketPath)
{
private const int MaximumResponseBytes = 16 * 1024;
public async Task<ProvisioningPreflightResult> RunPreflightAsync(
ProvisioningJob job,
CancellationToken cancellationToken)
{
var request = new ProvisioningHelperRequest(
"1", job.Id, job.ActionType, job.SchemaVersion,
ProvisioningActionCatalog.PreflightModuleHash, job.Parameters);
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), cancellationToken);
await using var stream = new NetworkStream(socket, ownsSocket: false);
var json = JsonSerializer.Serialize(request, SmmJsonContext.Default.ProvisioningHelperRequest) + "\n";
await stream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken);
var payload = await ReadResponseAsync(stream, cancellationToken);
var response = JsonSerializer.Deserialize(payload, SmmJsonContext.Default.ProvisioningHelperResponse)
?? throw new InvalidDataException("Provisioning helper returned an empty response.");
if (!response.Success || response.Preflight is null)
{
throw new InvalidOperationException($"Provisioning helper rejected the request: {response.Code}");
}
return response.Preflight;
}
private static async Task<byte[]> ReadResponseAsync(Stream stream, CancellationToken cancellationToken)
{
using var buffer = new MemoryStream();
var singleByte = new byte[1];
while (buffer.Length <= MaximumResponseBytes)
{
var count = await stream.ReadAsync(singleByte, cancellationToken);
if (count == 0 || singleByte[0] == (byte)'\n')
{
break;
}
buffer.WriteByte(singleByte[0]);
}
if (buffer.Length == 0 || buffer.Length > MaximumResponseBytes)
{
throw new InvalidDataException("Provisioning helper response size is invalid.");
}
return buffer.ToArray();
}
}

View file

@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace ServerMonitorManager.Core; namespace ServerMonitorManager.Core;
@ -163,6 +164,31 @@ public sealed record ProvisioningJobProgressRequest(
string Message, string Message,
string IdempotencyKey); string IdempotencyKey);
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
public sealed record ProvisioningHelperRequest(
string ProtocolVersion,
string JobId,
string ActionType,
int SchemaVersion,
string ModuleHash,
JsonElement Parameters);
public sealed record ProvisioningHelperResponse(
bool Success,
string Code,
string Message,
ProvisioningPreflightResult? Preflight);
public sealed record ProvisioningPreflightResult(
string OperatingSystem,
string OperatingSystemVersion,
string Architecture,
bool HasSystemd,
bool HasSshd,
bool HasNftables,
bool HasWireGuard,
bool HasApt);
public sealed record ProvisioningJob( public sealed record ProvisioningJob(
string Id, string Id,
string NodeId, string NodeId,
@ -208,3 +234,9 @@ public static class ProvisioningJobStates
public const string RollbackFailed = "RollbackFailed"; public const string RollbackFailed = "RollbackFailed";
public const string Cancelled = "Cancelled"; public const string Cancelled = "Cancelled";
} }
public static class ProvisioningActionCatalog
{
public const string PreflightModuleHash =
"2dc48fb4528a291221954fc2dd3478d431b66fe34228f29684ce1648dbe2f32b";
}

View file

@ -28,6 +28,9 @@ namespace ServerMonitorManager.Core;
[JsonSerializable(typeof(ProvisioningJobCreateRequest))] [JsonSerializable(typeof(ProvisioningJobCreateRequest))]
[JsonSerializable(typeof(ProvisioningJobCommandRequest))] [JsonSerializable(typeof(ProvisioningJobCommandRequest))]
[JsonSerializable(typeof(ProvisioningJobProgressRequest))] [JsonSerializable(typeof(ProvisioningJobProgressRequest))]
[JsonSerializable(typeof(ProvisioningHelperRequest))]
[JsonSerializable(typeof(ProvisioningHelperResponse))]
[JsonSerializable(typeof(ProvisioningPreflightResult))]
[JsonSerializable(typeof(ProvisioningJob))] [JsonSerializable(typeof(ProvisioningJob))]
[JsonSerializable(typeof(ProvisioningJob[]))] [JsonSerializable(typeof(ProvisioningJob[]))]
[JsonSerializable(typeof(ProvisioningEvent))] [JsonSerializable(typeof(ProvisioningEvent))]

View file

@ -0,0 +1,24 @@
using ServerMonitorManager.Provisioning.Helper;
if (!OperatingSystem.IsLinux())
{
Console.Error.WriteLine("The provisioning helper is supported only on Linux.");
return 2;
}
if (args.Length != 0)
{
Console.Error.WriteLine("The provisioning helper does not accept command-line arguments.");
return 2;
}
const string socketPath = "/run/ochenstarik-server-monitor-manager/provisioning.sock";
using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
shutdown.Cancel();
};
await new ProvisioningHelperServer(socketPath).RunAsync(shutdown.Token);
return 0;

View file

@ -0,0 +1,139 @@
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;
using ServerMonitorManager.Core;
namespace ServerMonitorManager.Provisioning.Helper;
public sealed class ProvisioningHelperServer(string socketPath)
{
private const int MaximumRequestBytes = 16 * 1024;
[SupportedOSPlatform("linux")]
public async Task RunAsync(CancellationToken cancellationToken)
{
Directory.CreateDirectory(Path.GetDirectoryName(socketPath)!);
File.Delete(socketPath);
using var listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
listener.Bind(new UnixDomainSocketEndPoint(socketPath));
File.SetUnixFileMode(socketPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite
| UnixFileMode.GroupRead | UnixFileMode.GroupWrite);
listener.Listen(8);
try
{
while (!cancellationToken.IsCancellationRequested)
{
var connection = await listener.AcceptAsync(cancellationToken);
await HandleAsync(connection, cancellationToken);
}
}
finally
{
File.Delete(socketPath);
}
}
public static ProvisioningHelperResponse Execute(ProvisioningHelperRequest request)
{
if (request.ProtocolVersion != "1")
{
return Failure("protocol.unsupported", "Unsupported helper protocol version.");
}
if (request.JobId.Length != 32 || !request.JobId.All(Uri.IsHexDigit))
{
return Failure("request.invalid-job", "Invalid provisioning job identifier.");
}
if (request.ActionType != "preflight" || request.SchemaVersion != 1
|| request.ModuleHash != ProvisioningActionCatalog.PreflightModuleHash
|| request.Parameters.ValueKind != JsonValueKind.Object
|| request.Parameters.EnumerateObject().Any())
{
return Failure("action.denied", "The requested action is not allowed.");
}
var release = ReadOperatingSystemRelease();
var result = new ProvisioningPreflightResult(
release.GetValueOrDefault("ID", "linux"),
release.GetValueOrDefault("VERSION_ID", "unknown"),
RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(),
Directory.Exists("/run/systemd/system"),
Exists("/usr/sbin/sshd", "/usr/bin/sshd", "/sbin/sshd"),
Exists("/usr/sbin/nft", "/usr/bin/nft", "/sbin/nft"),
Exists("/usr/bin/wg", "/usr/sbin/wg", "/bin/wg"),
Exists("/usr/bin/apt-get", "/bin/apt-get"));
return new ProvisioningHelperResponse(true, "preflight.completed", "Preflight completed.", result);
}
private static async Task HandleAsync(Socket socket, CancellationToken cancellationToken)
{
using (socket)
await using (var stream = new NetworkStream(socket, ownsSocket: false))
{
ProvisioningHelperResponse response;
try
{
var payload = await ReadRequestAsync(stream, cancellationToken);
var request = JsonSerializer.Deserialize(payload, SmmJsonContext.Default.ProvisioningHelperRequest)
?? throw new JsonException("Empty request.");
response = Execute(request);
}
catch (Exception exception) when (exception is JsonException or InvalidDataException)
{
response = Failure("request.invalid", "Invalid helper request.");
}
var json = JsonSerializer.Serialize(response, SmmJsonContext.Default.ProvisioningHelperResponse) + "\n";
await stream.WriteAsync(Encoding.UTF8.GetBytes(json), cancellationToken);
}
}
private static async Task<byte[]> ReadRequestAsync(Stream stream, CancellationToken cancellationToken)
{
using var buffer = new MemoryStream();
var singleByte = new byte[1];
while (buffer.Length <= MaximumRequestBytes)
{
var count = await stream.ReadAsync(singleByte, cancellationToken);
if (count == 0 || singleByte[0] == (byte)'\n')
{
break;
}
buffer.WriteByte(singleByte[0]);
}
if (buffer.Length == 0 || buffer.Length > MaximumRequestBytes)
{
throw new InvalidDataException("Request size is invalid.");
}
return buffer.ToArray();
}
private static Dictionary<string, string> ReadOperatingSystemRelease()
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
if (!File.Exists("/etc/os-release"))
{
return result;
}
foreach (var line in File.ReadLines("/etc/os-release"))
{
var separator = line.IndexOf('=');
if (separator <= 0)
{
continue;
}
var key = line[..separator];
if (key is "ID" or "VERSION_ID")
{
result[key] = line[(separator + 1)..].Trim().Trim('"');
}
}
return result;
}
private static bool Exists(params string[] paths) => paths.Any(File.Exists);
private static ProvisioningHelperResponse Failure(string code, string message)
=> new(false, code, message, null);
}

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>
<Version>0.1.0</Version>
<AssemblyName>ochenstarik-smm-provisioning-helper</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ServerMonitorManager.Core\ServerMonitorManager.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,56 @@
using System.Text.Json;
using ServerMonitorManager.Core;
using ServerMonitorManager.Provisioning.Helper;
using Xunit;
namespace ServerMonitorManager.Control.Tests;
public sealed class ProvisioningHelperTests
{
[Fact]
public void HelperRejectsEveryActionOutsideFixedAllowlist()
{
using var document = JsonDocument.Parse("{}");
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", new string('a', 32), "shell", 1,
ProvisioningActionCatalog.PreflightModuleHash, document.RootElement.Clone()));
Assert.False(response.Success);
Assert.Equal("action.denied", response.Code);
Assert.Null(response.Preflight);
}
[Fact]
public void HelperAcceptsOnlyEmptyPreflightSchemaOne()
{
if (!OperatingSystem.IsLinux())
{
return;
}
using var document = JsonDocument.Parse("{}");
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", new string('b', 32), "preflight", 1,
ProvisioningActionCatalog.PreflightModuleHash, document.RootElement.Clone()));
Assert.True(response.Success);
Assert.Equal("preflight.completed", response.Code);
Assert.NotNull(response.Preflight);
Assert.NotEmpty(response.Preflight.OperatingSystem);
Assert.NotEmpty(response.Preflight.Architecture);
}
[Fact]
public void HelperContractRejectsUnknownJsonMembers()
{
const string json = """
{"protocolVersion":"1","jobId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actionType":"preflight","schemaVersion":1,
"moduleHash":"2dc48fb4528a291221954fc2dd3478d431b66fe34228f29684ce1648dbe2f32b",
"parameters":{},"command":"id"}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize(json, SmmJsonContext.Default.ProvisioningHelperRequest));
}
}

View file

@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\ServerMonitorManager.Agent\ServerMonitorManager.Agent.csproj" /> <ProjectReference Include="..\..\src\ServerMonitorManager.Agent\ServerMonitorManager.Agent.csproj" />
<ProjectReference Include="..\..\src\ServerMonitorManager.Control\ServerMonitorManager.Control.csproj" Aliases="global,controlapp" /> <ProjectReference Include="..\..\src\ServerMonitorManager.Control\ServerMonitorManager.Control.csproj" Aliases="global,controlapp" />
<ProjectReference Include="..\..\src\ServerMonitorManager.Provisioning.Helper\ServerMonitorManager.Provisioning.Helper.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit.v3" Version="3.2.2" /> <PackageReference Include="xunit.v3" Version="3.2.2" />

View file

@ -53,16 +53,18 @@ rm -f -- "$policy_state"
fixture="$(mktemp -d -t smm-bootstrap-test.XXXXXXXX)" fixture="$(mktemp -d -t smm-bootstrap-test.XXXXXXXX)"
trap 'rm -rf -- "$fixture"' EXIT trap 'rm -rf -- "$fixture"' EXIT
mkdir -p "$fixture/payload/agent" "$fixture/payload/control" "$fixture/payload/deploy" "$fixture/payload/bootstrap" mkdir -p "$fixture/payload/agent" "$fixture/payload/control" "$fixture/payload/provisioning-helper" "$fixture/payload/deploy" "$fixture/payload/bootstrap"
install -m 0755 /bin/true "$fixture/payload/agent/ochenstarik-smm-agent" install -m 0755 /bin/true "$fixture/payload/agent/ochenstarik-smm-agent"
install -m 0755 /bin/true "$fixture/payload/control/ochenstarik-smm-control" install -m 0755 /bin/true "$fixture/payload/control/ochenstarik-smm-control"
install -m 0755 /bin/true "$fixture/payload/provisioning-helper/ochenstarik-smm-provisioning-helper"
install -m 0755 "$helper" "$fixture/payload/deploy/ochenstarik-smm-policy-apply" install -m 0755 "$helper" "$fixture/payload/deploy/ochenstarik-smm-policy-apply"
install -m 0755 "$emergency" "$fixture/payload/deploy/ochenstarik-smm-emergency" install -m 0755 "$emergency" "$fixture/payload/deploy/ochenstarik-smm-emergency"
install -m 0644 "$root/deploy/ochenstarik-smm-control.service" "$fixture/payload/deploy/" install -m 0644 "$root/deploy/ochenstarik-smm-control.service" "$fixture/payload/deploy/"
install -m 0644 "$root/deploy/ochenstarik-smm-agent.service" "$fixture/payload/deploy/" install -m 0644 "$root/deploy/ochenstarik-smm-agent.service" "$fixture/payload/deploy/"
install -m 0644 "$root/deploy/ochenstarik-smm-provisioning-helper.service" "$fixture/payload/deploy/"
install -m 0644 "$root/deploy/ochenstarik-smm-firewall.service" "$fixture/payload/deploy/" install -m 0644 "$root/deploy/ochenstarik-smm-firewall.service" "$fixture/payload/deploy/"
install -m 0755 "$bootstrap" "$fixture/payload/bootstrap/ochenstarik-server-monitor-manager.sh" install -m 0755 "$bootstrap" "$fixture/payload/bootstrap/ochenstarik-server-monitor-manager.sh"
tar -C "$fixture/payload" -czf "$fixture/release.tar.gz" agent control deploy bootstrap tar -C "$fixture/payload" -czf "$fixture/release.tar.gz" agent control provisioning-helper deploy bootstrap
sha256sum "$fixture/release.tar.gz" >"$fixture/release.tar.gz.sha256" sha256sum "$fixture/release.tar.gz" >"$fixture/release.tar.gz.sha256"
bash "$bootstrap" verify-release "$fixture/release.tar.gz" >/dev/null bash "$bootstrap" verify-release "$fixture/release.tar.gz" >/dev/null