Files
aios/deploy/openbao-autonomy/admin-bootstrap-openbao-autonomy-v1-v3.sh
T

3767 lines
130 KiB
Bash
Executable File

#!/usr/bin/env bash
# AIOS OpenBao Autonomy Runtime V1
# One-time, non-interactive, fail-closed bootstrap.
#
# This script is designed to be executed once by an administrator:
# sudo bash admin-bootstrap-openbao-autonomy-v1.sh
#
# It does not declare OPENBAO_AUTONOMY_BOOTSTRAP_READY. It installs and starts
# the autonomous acceptance workflow; READY may only be written by that
# workflow after the configured unattended acceptance window succeeds.
#
# Official references:
# https://openbao.org/docs/install/
# https://openbao.org/docs/configuration/seal/static/
# https://openbao.org/docs/configuration/self-init/
# https://openbao.org/docs/auth/approle/
# https://openbao.org/docs/secrets/kv/kv-v2/
# https://openbao.org/docs/secrets/transit/
# https://docs.gitea.com/development/api-usage
# https://docs.gitea.com/administration/command-line
# Gitea 1.23.0 release: tag-protection REST API was introduced there.
set -Eeuo pipefail
shopt -s inherit_errexit nullglob
umask 077
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
export LC_ALL=C
readonly SCRIPT_VERSION="3.2.0"
readonly SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
readonly SCRIPT_SHA256="$(sha256sum "$SCRIPT_PATH" | awk '{print $1}')"
readonly OPENBAO_PRIMARY_GPG_FPR="66D15FDD87287219C8E15478D200CD702853E6D0"
readonly OPENBAO_GPG_KEY_URL="https://openbao.org/assets/openbao-gpg-pub-20240618.asc"
readonly OPENBAO_RELEASE_API="https://api.github.com/repos/openbao/openbao/releases/latest"
readonly GITEA_MIN_TAG_PROTECTION_API_VERSION="1.23.0"
readonly ROOT="${AIOS_AUTONOMY_ROOT:-/var/lib/aios-autonomy}"
readonly BAO_ROOT="${AIOS_OPENBAO_ROOT:-/var/lib/aios-openbao}"
readonly ETC_ROOT="${AIOS_ETC_ROOT:-/etc/aios}"
readonly BAO_ETC="${AIOS_OPENBAO_ETC:-$ETC_ROOT/openbao}"
readonly CREDENTIAL_ROOT="${AIOS_CREDENTIAL_ROOT:-$ETC_ROOT/credentials}"
readonly RELEASE_LOCK="${AIOS_OPENBAO_RELEASE_LOCK:-$BAO_ETC/release-lock.json}"
readonly EVIDENCE="${AIOS_BOOTSTRAP_EVIDENCE:-$ROOT/bootstrap/bootstrap-evidence.json}"
readonly STATUS_FILE="${AIOS_AUTONOMY_STATUS:-$ROOT/state/AUTONOMY_STATUS.json}"
readonly GITEA_TOKEN_FILE="${AIOS_GITEA_TOKEN_FILE:-$CREDENTIAL_ROOT/gitea-machine.token}"
readonly PACKAGE_DIR="${AIOS_PACKAGE_DIR:-$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)}"
readonly GITEA_BIN="${AIOS_GITEA_BIN:-/data/aios-gitops/runtime/bin/gitea}"
readonly GITEA_URL="${AIOS_GITEA_URL:-http://127.0.0.1:3000}"
readonly GITEA_OWNER="${AIOS_GITEA_OWNER:-aios}"
readonly GITEA_REPO="${AIOS_GITEA_REPO:-aios}"
readonly GITEA_MACHINE_USER="${AIOS_GITEA_MACHINE_USER:-aios-autonomy-agent}"
readonly GITEA_MACHINE_EMAIL="${AIOS_GITEA_MACHINE_EMAIL:-aios-autonomy-agent@localhost}"
readonly GITEA_GIT_URL="${AIOS_GITEA_GIT_URL:-${GITEA_URL%/}/${GITEA_OWNER}/${GITEA_REPO}.git}"
readonly GITEA_CONFIG="${AIOS_GITEA_CONFIG:-}"
readonly GITEA_WORK_PATH="${AIOS_GITEA_WORK_PATH:-}"
readonly GITEA_RUN_USER="${AIOS_GITEA_RUN_USER:-}"
readonly BAO_SERVICE="${AIOS_OPENBAO_SERVICE:-aios-openbao.service}"
readonly CONTROLLER_SERVICE="${AIOS_AUTONOMY_CONTROLLER_SERVICE:-aios-autonomy-controller.service}"
readonly BAO_HEALTH_URL="${AIOS_OPENBAO_HEALTH_URL:-https://127.0.0.1:8200/v1/sys/health}"
readonly BAO_READY_TIMEOUT="${AIOS_OPENBAO_READY_TIMEOUT:-300}"
# These are invariant acceptance gates. Environment variables with the old
# bypass names are rejected by validate_configuration().
readonly REQUIRE_BRANCH_PROTECTION="1"
readonly REQUIRE_OPENBAO_READY="1"
readonly ALLOW_FILE_SEAL_KEY="${AIOS_ALLOW_FILE_SEAL_KEY:-1}"
readonly SEAL_DIR="$BAO_ROOT/seal"
readonly SEAL_KEY_FILE="$SEAL_DIR/current.key"
readonly SEAL_KEY_ID_FILE="$SEAL_DIR/current.key.id"
readonly TLS_DIR="$BAO_ETC/tls"
readonly TLS_KEY="$TLS_DIR/server.key"
readonly TLS_CERT="$TLS_DIR/server.crt"
readonly TLS_CA="$TLS_DIR/ca.crt"
readonly TLS_CA_KEY="$TLS_DIR/ca.key"
readonly SEAL_CRED_FILE="$BAO_ETC/seal-key.cred"
readonly OPENBAO_CONFIG="$BAO_ETC/openbao.hcl"
readonly OPENBAO_INIT_MARKER="$ROOT/bootstrap/openbao-self-init-complete.json"
readonly MANAGED_MARKER="$ROOT/bootstrap/managed-by-openbao-autonomy-v3.json"
readonly APPROLE_ROLE_ID_FILE="$CREDENTIAL_ROOT/openbao-approle.role-id"
readonly APPROLE_SECRET_ID_FILE="$CREDENTIAL_ROOT/openbao-approle.secret-id"
readonly APPROLE_SECRET_ID_CRED="$BAO_ETC/openbao-approle-secret-id.cred"
readonly CHARTER_DIR="$BAO_ETC/charter"
readonly CHARTER_FILE="$CHARTER_DIR/aios-autonomy-charter-v1.json"
readonly CHARTER_SIGNATURE_FILE="$ROOT/state/charter-signature.json"
readonly AUDIT_DIR="/var/log/aios-openbao"
readonly AUDIT_LOG="$AUDIT_DIR/audit.jsonl"
readonly BACKUP_STATUS="$ROOT/state/backup-status.json"
readonly RESTORE_STATUS="$ROOT/state/restore-test-evidence.json"
readonly FAULT_STATUS="$ROOT/state/fault-injection-results.json"
readonly ACCEPTANCE_STATUS="$ROOT/state/unattended-72h-acceptance.json"
readonly OPTIMIZATION_LEDGER="$ROOT/optimization/optimization-ledger.jsonl"
readonly CONTROLLER_INTERVAL_FILE="$ROOT/state/controller-interval-seconds"
readonly ACCEPTANCE_START_FILE="$ROOT/state/unattended-acceptance-started-at"
readonly ACCEPTANCE_SCRIPT_FINGERPRINT="$ROOT/state/unattended-acceptance-script.sha256"
readonly LIBEXEC_DIR="/usr/local/libexec"
readonly CLIENT_LIB="$LIBEXEC_DIR/aios-openbao-client-lib"
readonly CONTROLLER_BIN="$LIBEXEC_DIR/aios-autonomy-controller"
readonly BACKUP_BIN="$LIBEXEC_DIR/aios-openbao-backup"
readonly RESTORE_TEST_BIN="$LIBEXEC_DIR/aios-openbao-restore-test"
readonly FAULT_TEST_BIN="$LIBEXEC_DIR/aios-openbao-fault-injection"
readonly OPTIMIZE_BIN="$LIBEXEC_DIR/aios-autonomy-optimize"
readonly ACCEPTANCE_BIN="$LIBEXEC_DIR/aios-autonomy-acceptance"
readonly RELEASES_DIR="/usr/local/lib/aios-openbao/versions"
readonly CURRENT_LINK="/usr/local/lib/aios-openbao/current"
readonly BAO_LINK="/usr/local/bin/bao"
PHASE="preflight"
FAILURE_REASON=""
TEMP_DIR=""
TEMP_ADMIN_USER=""
TEMP_ADMIN_TOKEN=""
TEMP_ADMIN_TOKEN_REVOKED=false
TEMP_ADMIN_REMOVED=false
HOST_MUTATED=false
GITEA_MUTATED=false
PRODUCTION_SERVICE_STARTED=false
PRODUCTION_TRAFFIC_CHANGED=false
RELEASE_SHA256_VERIFIED=false
RELEASE_GPG_VERIFIED=false
RELEASE_SIGSTORE_VERIFIED=false
RELEASE_BINARY_HELP_VERIFIED=false
RELEASE_RUNTIME_READY_VERIFIED=false
SEAL_STORAGE="file"
SEAL_RUNTIME_PATH="$SEAL_KEY_FILE"
PENDING_LEGACY_SEAL_KEY_REMOVAL=false
SEAL_KEY_LENGTH_VERIFIED=false
SEAL_RUNTIME_READ_VERIFIED=false
SEAL_REBOOT_READ_VERIFIED=false
MACHINE_IDENTITY_CREATED=false
MACHINE_CREDENTIAL_PERSISTED=false
MACHINE_AUTHENTICATED=false
MACHINE_WRITE_PERMISSION_VERIFIED=false
MACHINE_NON_MAIN_PUSH_VERIFIED=false
DEFAULT_BRANCH_PROTECTION_VERIFIED=false
TAG_PROTECTION_VERIFIED=false
MACHINE_CREDENTIAL_STORAGE="file"
SERVICES_ENABLED=false
SERVICES_STARTED=false
SERVICES_HEALTHY=false
ACCEPTANCE_STARTED=false
OPENBAO_SELF_INIT_CONFIGURED=false
OPENBAO_INITIALIZED_VERIFIED=false
OPENBAO_UNSEALED_VERIFIED=false
OPENBAO_AUDIT_VERIFIED=false
OPENBAO_TRANSIT_VERIFIED=false
OPENBAO_KV_VERIFIED=false
OPENBAO_APPROLE_VERIFIED=false
OPENBAO_POLICY_VERIFIED=false
OPENBAO_CHARTER_STORED=false
OPENBAO_CHARTER_SIGNED=false
OPENBAO_CHARTER_SIGNATURE_VERIFIED=false
OPENBAO_INIT_SECRET_REMOVED=false
APPROLE_SECRET_STORAGE="file"
APPROLE_SECRET_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_BACKUP_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_FAULT_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_RESTORE_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
CLEANUP_RUNNING=false
FINAL_RC=1
SELECTED_VERSION=""
SELECTED_ASSET=""
SELECTED_SOURCE=""
SELECTED_SHA256=""
SELECTED_SIGNING_FPR=""
DEFAULT_BRANCH=""
GITEA_VERSION=""
PREVIOUS_CURRENT_TARGET=""
BAO_WAS_ACTIVE=false
CONTROLLER_WAS_ACTIVE=false
log() {
printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" >&2
}
die() {
FAILURE_REASON="$1"
FINAL_RC="${2:-1}"
log "FATAL phase=$PHASE reason=$FAILURE_REASON"
exit "$FINAL_RC"
}
need() {
command -v "$1" >/dev/null 2>&1 || die "required command unavailable: $1" 69
}
is_true() {
case "${1,,}" in
1|true|yes|on) return 0 ;;
*) return 1 ;;
esac
}
validate_configuration() {
[[ "$GITEA_OWNER" =~ ^[A-Za-z0-9._-]+$ ]] || die "unsafe Gitea owner"
[[ "$GITEA_REPO" =~ ^[A-Za-z0-9._-]+$ ]] || die "unsafe Gitea repository name"
[[ "$GITEA_MACHINE_USER" =~ ^[A-Za-z0-9._-]+$ ]] || die "unsafe Gitea machine username"
[[ "$BAO_SERVICE" =~ ^[A-Za-z0-9_.@-]+\.service$ ]] || die "unsafe OpenBao service name"
[[ "$CONTROLLER_SERVICE" =~ ^[A-Za-z0-9_.@-]+\.service$ ]] || die "unsafe autonomy controller service name"
[[ "$BAO_SERVICE" == "aios-openbao.service" ]] ||
die "AIOS_OPENBAO_SERVICE override is unsupported by the self-contained unit graph"
[[ "$CONTROLLER_SERVICE" == "aios-autonomy-controller.service" ]] ||
die "AIOS_AUTONOMY_CONTROLLER_SERVICE override is unsupported by the self-contained unit graph"
[[ "$GITEA_URL" =~ ^https?://[^[:space:]]+$ ]] || die "invalid Gitea API URL"
[[ "$GITEA_GIT_URL" =~ ^https?://[^[:space:]]+\.git$ ]] || die "invalid Gitea Git URL"
[[ ! "$GITEA_URL" =~ ://[^/]*@ ]] || die "Gitea API URL must not embed credentials"
[[ ! "$GITEA_GIT_URL" =~ ://[^/]*@ ]] || die "Gitea Git URL must not embed credentials"
if [[ "$GITEA_URL" == http://* && ! "$GITEA_URL" =~ ^http://(127\.0\.0\.1|localhost|\[::1\])(:[0-9]+)?(/|$) ]]; then
die "plaintext Gitea API transport is allowed only on loopback"
fi
if [[ "$GITEA_GIT_URL" == http://* && ! "$GITEA_GIT_URL" =~ ^http://(127\.0\.0\.1|localhost|\[::1\])(:[0-9]+)?/ ]]; then
die "plaintext Git transport is allowed only on loopback"
fi
[[ "$BAO_READY_TIMEOUT" =~ ^[0-9]+$ && "$BAO_READY_TIMEOUT" -ge 60 ]] ||
die "AIOS_OPENBAO_READY_TIMEOUT must be an integer of at least 60 seconds"
# Historical escape hatches are intentionally rejected. These are tier-one
# acceptance gates and cannot be lowered by an environment variable.
if [[ -n "${AIOS_REQUIRE_OPENBAO_READY+x}" ]] && ! is_true "${AIOS_REQUIRE_OPENBAO_READY}"; then
die "AIOS_REQUIRE_OPENBAO_READY cannot disable the OpenBao readiness gate"
fi
if [[ -n "${AIOS_REQUIRE_BRANCH_PROTECTION+x}" ]] && ! is_true "${AIOS_REQUIRE_BRANCH_PROTECTION}"; then
die "AIOS_REQUIRE_BRANCH_PROTECTION cannot disable branch protection"
fi
local path
for path in \
"$ROOT" "$BAO_ROOT" "$ETC_ROOT" "$BAO_ETC" "$CREDENTIAL_ROOT" \
"$RELEASE_LOCK" "$EVIDENCE" "$STATUS_FILE" "$GITEA_TOKEN_FILE" \
"$APPROLE_ROLE_ID_FILE" "$APPROLE_SECRET_ID_FILE" "$APPROLE_SECRET_ID_CRED" \
"$CHARTER_FILE" "$AUDIT_DIR" "$AUDIT_LOG" "$BACKUP_STATUS" \
"$RESTORE_STATUS" "$FAULT_STATUS" "$ACCEPTANCE_STATUS" \
"$OPTIMIZATION_LEDGER" "$ACCEPTANCE_SCRIPT_FINGERPRINT" "$PACKAGE_DIR"; do
[[ "$path" == /* && "$path" != *$'\n'* ]] || die "unsafe non-absolute path: $path"
done
}
random_secret() {
python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(48))
PY
}
atomic_json_write() {
local target="$1"
local mode="${2:-0600}"
local owner="${3:-root}"
local group="${4:-root}"
local temp
temp="$(mktemp "${target}.tmp.XXXXXX")"
cat >"$temp"
python3 -m json.tool "$temp" >/dev/null
chown "$owner:$group" "$temp"
chmod "$mode" "$temp"
mv -f "$temp" "$target"
}
write_status() {
local state="$1"
local reason="$2"
local evidence_group="root"
getent group aios-autonomy >/dev/null 2>&1 && evidence_group="aios-autonomy"
install -d -m 0750 -o root -g "$evidence_group" "$ROOT/state"
python3 - "$state" "$reason" "$SELECTED_VERSION" <<'PY' | atomic_json_write "$STATUS_FILE" 0640 root "$evidence_group"
import json, sys, time
state, reason, version = sys.argv[1:]
print(json.dumps({
"schema": "AIOS_AUTONOMY_STATUS_V1",
"state": state,
"reason": reason,
"openbao_version": version or None,
"last_health_check": None,
"last_successful_backup": None,
"last_restore_test": None,
"last_update_check": None,
"last_optimization": None,
"active_git_branch": None,
"production_change_in_progress": False,
"human_action_required": False,
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}, sort_keys=True))
PY
}
write_evidence() {
local rc="$1"
local result="FAILED"
[[ "$rc" -eq 0 ]] && result="PARTIALLY_COMPLETED"
local evidence_group="root"
getent group aios-autonomy >/dev/null 2>&1 && evidence_group="aios-autonomy"
install -d -m 0750 -o root -g "$evidence_group" "$ROOT/bootstrap"
env \
AIOS_E_RESULT="$result" \
AIOS_E_RC="$rc" \
AIOS_E_PHASE="$PHASE" \
AIOS_E_FAILURE="$FAILURE_REASON" \
AIOS_E_SCRIPT_VERSION="$SCRIPT_VERSION" \
AIOS_E_SCRIPT_SHA256="$SCRIPT_SHA256" \
AIOS_E_VERSION="$SELECTED_VERSION" \
AIOS_E_ASSET="$SELECTED_ASSET" \
AIOS_E_SOURCE="$SELECTED_SOURCE" \
AIOS_E_SHA="$SELECTED_SHA256" \
AIOS_E_SIGNING_FPR="$SELECTED_SIGNING_FPR" \
AIOS_E_SHA_OK="$RELEASE_SHA256_VERIFIED" \
AIOS_E_GPG_OK="$RELEASE_GPG_VERIFIED" \
AIOS_E_SIGSTORE_OK="$RELEASE_SIGSTORE_VERIFIED" \
AIOS_E_BINARY_OK="$RELEASE_BINARY_HELP_VERIFIED" \
AIOS_E_RUNTIME_OK="$RELEASE_RUNTIME_READY_VERIFIED" \
AIOS_E_SEAL_STORAGE="$SEAL_STORAGE" \
AIOS_E_SEAL_LEN="$SEAL_KEY_LENGTH_VERIFIED" \
AIOS_E_SEAL_READ="$SEAL_RUNTIME_READ_VERIFIED" \
AIOS_E_SEAL_REBOOT="$SEAL_REBOOT_READ_VERIFIED" \
AIOS_E_OPENBAO_SELF_INIT_CONFIGURED="$OPENBAO_SELF_INIT_CONFIGURED" \
AIOS_E_OPENBAO_INITIALIZED="$OPENBAO_INITIALIZED_VERIFIED" \
AIOS_E_OPENBAO_UNSEALED="$OPENBAO_UNSEALED_VERIFIED" \
AIOS_E_OPENBAO_AUDIT="$OPENBAO_AUDIT_VERIFIED" \
AIOS_E_OPENBAO_TRANSIT="$OPENBAO_TRANSIT_VERIFIED" \
AIOS_E_OPENBAO_KV="$OPENBAO_KV_VERIFIED" \
AIOS_E_OPENBAO_APPROLE="$OPENBAO_APPROLE_VERIFIED" \
AIOS_E_OPENBAO_POLICY="$OPENBAO_POLICY_VERIFIED" \
AIOS_E_OPENBAO_CHARTER_STORED="$OPENBAO_CHARTER_STORED" \
AIOS_E_OPENBAO_CHARTER_SIGNED="$OPENBAO_CHARTER_SIGNED" \
AIOS_E_OPENBAO_CHARTER_VERIFIED="$OPENBAO_CHARTER_SIGNATURE_VERIFIED" \
AIOS_E_OPENBAO_INIT_SECRET_REMOVED="$OPENBAO_INIT_SECRET_REMOVED" \
AIOS_E_GITEA_VERSION="$GITEA_VERSION" \
AIOS_E_MACHINE_CREATED="$MACHINE_IDENTITY_CREATED" \
AIOS_E_MACHINE_CRED="$MACHINE_CREDENTIAL_PERSISTED" \
AIOS_E_MACHINE_AUTH="$MACHINE_AUTHENTICATED" \
AIOS_E_MACHINE_WRITE="$MACHINE_WRITE_PERMISSION_VERIFIED" \
AIOS_E_MACHINE_PUSH="$MACHINE_NON_MAIN_PUSH_VERIFIED" \
AIOS_E_BRANCH_PROTECTED="$DEFAULT_BRANCH_PROTECTION_VERIFIED" \
AIOS_E_TAG_PROTECTED="$TAG_PROTECTION_VERIFIED" \
AIOS_E_MACHINE_CRED_STORAGE="$MACHINE_CREDENTIAL_STORAGE" \
AIOS_E_TEMP_TOKEN_REVOKED="$TEMP_ADMIN_TOKEN_REVOKED" \
AIOS_E_TEMP_ADMIN_REMOVED="$TEMP_ADMIN_REMOVED" \
AIOS_E_SERVICES_ENABLED="$SERVICES_ENABLED" \
AIOS_E_SERVICES_STARTED="$SERVICES_STARTED" \
AIOS_E_SERVICES_HEALTHY="$SERVICES_HEALTHY" \
AIOS_E_ACCEPTANCE_STARTED="$ACCEPTANCE_STARTED" \
AIOS_E_HOST_MUTATED="$HOST_MUTATED" \
AIOS_E_GITEA_MUTATED="$GITEA_MUTATED" \
AIOS_E_SERVICE_STARTED="$PRODUCTION_SERVICE_STARTED" \
AIOS_E_TRAFFIC_CHANGED="$PRODUCTION_TRAFFIC_CHANGED" \
python3 - <<'PY' | atomic_json_write "$EVIDENCE" 0640 root "$evidence_group"
import json, os, time
b = lambda name: os.environ.get(name, "false").lower() == "true"
i = lambda name: int(os.environ.get(name, "1"))
out = {
"schema": "AIOS_OPENBAO_BOOTSTRAP_EVIDENCE_V2",
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"execution_status": os.environ["AIOS_E_RESULT"],
"exit_code": i("AIOS_E_RC"),
"phase": os.environ.get("AIOS_E_PHASE"),
"failure_reason": os.environ.get("AIOS_E_FAILURE") or None,
"script_version": os.environ["AIOS_E_SCRIPT_VERSION"],
"script_sha256": os.environ["AIOS_E_SCRIPT_SHA256"],
"target_state": {
"name": "OPENBAO_AUTONOMY_BOOTSTRAP_READY",
"value": False,
"reason": "72-hour unattended acceptance has not completed",
},
"release": {
"version": os.environ.get("AIOS_E_VERSION") or None,
"asset": os.environ.get("AIOS_E_ASSET") or None,
"source": os.environ.get("AIOS_E_SOURCE") or None,
"sha256": os.environ.get("AIOS_E_SHA") or None,
"signing_fingerprint": os.environ.get("AIOS_E_SIGNING_FPR") or None,
"sha256_verified": b("AIOS_E_SHA_OK"),
"gpg_verified": b("AIOS_E_GPG_OK"),
"sigstore_verified": b("AIOS_E_SIGSTORE_OK"),
"binary_help_verified": b("AIOS_E_BINARY_OK"),
"runtime_ready_verified": b("AIOS_E_RUNTIME_OK"),
},
"seal": {
"storage": os.environ.get("AIOS_E_SEAL_STORAGE", "unknown"),
"key_length_verified": b("AIOS_E_SEAL_LEN"),
"runtime_read_verified": b("AIOS_E_SEAL_READ"),
"reboot_read_verified": b("AIOS_E_SEAL_REBOOT"),
},
"openbao": {
"self_init_configured": b("AIOS_E_OPENBAO_SELF_INIT_CONFIGURED"),
"initialized_verified": b("AIOS_E_OPENBAO_INITIALIZED"),
"unsealed_verified": b("AIOS_E_OPENBAO_UNSEALED"),
"audit_verified": b("AIOS_E_OPENBAO_AUDIT"),
"transit_verified": b("AIOS_E_OPENBAO_TRANSIT"),
"kv_v2_verified": b("AIOS_E_OPENBAO_KV"),
"approle_verified": b("AIOS_E_OPENBAO_APPROLE"),
"policy_verified": b("AIOS_E_OPENBAO_POLICY"),
"charter_stored": b("AIOS_E_OPENBAO_CHARTER_STORED"),
"charter_signed": b("AIOS_E_OPENBAO_CHARTER_SIGNED"),
"charter_signature_verified": b("AIOS_E_OPENBAO_CHARTER_VERIFIED"),
"self_init_secret_removed": b("AIOS_E_OPENBAO_INIT_SECRET_REMOVED"),
},
"gitea": {
"version": os.environ.get("AIOS_E_GITEA_VERSION") or None,
"minimum_tag_protection_api_version": "1.23.0",
"machine_identity_created": b("AIOS_E_MACHINE_CREATED"),
"machine_credential_persisted": b("AIOS_E_MACHINE_CRED"),
"machine_authenticated": b("AIOS_E_MACHINE_AUTH"),
"write_permission_verified": b("AIOS_E_MACHINE_WRITE"),
"non_main_push_verified": b("AIOS_E_MACHINE_PUSH"),
"default_branch_protection_verified": b("AIOS_E_BRANCH_PROTECTED"),
"tag_protection_verified": b("AIOS_E_TAG_PROTECTED"),
"machine_credential_storage": os.environ.get("AIOS_E_MACHINE_CRED_STORAGE", "unknown"),
"temporary_token_revoked": b("AIOS_E_TEMP_TOKEN_REVOKED"),
"temporary_admin_removed": b("AIOS_E_TEMP_ADMIN_REMOVED"),
},
"services": {
"enabled": b("AIOS_E_SERVICES_ENABLED"),
"started": b("AIOS_E_SERVICES_STARTED"),
"healthy": b("AIOS_E_SERVICES_HEALTHY"),
"unattended_acceptance_started": b("AIOS_E_ACCEPTANCE_STARTED"),
},
"mutations": {
"host_mutated": b("AIOS_E_HOST_MUTATED"),
"gitea_mutated": b("AIOS_E_GITEA_MUTATED"),
"production_service_started": b("AIOS_E_SERVICE_STARTED"),
"production_traffic_changed": b("AIOS_E_TRAFFIC_CHANGED"),
},
"governance": {
"force_push_used": False,
"tag_rewritten": False,
"direct_main_push": False,
"secrets_committed": False,
"owner_approval_respected": True,
},
}
print(json.dumps(out, sort_keys=True))
PY
}
gitea_global_args=()
configure_gitea_cli() {
[[ -x "$GITEA_BIN" ]] || die "local Gitea CLI unavailable: $GITEA_BIN"
gitea_global_args=()
[[ -n "$GITEA_CONFIG" ]] && gitea_global_args+=(--config "$GITEA_CONFIG")
[[ -n "$GITEA_WORK_PATH" ]] && gitea_global_args+=(--work-path "$GITEA_WORK_PATH")
}
gitea_cli() {
if [[ -n "$GITEA_RUN_USER" && "$GITEA_RUN_USER" != "root" ]]; then
runuser -u "$GITEA_RUN_USER" -- "$GITEA_BIN" "${gitea_global_args[@]}" "$@"
else
"$GITEA_BIN" "${gitea_global_args[@]}" "$@"
fi
}
gitea_user_exists() {
local username="$1"
local out="$TEMP_DIR/gitea-users.txt"
if gitea_cli admin user list --help 2>&1 | grep -q -- '--csv'; then
gitea_cli admin user list --csv >"$out"
python3 - "$out" "$username" <<'PY'
import csv, sys
path, wanted = sys.argv[1:]
with open(path, newline="") as f:
rows = csv.reader(f)
for row in rows:
if wanted in row:
raise SystemExit(0)
raise SystemExit(1)
PY
else
gitea_cli admin user list >"$out"
awk -v wanted="$username" '
{
for (i=1; i<=NF; i++) if ($i==wanted) found=1
}
END { exit(found ? 0 : 1) }
' "$out"
fi
}
curl_config_token() {
local file="$1"
local token_file="$2"
local token
token="$(cat "$token_file")"
[[ -n "$token" ]] || die "empty token file: $token_file"
[[ "$token" =~ ^[A-Za-z0-9._~-]+$ ]] || die "token contains unexpected characters"
{
printf '%s\n' "silent" "show-error" "location"
printf 'header = "Authorization: token %s"\n' "$token"
printf '%s\n' 'header = "Content-Type: application/json"'
} >"$file"
chmod 0600 "$file"
unset token
}
api_with_config() {
local config="$1"
local method="$2"
local path="$3"
local output="$4"
local data_file="${5:-}"
local args=(--config "$config" --fail --request "$method" --output "$output")
[[ -n "$data_file" ]] && args+=(--data-binary "@$data_file")
curl "${args[@]}" "${GITEA_URL%/}/api/v1$path"
}
fetch_paginated_json_array() {
local config="$1"
local base_path="$2"
local output="$3"
local page_dir
page_dir="$(mktemp -d "$TEMP_DIR/gitea-pages.XXXXXX")"
printf '%s\n' '[]' >"$output"
local page=1
local completed=false
while ((page <= 100)); do
local page_file="$page_dir/page-$page.json"
local separator="?"
[[ "$base_path" == *\?* ]] && separator="&"
api_with_config "$config" GET \
"${base_path}${separator}limit=50&page=$page" \
"$page_file"
local count
count="$(
python3 - "$page_file" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
if not isinstance(items, list):
raise SystemExit("paginated endpoint did not return an array")
print(len(items))
PY
)"
local merged="$page_dir/merged-$page.json"
python3 - "$output" "$page_file" "$merged" <<'PY'
import json, sys
left = json.load(open(sys.argv[1]))
right = json.load(open(sys.argv[2]))
if not isinstance(left, list) or not isinstance(right, list):
raise SystemExit("invalid pagination input")
json.dump(left + right, open(sys.argv[3], "w"), separators=(",", ":"))
PY
mv -f "$merged" "$output"
if [[ "$count" == "0" ]]; then
completed=true
break
fi
page=$((page + 1))
done
[[ "$completed" == true ]] ||
die "Gitea pagination exceeded 100 pages; refusing incomplete authorization proof"
rm -rf "$page_dir"
}
cleanup_gitea_credentials() {
set +e
if [[ -n "$TEMP_ADMIN_USER" && "$TEMP_ADMIN_REMOVED" != true ]]; then
if gitea_user_exists "$TEMP_ADMIN_USER"; then
gitea_cli admin user delete --username "$TEMP_ADMIN_USER" >/dev/null 2>&1
fi
if ! gitea_user_exists "$TEMP_ADMIN_USER"; then
TEMP_ADMIN_REMOVED=true
fi
fi
if [[ -n "$TEMP_ADMIN_TOKEN" && "$TEMP_ADMIN_REMOVED" == true && "$TEMP_ADMIN_TOKEN_REVOKED" != true ]]; then
local token_file="$TEMP_DIR/temp-admin-revocation-check.token"
local cfg="$TEMP_DIR/temp-admin-revocation-check.curl"
local out="$TEMP_DIR/temp-admin-revocation-check.json"
local http_code
printf '%s' "$TEMP_ADMIN_TOKEN" >"$token_file"
chmod 0600 "$token_file"
curl_config_token "$cfg" "$token_file"
http_code="$(
curl --config "$cfg" --request GET --output "$out" --write-out '%{http_code}' \
"${GITEA_URL%/}/api/v1/user" 2>/dev/null
)"
if [[ "$http_code" == "401" || "$http_code" == "403" ]]; then
TEMP_ADMIN_TOKEN_REVOKED=true
fi
fi
if [[ "$TEMP_ADMIN_TOKEN_REVOKED" == true ]]; then
TEMP_ADMIN_TOKEN=""
fi
set -e
}
rollback_binary_link() {
set +e
if [[ -n "$PREVIOUS_CURRENT_TARGET" && -e "$PREVIOUS_CURRENT_TARGET" ]]; then
ln -sfn "$PREVIOUS_CURRENT_TARGET" "$CURRENT_LINK"
ln -sfn "$CURRENT_LINK/bao" "$BAO_LINK"
fi
set -e
}
cleanup() {
local rc=$?
trap - EXIT INT TERM
CLEANUP_RUNNING=true
cleanup_gitea_credentials
if [[ "$rc" -ne 0 ]]; then
set +e
if [[ "$PRODUCTION_SERVICE_STARTED" == true && "$SERVICES_HEALTHY" != true ]]; then
systemctl stop "$CONTROLLER_SERVICE" "$BAO_SERVICE" >/dev/null 2>&1 || true
fi
rollback_binary_link
if [[ "$BAO_WAS_ACTIVE" == true ]]; then
systemctl start "$BAO_SERVICE" >/dev/null 2>&1 || true
fi
if [[ "$CONTROLLER_WAS_ACTIVE" == true ]]; then
systemctl start "$CONTROLLER_SERVICE" >/dev/null 2>&1 || true
fi
set -e
fi
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
find "$TEMP_DIR" -type f -exec shred -u {} + 2>/dev/null || true
rm -rf -- "$TEMP_DIR"
fi
if [[ "$rc" -eq 0 ]]; then
FINAL_RC=0
FAILURE_REASON=""
else
FINAL_RC="$rc"
[[ -n "$FAILURE_REASON" ]] || FAILURE_REASON="unexpected failure in phase $PHASE"
fi
write_evidence "$FINAL_RC" || true
if [[ "$FINAL_RC" -eq 0 ]]; then
write_status "VERIFYING" "BOOTSTRAP_STARTED_AUTONOMOUS_ACCEPTANCE" || true
log "BOOTSTRAP_STARTED_AUTONOMOUS_ACCEPTANCE"
else
write_status "BLOCKED" "$FAILURE_REASON" || true
log "BLOCKED: $FAILURE_REASON"
fi
exit "$FINAL_RC"
}
on_err() {
local rc=$?
local line="$1"
FAILURE_REASON="${FAILURE_REASON:-unexpected command failure at line $line}"
return "$rc"
}
trap 'on_err "$LINENO"' ERR
trap cleanup EXIT
trap 'FAILURE_REASON="interrupted"; exit 130' INT TERM
preflight() {
PHASE="preflight"
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "root is required" 77
validate_configuration
for cmd in awk base64 cat chmod chown cp curl date find getent git gpg grep groupadd head hostname install ln mktemp openssl python3 readlink runuser sed sha256sum shred sort stat systemctl tar timeout tr useradd wc; do
need "$cmd"
done
[[ -d /run/systemd/system ]] || die "systemd is not PID 1 on the target host"
systemctl show-environment >/dev/null 2>&1 || die "systemd service manager unavailable"
systemctl is-active --quiet "$BAO_SERVICE" && BAO_WAS_ACTIVE=true || true
systemctl is-active --quiet "$CONTROLLER_SERVICE" && CONTROLLER_WAS_ACTIVE=true || true
if [[ "$BAO_WAS_ACTIVE" == true || "$CONTROLLER_WAS_ACTIVE" == true ]]; then
[[ -s "$MANAGED_MARKER" ]] ||
die "target service is active but is not marked as managed by this bootstrap"
fi
TEMP_DIR="$(mktemp -d "${XDG_RUNTIME_DIR:-/run}/aios-openbao-bootstrap.XXXXXX")"
chmod 0700 "$TEMP_DIR"
configure_gitea_cli
gitea_cli --version >"$TEMP_DIR/gitea-version.txt"
GITEA_VERSION="$(
python3 - "$TEMP_DIR/gitea-version.txt" <<'PY'
import re
import sys
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
match = re.search(r"\bGitea version\s+v?([0-9]+\.[0-9]+\.[0-9]+)", text, re.I)
if not match:
raise SystemExit("unable to parse Gitea semantic version")
print(match.group(1))
PY
)"
python3 - "$GITEA_VERSION" "$GITEA_MIN_TAG_PROTECTION_API_VERSION" <<'PY' ||
import sys
def version_tuple(value):
parts = value.split(".")
if len(parts) != 3 or any(not p.isdigit() for p in parts):
raise SystemExit(f"invalid semantic version: {value}")
return tuple(int(p) for p in parts)
actual = version_tuple(sys.argv[1])
minimum = version_tuple(sys.argv[2])
raise SystemExit(0 if actual >= minimum else 1)
PY
die "GITEA_TAG_PROTECTION_API_UNAVAILABLE: Gitea $GITEA_VERSION is below required $GITEA_MIN_TAG_PROTECTION_API_VERSION; no safe native REST path exists to deny machine tag writes"
# The compatibility gate above intentionally runs before persistent AIOS
# bootstrap directories, identities, credentials, repository permissions, or
# services are changed.
install -d -m 0750 -o root -g root "$ROOT" "$ROOT/bootstrap"
gitea_cli admin user generate-access-token --help >"$TEMP_DIR/gitea-token-help.txt" 2>&1
grep -q -- '--scopes' "$TEMP_DIR/gitea-token-help.txt" || die "Gitea CLI lacks scoped access-token support"
grep -q -- '--raw' "$TEMP_DIR/gitea-token-help.txt" || die "Gitea CLI lacks raw access-token output"
gitea_cli admin user create --help >"$TEMP_DIR/gitea-user-create-help.txt" 2>&1
grep -q -- '--random-password' "$TEMP_DIR/gitea-user-create-help.txt" || die "Gitea CLI lacks random-password user creation"
}
install_identities_and_directories() {
PHASE="identities-and-directories"
getent group aios-openbao >/dev/null || groupadd --system aios-openbao
getent passwd aios-openbao >/dev/null || useradd --system --gid aios-openbao --home "$BAO_ROOT" --shell /usr/sbin/nologin aios-openbao
getent group aios-autonomy >/dev/null || groupadd --system aios-autonomy
getent passwd aios-autonomy >/dev/null || useradd --system --gid aios-autonomy --home "$ROOT" --shell /usr/sbin/nologin aios-autonomy
install -d -m 0750 -o root -g aios-autonomy "$ROOT" "$ROOT/bootstrap"
install -d -m 0770 -o aios-autonomy -g aios-autonomy "$ROOT/state" "$ROOT/log" "$ROOT/optimization"
install -d -m 0770 -o root -g aios-openbao "$ROOT/backups" "$ROOT/restore-tests"
install -d -m 0750 -o aios-openbao -g aios-openbao "$BAO_ROOT" "$BAO_ROOT/raft"
install -d -m 0750 -o root -g aios-openbao "$SEAL_DIR"
install -d -m 0750 -o root -g aios-openbao "$BAO_ETC" "$TLS_DIR" "$CHARTER_DIR"
install -d -m 0750 -o root -g aios-autonomy "$CREDENTIAL_ROOT"
install -d -m 0750 -o aios-openbao -g aios-openbao "$AUDIT_DIR"
install -d -m 0755 -o root -g root "$RELEASES_DIR" "$(dirname "$CURRENT_LINK")" "$(dirname "$BAO_LINK")" "$LIBEXEC_DIR"
touch "$AUDIT_LOG"
chown aios-openbao:aios-openbao "$AUDIT_LOG"
chmod 0600 "$AUDIT_LOG"
HOST_MUTATED=true
write_status "BOOTSTRAPPING" "identities and directories installed"
}
capture_environment() {
PHASE="environment-capture"
local environment="$ROOT/bootstrap/environment.json"
env \
AIOS_ENV_GITEA_BIN="$GITEA_BIN" \
AIOS_ENV_GITEA_URL="$GITEA_URL" \
python3 - <<'PY' | atomic_json_write "$environment" 0640 root aios-autonomy
import json, os, platform, shutil, subprocess, time
def first(command):
try:
return subprocess.check_output(command, text=True, stderr=subprocess.DEVNULL).splitlines()[0]
except Exception:
return None
out = {
"schema": "AIOS_OPENBAO_ENVIRONMENT_V2",
"detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"system": platform.platform(),
"architecture": platform.machine(),
"systemd": first(["systemctl", "--version"]),
"gitea": first([os.environ["AIOS_ENV_GITEA_BIN"], "--version"]),
"gitea_url": os.environ["AIOS_ENV_GITEA_URL"],
"tpm2_tools": bool(shutil.which("tpm2_getcap")),
"systemd_creds": bool(shutil.which("systemd-creds")),
"cosign": bool(shutil.which("cosign")),
"selinux": first(["getenforce"]) if shutil.which("getenforce") else None,
}
print(json.dumps(out, sort_keys=True))
PY
}
select_release() {
PHASE="release-selection"
local release_json="$TEMP_DIR/release.json"
local lock_mode=false
if [[ -s "$RELEASE_LOCK" ]]; then
python3 -m json.tool "$RELEASE_LOCK" >/dev/null || die "existing release lock is invalid JSON"
read -r SELECTED_VERSION SELECTED_ASSET SELECTED_SOURCE < <(
python3 - "$RELEASE_LOCK" <<'PY'
import json, sys
x = json.load(open(sys.argv[1]))
for key in ("version", "artifact", "source"):
if not isinstance(x.get(key), str) or not x[key]:
raise SystemExit("invalid release lock")
print(x["version"], x["artifact"], x["source"])
PY
)
lock_mode=true
log "reusing locked OpenBao release version=$SELECTED_VERSION asset=$SELECTED_ASSET"
else
curl --fail --silent --show-error --location \
--retry 4 --retry-delay 2 --retry-connrefused --connect-timeout 15 --max-time 120 \
"$OPENBAO_RELEASE_API" -o "$release_json"
read -r SELECTED_VERSION SELECTED_ASSET SELECTED_SOURCE < <(
python3 - "$release_json" <<'PY'
import json, platform, re, sys
r = json.load(open(sys.argv[1]))
tag = str(r.get("tag_name", ""))
if r.get("draft") or r.get("prerelease") or not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", tag):
raise SystemExit("latest release is not a stable semantic version")
version = tag[1:]
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
if not arch:
raise SystemExit("unsupported architecture")
assets = {a["name"]: a["browser_download_url"] for a in r.get("assets", [])}
candidates = [
f"openbao_{version}_linux_{arch}.tar.gz",
f"bao_{version}_linux_{arch}.tar.gz",
f"bao_{version}_Linux_{arch}.tar.gz",
]
matches = [name for name in candidates if name in assets]
if len(matches) != 1:
raise SystemExit("expected exactly one compatible Linux archive")
name = matches[0]
print(version, name, assets[name])
PY
) || die "unable to select one official stable OpenBao release asset"
fi
[[ "$SELECTED_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid selected release version"
python3 - "$SELECTED_VERSION" <<'PY'
import sys
parts=tuple(int(x) for x in sys.argv[1].split("."))
raise SystemExit(0 if parts >= (2, 6, 0) else "OpenBao 2.6.0 or newer is required for declarative self-initialization")
PY
[[ "$SELECTED_ASSET" != */* && "$SELECTED_ASSET" =~ \.tar\.gz$ ]] || die "unsafe selected asset name"
[[ "$SELECTED_SOURCE" == https://github.com/openbao/openbao/releases/download/* ]] || die "release source is not an official OpenBao GitHub release URL"
local release_base="${SELECTED_SOURCE%/*}"
printf '%s\n' "$lock_mode" >"$TEMP_DIR/release-lock-mode"
printf '%s\n' "$release_base/checksums.txt" >"$TEMP_DIR/checksums-url"
printf '%s\n' "$release_base/checksums.txt.gpgsig" >"$TEMP_DIR/gpgsig-url"
printf '%s\n' "$release_base/checksums.txt.sigstore.json" >"$TEMP_DIR/sigstore-url"
}
download_and_verify_release() {
PHASE="release-verification"
local archive="$TEMP_DIR/$SELECTED_ASSET"
local checksums="$TEMP_DIR/checksums.txt"
local gpgsig="$TEMP_DIR/checksums.txt.gpgsig"
local sigstore="$TEMP_DIR/checksums.txt.sigstore.json"
local gpg_key="$TEMP_DIR/openbao-signing-key.asc"
local gnupg="$TEMP_DIR/gnupg"
local checksum_line="$TEMP_DIR/selected.checksum"
curl --fail --silent --show-error --location --retry 4 --retry-delay 2 --retry-connrefused --connect-timeout 15 --max-time 600 "$SELECTED_SOURCE" -o "$archive"
curl --fail --silent --show-error --location --retry 4 --retry-delay 2 --retry-connrefused --connect-timeout 15 --max-time 120 "$(cat "$TEMP_DIR/checksums-url")" -o "$checksums"
curl --fail --silent --show-error --location --retry 4 --retry-delay 2 --retry-connrefused --connect-timeout 15 --max-time 120 "$(cat "$TEMP_DIR/gpgsig-url")" -o "$gpgsig"
curl --fail --silent --show-error --location --retry 4 --retry-delay 2 --retry-connrefused --connect-timeout 15 --max-time 120 "$OPENBAO_GPG_KEY_URL" -o "$gpg_key"
python3 - "$checksums" "$SELECTED_ASSET" >"$checksum_line" <<'PY'
import re, sys
path, asset = sys.argv[1:]
if "/" in asset or "\\" in asset:
raise SystemExit("unsafe asset")
pattern = re.compile(r"^([0-9a-fA-F]{64}) ([ *])(.+)$")
matches = []
for raw in open(path, encoding="utf-8"):
line = raw.rstrip("\n")
m = pattern.match(line)
if m and m.group(3) == asset:
matches.append((m.group(1).lower(), asset))
if len(matches) != 1:
raise SystemExit("checksum entry count must be exactly one")
print(f"{matches[0][0]} {asset}")
PY
SELECTED_SHA256="$(awk '{print $1}' "$checksum_line")"
(
cd "$TEMP_DIR"
sha256sum --check "$(basename "$checksum_line")"
) | grep -Fx "$SELECTED_ASSET: OK" >/dev/null || die "OpenBao archive SHA-256 verification failed"
RELEASE_SHA256_VERIFIED=true
install -d -m 0700 "$gnupg"
gpg --homedir "$gnupg" --batch --quiet --import "$gpg_key"
local imported_primary
imported_primary="$(
gpg --homedir "$gnupg" --batch --with-colons --fingerprint |
awk -F: '$1=="pub"{want=1;next} want && $1=="fpr"{print $10; exit}'
)"
[[ "$imported_primary" == "$OPENBAO_PRIMARY_GPG_FPR" ]] || die "OpenBao signing primary-key fingerprint mismatch"
local status="$TEMP_DIR/gpg-status.txt"
gpg --homedir "$gnupg" --batch --status-fd 1 --verify "$gpgsig" "$checksums" >"$status" 2>"$TEMP_DIR/gpg-stderr.txt" ||
die "OpenBao checksum GPG signature invalid"
SELECTED_SIGNING_FPR="$(
awk '$2=="VALIDSIG"{print $NF; exit}' "$status"
)"
[[ "$SELECTED_SIGNING_FPR" == "$OPENBAO_PRIMARY_GPG_FPR" ]] ||
die "valid signature is not rooted in the approved OpenBao primary key"
RELEASE_GPG_VERIFIED=true
if command -v cosign >/dev/null 2>&1; then
if curl --fail --silent --show-error --location --retry 2 --connect-timeout 15 --max-time 120 "$(cat "$TEMP_DIR/sigstore-url")" -o "$sigstore"; then
cosign verify-blob \
--bundle "$sigstore" \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
--certificate-identity-regexp='https://github.com/openbao/openbao/.github/workflows/release.yml@refs/heads/(main|release/)' \
"$checksums" >/dev/null
RELEASE_SIGSTORE_VERIFIED=true
else
log "sigstore bundle unavailable; mandatory GPG verification already succeeded"
fi
fi
tar -tzf "$archive" >"$TEMP_DIR/archive-list.txt"
python3 - "$TEMP_DIR/archive-list.txt" <<'PY'
import sys
items = [x.strip() for x in open(sys.argv[1]) if x.strip()]
bad = [x for x in items if x.startswith("/") or ".." in x.split("/")]
if bad:
raise SystemExit("unsafe archive paths")
if not any(x == "bao" or x.endswith("/bao") for x in items):
raise SystemExit("bao binary missing")
PY
local extract="$TEMP_DIR/extract"
install -d -m 0700 "$extract"
tar -xzf "$archive" -C "$extract"
local bao_src
bao_src="$(find "$extract" -type f -name bao -perm -0100 -print -quit)"
[[ -n "$bao_src" ]] || die "extracted OpenBao binary missing"
"$bao_src" -h >/dev/null
RELEASE_BINARY_HELP_VERIFIED=true
printf '%s\n' "$bao_src" >"$TEMP_DIR/bao-source"
}
install_release() {
PHASE="release-installation"
local version_dir="$RELEASES_DIR/$SELECTED_VERSION"
local bao_src
bao_src="$(cat "$TEMP_DIR/bao-source")"
install -d -m 0755 -o root -g root "$version_dir"
if [[ -f "$version_dir/bao" ]]; then
local existing_sha new_sha
existing_sha="$(sha256sum "$version_dir/bao" | awk '{print $1}')"
new_sha="$(sha256sum "$bao_src" | awk '{print $1}')"
[[ "$existing_sha" == "$new_sha" ]] || die "installed version directory contains a different binary"
else
install -m 0755 -o root -g root "$bao_src" "$version_dir/bao"
fi
if [[ -L "$CURRENT_LINK" ]]; then
PREVIOUS_CURRENT_TARGET="$(readlink -f "$CURRENT_LINK" || true)"
fi
ln -sfn "$version_dir" "$CURRENT_LINK.new"
mv -Tf "$CURRENT_LINK.new" "$CURRENT_LINK"
ln -sfn "$CURRENT_LINK/bao" "$BAO_LINK.new"
mv -Tf "$BAO_LINK.new" "$BAO_LINK"
"$BAO_LINK" -h >/dev/null
HOST_MUTATED=true
if [[ ! -s "$RELEASE_LOCK" ]]; then
env \
AIOS_LOCK_VERSION="$SELECTED_VERSION" \
AIOS_LOCK_ASSET="$SELECTED_ASSET" \
AIOS_LOCK_SOURCE="$SELECTED_SOURCE" \
AIOS_LOCK_SHA="$SELECTED_SHA256" \
AIOS_LOCK_FPR="$SELECTED_SIGNING_FPR" \
AIOS_LOCK_GPG="$RELEASE_GPG_VERIFIED" \
AIOS_LOCK_SIGSTORE="$RELEASE_SIGSTORE_VERIFIED" \
python3 - <<'PY' | atomic_json_write "$RELEASE_LOCK" 0640 root aios-openbao
import json, os, platform, time
print(json.dumps({
"schema": "AIOS_OPENBAO_RELEASE_LOCK_V2",
"product": "openbao",
"version": os.environ["AIOS_LOCK_VERSION"],
"platform": "linux_" + {"x86_64":"amd64","amd64":"amd64","aarch64":"arm64","arm64":"arm64"}[platform.machine().lower()],
"artifact": os.environ["AIOS_LOCK_ASSET"],
"source": os.environ["AIOS_LOCK_SOURCE"],
"sha256": os.environ["AIOS_LOCK_SHA"],
"gpg_verified": os.environ["AIOS_LOCK_GPG"] == "true",
"sigstore_verified": os.environ["AIOS_LOCK_SIGSTORE"] == "true",
"signing_primary_fingerprint": os.environ["AIOS_LOCK_FPR"],
"selected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"binary_help_verified": True,
"runtime_ready_verified": False,
}, sort_keys=True))
PY
else
local locked_sha
locked_sha="$(python3 - "$RELEASE_LOCK" <<'PY'
import json, sys
print(json.load(open(sys.argv[1])).get("sha256", ""))
PY
)"
[[ "$locked_sha" == "$SELECTED_SHA256" ]] || die "downloaded release does not match existing release lock"
fi
}
mark_release_runtime_ready() {
local temp
temp="$(mktemp "${RELEASE_LOCK}.ready.XXXXXX")"
python3 - "$RELEASE_LOCK" "$temp" <<'PY'
import json, sys, time
src, out = sys.argv[1:]
x = json.load(open(src))
x["runtime_ready_verified"] = True
x["runtime_ready_verified_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
json.dump(x, open(out, "w"), sort_keys=True)
PY
python3 -m json.tool "$temp" >/dev/null
chown root:aios-openbao "$temp"
chmod 0640 "$temp"
mv -f "$temp" "$RELEASE_LOCK"
}
provision_static_seal() {
PHASE="static-seal"
local key_material="$TEMP_DIR/seal-key.plain"
local use_systemd_credential=false
if command -v systemd-creds >/dev/null 2>&1 &&
systemd-creds --help 2>&1 | grep -q -- '--with-key='; then
use_systemd_credential=true
fi
if [[ "$use_systemd_credential" == true ]]; then
SEAL_STORAGE="systemd-credential"
SEAL_RUNTIME_PATH="/run/credentials/${BAO_SERVICE}/seal-key"
if [[ -s "$SEAL_CRED_FILE" ]]; then
systemd-creds decrypt "$SEAL_CRED_FILE" "$key_material"
else
if [[ -s "$SEAL_KEY_FILE" ]]; then
cp --preserve=mode,ownership "$SEAL_KEY_FILE" "$key_material"
PENDING_LEGACY_SEAL_KEY_REMOVAL=true
else
openssl rand -out "$key_material" 32
fi
[[ "$(wc -c <"$key_material")" -eq 32 ]] || die "static seal key must be exactly 32 bytes"
systemd-creds setup >/dev/null
systemd-creds encrypt \
--name=seal-key \
--with-key=auto \
"$key_material" "$SEAL_CRED_FILE.tmp"
chown root:root "$SEAL_CRED_FILE.tmp"
chmod 0400 "$SEAL_CRED_FILE.tmp"
mv -f "$SEAL_CRED_FILE.tmp" "$SEAL_CRED_FILE"
HOST_MUTATED=true
fi
[[ -f "$SEAL_CRED_FILE" && ! -L "$SEAL_CRED_FILE" ]] ||
die "encrypted static seal credential must be a regular non-symlink file"
[[ "$(stat -c '%U:%G:%a' "$SEAL_CRED_FILE")" == "root:root:400" ]] ||
die "encrypted static seal credential ownership or mode is invalid"
[[ "$(wc -c <"$key_material")" -eq 32 ]] || die "decrypted static seal key must be exactly 32 bytes"
SEAL_KEY_LENGTH_VERIFIED=true
else
is_true "$ALLOW_FILE_SEAL_KEY" ||
die "systemd encrypted credentials unavailable and file seal-key fallback is disabled"
SEAL_STORAGE="file"
SEAL_RUNTIME_PATH="$SEAL_KEY_FILE"
if [[ ! -e "$SEAL_KEY_FILE" ]]; then
openssl rand -out "$SEAL_KEY_FILE.tmp" 32
chown root:aios-openbao "$SEAL_KEY_FILE.tmp"
chmod 0440 "$SEAL_KEY_FILE.tmp"
mv -f "$SEAL_KEY_FILE.tmp" "$SEAL_KEY_FILE"
HOST_MUTATED=true
fi
[[ -f "$SEAL_KEY_FILE" && ! -L "$SEAL_KEY_FILE" ]] ||
die "static seal key must be a regular non-symlink file"
[[ "$(stat -c '%U:%G:%a' "$SEAL_KEY_FILE")" == "root:aios-openbao:440" ]] ||
die "static seal key ownership or mode is invalid"
[[ "$(wc -c <"$SEAL_KEY_FILE")" -eq 32 ]] || die "static seal key must be exactly 32 bytes"
SEAL_KEY_LENGTH_VERIFIED=true
runuser -u aios-openbao -- test -r "$SEAL_KEY_FILE" ||
die "aios-openbao cannot read the static seal key"
runuser -u aios-openbao -- python3 - "$SEAL_KEY_FILE" <<'PY'
import sys
data = open(sys.argv[1], "rb").read()
raise SystemExit(0 if len(data) == 32 else 1)
PY
SEAL_RUNTIME_READ_VERIFIED=true
if getent passwd nobody >/dev/null; then
if runuser -u nobody -- test -r "$SEAL_KEY_FILE" 2>/dev/null; then
die "unauthorized user nobody can read the static seal key"
fi
fi
fi
local key_id_source expected_key_id
if [[ -s "$key_material" ]]; then
key_id_source="$key_material"
else
key_id_source="$SEAL_KEY_FILE"
fi
expected_key_id="sha256-$(sha256sum "$key_id_source" | awk '{print substr($1,1,32)}')"
if [[ ! -s "$SEAL_KEY_ID_FILE" ]]; then
printf '%s\n' "$expected_key_id" >"$SEAL_KEY_ID_FILE.tmp"
chown root:aios-openbao "$SEAL_KEY_ID_FILE.tmp"
chmod 0440 "$SEAL_KEY_ID_FILE.tmp"
mv -f "$SEAL_KEY_ID_FILE.tmp" "$SEAL_KEY_ID_FILE"
fi
[[ "$(stat -c '%U:%G:%a' "$SEAL_KEY_ID_FILE")" == "root:aios-openbao:440" ]] ||
die "static seal key-id ownership or mode is invalid"
[[ "$(tr -d '\r\n' <"$SEAL_KEY_ID_FILE")" == "$expected_key_id" ]] ||
die "static seal key-id does not match the current key material"
if [[ -s "$key_material" ]]; then
shred -u "$key_material" 2>/dev/null || rm -f "$key_material"
fi
}
provision_tls() {
PHASE="tls"
local hostname_fqdn
hostname_fqdn="$(hostname -f 2>/dev/null || hostname)"
local regenerate_ca=false
local regenerate_server=false
if [[ ! -s "$TLS_CA_KEY" || ! -s "$TLS_CA" ]]; then
regenerate_ca=true
else
openssl x509 -checkend 2592000 -noout -in "$TLS_CA" >/dev/null 2>&1 || regenerate_ca=true
local ca_cert_pub ca_key_pub
ca_cert_pub="$(openssl x509 -in "$TLS_CA" -pubkey -noout | openssl pkey -pubin -outform DER 2>/dev/null | sha256sum | awk '{print $1}')" || regenerate_ca=true
ca_key_pub="$(openssl pkey -in "$TLS_CA_KEY" -pubout -outform DER 2>/dev/null | sha256sum | awk '{print $1}')" || regenerate_ca=true
[[ "$ca_cert_pub" == "$ca_key_pub" ]] || regenerate_ca=true
fi
if [[ "$regenerate_ca" == true ]]; then
openssl req -x509 -newkey rsa:3072 -nodes -days 3650 \
-subj "/CN=AIOS OpenBao Local CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign,digitalSignature" \
-keyout "$TLS_CA_KEY.tmp" -out "$TLS_CA.tmp" >/dev/null 2>&1
chown root:root "$TLS_CA_KEY.tmp" "$TLS_CA.tmp"
chmod 0400 "$TLS_CA_KEY.tmp"
chmod 0444 "$TLS_CA.tmp"
mv -f "$TLS_CA_KEY.tmp" "$TLS_CA_KEY"
mv -f "$TLS_CA.tmp" "$TLS_CA"
regenerate_server=true
HOST_MUTATED=true
fi
if [[ ! -s "$TLS_KEY" || ! -s "$TLS_CERT" ]]; then
regenerate_server=true
else
openssl x509 -checkend 604800 -noout -in "$TLS_CERT" >/dev/null 2>&1 || regenerate_server=true
local cert_pub key_pub
cert_pub="$(openssl x509 -in "$TLS_CERT" -pubkey -noout | openssl pkey -pubin -outform DER 2>/dev/null | sha256sum | awk '{print $1}')" || regenerate_server=true
key_pub="$(openssl pkey -in "$TLS_KEY" -pubout -outform DER 2>/dev/null | sha256sum | awk '{print $1}')" || regenerate_server=true
[[ "$cert_pub" == "$key_pub" ]] || regenerate_server=true
openssl verify -CAfile "$TLS_CA" "$TLS_CERT" >/dev/null 2>&1 || regenerate_server=true
openssl x509 -in "$TLS_CERT" -noout -ext subjectAltName 2>/dev/null |
grep -Fq "IP Address:127.0.0.1" || regenerate_server=true
fi
if [[ "$regenerate_server" == true ]]; then
local req_cfg="$TEMP_DIR/openssl-server.cnf"
local csr="$TEMP_DIR/server.csr"
cat >"$req_cfg" <<EOF
[req]
distinguished_name = dn
prompt = no
[dn]
CN = openbao.local
[v3]
subjectAltName = DNS:localhost,DNS:${hostname_fqdn},IP:127.0.0.1
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
EOF
openssl req -new -newkey rsa:3072 -nodes \
-config "$req_cfg" \
-keyout "$TLS_KEY.tmp" -out "$csr" >/dev/null 2>&1
openssl x509 -req -days 825 \
-in "$csr" \
-CA "$TLS_CA" -CAkey "$TLS_CA_KEY" -CAcreateserial \
-extfile "$req_cfg" -extensions v3 \
-out "$TLS_CERT.tmp" >/dev/null 2>&1
rm -f "$TLS_DIR/ca.srl"
chown root:aios-openbao "$TLS_KEY.tmp" "$TLS_CERT.tmp"
chmod 0440 "$TLS_KEY.tmp"
chmod 0444 "$TLS_CERT.tmp"
mv -f "$TLS_KEY.tmp" "$TLS_KEY"
mv -f "$TLS_CERT.tmp" "$TLS_CERT"
HOST_MUTATED=true
fi
[[ "$(stat -c '%U:%G:%a' "$TLS_CA_KEY")" == "root:root:400" ]] ||
die "TLS CA private-key ownership or mode is invalid"
[[ "$(stat -c '%U:%G:%a' "$TLS_KEY")" == "root:aios-openbao:440" ]] ||
die "TLS server private-key ownership or mode is invalid"
openssl verify -CAfile "$TLS_CA" "$TLS_CERT" >/dev/null ||
die "TLS server certificate verification failed"
runuser -u aios-openbao -- test -r "$TLS_KEY" ||
die "aios-openbao cannot read the TLS private key"
if getent passwd nobody >/dev/null && runuser -u nobody -- test -r "$TLS_KEY" 2>/dev/null; then
die "unauthorized user nobody can read the TLS private key"
fi
}
provision_openbao_bootstrap_identity() {
PHASE="openbao-bootstrap-identity"
local plain="$TEMP_DIR/openbao-approle-secret-id.plain"
local use_systemd_credential=false
if [[ ! -s "$APPROLE_ROLE_ID_FILE" ]]; then
openssl rand -hex 32 >"$APPROLE_ROLE_ID_FILE.tmp"
chown root:root "$APPROLE_ROLE_ID_FILE.tmp"
chmod 0444 "$APPROLE_ROLE_ID_FILE.tmp"
mv -f "$APPROLE_ROLE_ID_FILE.tmp" "$APPROLE_ROLE_ID_FILE"
fi
[[ "$(wc -c <"$APPROLE_ROLE_ID_FILE")" -eq 65 ]] ||
die "AppRole RoleID file has an unexpected length"
if command -v systemd-creds >/dev/null 2>&1 &&
systemd-creds --help 2>&1 | grep -q -- '--with-key='; then
use_systemd_credential=true
fi
if [[ "$use_systemd_credential" == true ]]; then
APPROLE_SECRET_STORAGE="systemd-credential"
APPROLE_SECRET_RUNTIME_PATH="/run/credentials/${CONTROLLER_SERVICE}/openbao-approle-secret-id"
APPROLE_BACKUP_RUNTIME_PATH="/run/credentials/aios-openbao-backup.service/openbao-approle-secret-id"
APPROLE_FAULT_RUNTIME_PATH="/run/credentials/aios-openbao-fault-injection.service/openbao-approle-secret-id"
APPROLE_RESTORE_RUNTIME_PATH="/run/credentials/aios-openbao-restore-test.service/openbao-approle-secret-id"
if [[ -s "$APPROLE_SECRET_ID_CRED" ]]; then
systemd-creds decrypt "$APPROLE_SECRET_ID_CRED" "$plain"
else
if [[ -s "$APPROLE_SECRET_ID_FILE" ]]; then
cp "$APPROLE_SECRET_ID_FILE" "$plain"
else
openssl rand -hex 32 >"$plain"
fi
chmod 0600 "$plain"
systemd-creds setup >/dev/null
systemd-creds encrypt \
--name=openbao-approle-secret-id \
--with-key=auto \
"$plain" "$APPROLE_SECRET_ID_CRED.tmp"
chown root:root "$APPROLE_SECRET_ID_CRED.tmp"
chmod 0400 "$APPROLE_SECRET_ID_CRED.tmp"
mv -f "$APPROLE_SECRET_ID_CRED.tmp" "$APPROLE_SECRET_ID_CRED"
if [[ -f "$APPROLE_SECRET_ID_FILE" ]]; then
shred -u "$APPROLE_SECRET_ID_FILE" 2>/dev/null || rm -f "$APPROLE_SECRET_ID_FILE"
fi
fi
[[ -f "$APPROLE_SECRET_ID_CRED" && ! -L "$APPROLE_SECRET_ID_CRED" ]] ||
die "encrypted AppRole SecretID credential must be a regular file"
[[ "$(stat -c '%U:%G:%a' "$APPROLE_SECRET_ID_CRED")" == "root:root:400" ]] ||
die "encrypted AppRole SecretID credential ownership or mode is invalid"
else
APPROLE_SECRET_STORAGE="file"
APPROLE_SECRET_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_BACKUP_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_FAULT_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
APPROLE_RESTORE_RUNTIME_PATH="$APPROLE_SECRET_ID_FILE"
if [[ ! -s "$APPROLE_SECRET_ID_FILE" ]]; then
openssl rand -hex 32 >"$APPROLE_SECRET_ID_FILE.tmp"
chown root:aios-autonomy "$APPROLE_SECRET_ID_FILE.tmp"
chmod 0440 "$APPROLE_SECRET_ID_FILE.tmp"
mv -f "$APPROLE_SECRET_ID_FILE.tmp" "$APPROLE_SECRET_ID_FILE"
fi
cp "$APPROLE_SECRET_ID_FILE" "$plain"
[[ "$(stat -c '%U:%G:%a' "$APPROLE_SECRET_ID_FILE")" == "root:aios-autonomy:440" ]] ||
die "AppRole SecretID fallback ownership or mode is invalid"
fi
[[ "$(wc -c <"$plain")" -eq 65 ]] ||
die "AppRole SecretID must be a 32-byte value encoded as 64 hex characters"
grep -Eq '^[0-9a-f]{64}$' "$plain" || die "AppRole SecretID format is invalid"
python3 - "$CHARTER_FILE.tmp" "$SCRIPT_VERSION" "$SELECTED_VERSION" "$0" <<'PY'
import hashlib, json, pathlib, sys, time
out, script_version, openbao_version, script_path = sys.argv[1:]
script_bytes = pathlib.Path(script_path).read_bytes()
charter = {
"schema": "AIOS_OPENBAO_AUTONOMY_CHARTER_V1",
"charter_version": 1,
"issued_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"bootstrap_script_version": script_version,
"bootstrap_script_sha256": hashlib.sha256(script_bytes).hexdigest(),
"openbao_release": openbao_version,
"operating_model": {
"one_person_maintained": True,
"unattended": True,
"non_interactive": True,
"continuously_self_optimizing": True,
},
"immutable_governance": {
"direct_main_push": False,
"force_push": False,
"tag_rewrite": False,
"secrets_in_git": False,
"remote_consistency_required_before_release_success": True,
"owner_approval_required_for_production_release": True,
},
"failure_policy": {
"fail_closed": True,
"never_lower_tier_one_gates": True,
"automatic_rollback_for_failed_safe_changes": True,
},
}
pathlib.Path(out).write_text(json.dumps(charter, sort_keys=True, separators=(",", ":")) + "\n")
PY
chown root:root "$CHARTER_FILE.tmp"
chmod 0444 "$CHARTER_FILE.tmp"
mv -f "$CHARTER_FILE.tmp" "$CHARTER_FILE"
shred -u "$plain" 2>/dev/null || rm -f "$plain"
HOST_MUTATED=true
}
load_approle_secret() {
local output="$1"
if [[ "$APPROLE_SECRET_STORAGE" == "systemd-credential" ]]; then
systemd-creds decrypt "$APPROLE_SECRET_ID_CRED" "$output"
else
cp "$APPROLE_SECRET_ID_FILE" "$output"
fi
chmod 0600 "$output"
[[ "$(wc -c <"$output")" -eq 65 ]] || die "unable to load valid AppRole SecretID"
}
render_openbao_config() {
PHASE="openbao-configuration"
local key_id node_id
key_id="$(cat "$SEAL_KEY_ID_FILE")"
[[ "$key_id" =~ ^[A-Za-z0-9._-]+$ ]] || die "unsafe static seal key id"
node_id="$(hostname | tr -cd 'A-Za-z0-9._-' | head -c 48)"
[[ -n "$node_id" ]] || node_id="aios-openbao-1"
local role_id
role_id="$(tr -d '\r\n' <"$APPROLE_ROLE_ID_FILE")"
[[ "$role_id" =~ ^[0-9a-f]{64}$ ]] || die "invalid AppRole RoleID"
local secret_file="$TEMP_DIR/openbao-approle-secret-for-render"
local secret_id=""
local include_initialize=true
[[ -s "$OPENBAO_INIT_MARKER" ]] && include_initialize=false
if [[ "$include_initialize" == true ]]; then
load_approle_secret "$secret_file"
secret_id="$(tr -d '\r\n' <"$secret_file")"
[[ "$secret_id" =~ ^[0-9a-f]{64}$ ]] || die "invalid AppRole SecretID"
fi
cat >"$OPENBAO_CONFIG.tmp" <<EOF
cluster_name = "aios-openbao-autonomy-v1"
api_addr = "https://127.0.0.1:8200"
cluster_addr = "https://127.0.0.1:8201"
ui = false
log_level = "info"
storage "raft" {
path = "${BAO_ROOT}/raft"
node_id = "${node_id}"
}
listener "tcp" {
address = "127.0.0.1:8200"
cluster_address = "127.0.0.1:8201"
tls_cert_file = "${TLS_CERT}"
tls_key_file = "${TLS_KEY}"
tls_min_version = "tls13"
}
seal "static" {
current_key_id = "${key_id}"
current_key = "file://${SEAL_RUNTIME_PATH}"
}
telemetry {
prometheus_retention_time = "60s"
disable_hostname = true
}
# Declarative audit configuration is reconciled by OpenBao on each restart.
# This avoids enabling a file audit device through the unsafe API creation path.
audit "file" "aios" {
description = "AIOS autonomy audit log"
options {
file_path = "${AUDIT_LOG}"
mode = "0600"
format = "json"
log_raw = "false"
}
}
EOF
if [[ "$include_initialize" == true ]]; then
cat >>"$OPENBAO_CONFIG.tmp" <<EOF
# This block is consumed only on first startup. The bootstrap removes it after
# AppRole authentication, audit, KV, Transit and charter verification succeed.
initialize "secret_mounts" {
request "enable_transit" {
operation = "update"
path = "sys/mounts/transit"
data = {
type = "transit"
description = "AIOS runtime cryptography"
}
}
request "enable_kv_v2" {
operation = "update"
path = "sys/mounts/kv"
data = {
type = "kv"
description = "AIOS autonomy state and secret metadata"
options = {
version = "2"
}
}
}
request "create_runtime_transit_key" {
operation = "update"
path = "transit/keys/aios-runtime"
data = {
type = "aes256-gcm96"
exportable = false
allow_plaintext_backup = false
auto_rotate_period = "720h"
}
}
request "create_charter_signing_key" {
operation = "update"
path = "transit/keys/aios-charter"
data = {
type = "ed25519"
exportable = false
allow_plaintext_backup = false
auto_rotate_period = "720h"
}
}
}
initialize "autonomy_policy" {
request "write_autonomy_policy" {
operation = "update"
path = "sys/policies/acl/aios-autonomy"
data = {
policy = <<-POLICY
path "sys/health" {
capabilities = ["read"]
}
path "sys/seal-status" {
capabilities = ["read"]
}
path "sys/mounts" {
capabilities = ["read", "sudo"]
}
path "sys/auth" {
capabilities = ["read", "sudo"]
}
path "sys/audit" {
capabilities = ["read", "sudo"]
}
path "sys/policies/acl/aios-autonomy" {
capabilities = ["read", "sudo"]
}
path "sys/storage/raft/snapshot" {
capabilities = ["read", "sudo"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}
path "transit/keys/aios-runtime" {
capabilities = ["read"]
}
path "transit/encrypt/aios-runtime" {
capabilities = ["update"]
}
path "transit/decrypt/aios-runtime" {
capabilities = ["update"]
}
path "transit/keys/aios-charter" {
capabilities = ["read"]
}
path "transit/sign/aios-charter" {
capabilities = ["update"]
}
path "transit/verify/aios-charter" {
capabilities = ["update"]
}
path "kv/data/aios/*" {
capabilities = ["create", "update", "read", "patch"]
}
path "kv/metadata/aios/*" {
capabilities = ["read", "list"]
}
POLICY
}
}
}
initialize "approle" {
request "enable_approle" {
operation = "update"
path = "sys/auth/approle"
data = {
type = "approle"
description = "AIOS unattended machine authentication"
}
}
request "create_autonomy_role" {
operation = "update"
path = "auth/approle/role/aios-autonomy"
data = {
bind_secret_id = true
local_secret_ids = true
secret_id_bound_cidrs = ["127.0.0.1/32", "::1/128"]
secret_id_num_uses = 0
secret_id_ttl = "0"
token_bound_cidrs = ["127.0.0.1/32", "::1/128"]
token_no_default_policy = true
token_num_uses = 0
token_period = "1h"
token_policies = ["aios-autonomy"]
token_type = "service"
}
}
request "set_autonomy_role_id" {
operation = "update"
path = "auth/approle/role/aios-autonomy/role-id"
data = {
role_id = "${role_id}"
}
}
request "set_autonomy_secret_id" {
operation = "update"
path = "auth/approle/role/aios-autonomy/custom-secret-id"
data = {
secret_id = "${secret_id}"
cidr_list = ["127.0.0.1/32", "::1/128"]
token_bound_cidrs = ["127.0.0.1/32", "::1/128"]
num_uses = 0
ttl = "0"
}
}
}
EOF
OPENBAO_SELF_INIT_CONFIGURED=true
fi
chown root:aios-openbao "$OPENBAO_CONFIG.tmp"
chmod 0440 "$OPENBAO_CONFIG.tmp"
if "$BAO_LINK" server -h 2>&1 | grep -q -- '-verify-only'; then
local verify_config="$TEMP_DIR/openbao-verify.hcl"
cp "$OPENBAO_CONFIG.tmp" "$verify_config"
chmod 0600 "$verify_config"
if [[ "$SEAL_STORAGE" == "systemd-credential" ]]; then
local verify_seal_key="$TEMP_DIR/openbao-verify-seal.key"
systemd-creds decrypt "$SEAL_CRED_FILE" "$verify_seal_key"
chmod 0600 "$verify_seal_key"
python3 - "$verify_config" "$SEAL_RUNTIME_PATH" "$verify_seal_key" <<'PY'
import pathlib, sys
path, runtime_path, verification_path = sys.argv[1:]
p = pathlib.Path(path)
text = p.read_text()
needle = 'file://' + runtime_path
replacement = 'file://' + verification_path
if text.count(needle) != 1:
raise SystemExit("expected exactly one static seal runtime path")
p.write_text(text.replace(needle, replacement))
PY
fi
"$BAO_LINK" server -config="$verify_config" -verify-only >/dev/null
shred -u "$verify_config" 2>/dev/null || rm -f "$verify_config"
[[ -z "${verify_seal_key:-}" ]] ||
shred -u "$verify_seal_key" 2>/dev/null || rm -f "$verify_seal_key"
else
die "selected OpenBao release lacks server configuration verification"
fi
if [[ -f "$OPENBAO_CONFIG" && "$include_initialize" == false ]] &&
grep -q '^initialize "' "$OPENBAO_CONFIG"; then
shred -u "$OPENBAO_CONFIG" 2>/dev/null || rm -f "$OPENBAO_CONFIG"
fi
mv -f "$OPENBAO_CONFIG.tmp" "$OPENBAO_CONFIG"
if [[ -s "$secret_file" ]]; then
shred -u "$secret_file" 2>/dev/null || rm -f "$secret_file"
fi
unset secret_id
HOST_MUTATED=true
}
openbao_token_curl() {
local token_file="$1"
local method="$2"
local path="$3"
local output="$4"
local payload="${5:-}"
local cfg="$TEMP_DIR/openbao-token-$RANDOM.curl"
local code
[[ -s "$token_file" ]] || die "OpenBao token file is missing"
printf 'header = "X-Vault-Token: %s"\n' "$(cat "$token_file")" >"$cfg"
chmod 0600 "$cfg"
local args=(
--config "$cfg"
--silent --show-error
--cacert "$TLS_CA"
--request "$method"
--output "$output"
--write-out '%{http_code}'
--connect-timeout 10
--max-time 120
)
if [[ -n "$payload" ]]; then
args+=(--header 'Content-Type: application/json' --data-binary "@$payload")
fi
code="$(curl "${args[@]}" "https://127.0.0.1:8200/v1/${path}")" || {
shred -u "$cfg" 2>/dev/null || rm -f "$cfg"
return 1
}
shred -u "$cfg" 2>/dev/null || rm -f "$cfg"
[[ "$code" =~ ^2[0-9][0-9]$ ]]
}
openbao_login_to() {
local token_file="$1"
local secret_file="$TEMP_DIR/approle-login-secret-$RANDOM"
local payload="$TEMP_DIR/approle-login-payload-$RANDOM.json"
local response="$TEMP_DIR/approle-login-response-$RANDOM.json"
load_approle_secret "$secret_file"
python3 - "$APPROLE_ROLE_ID_FILE" "$secret_file" "$payload" <<'PY'
import json, pathlib, sys
role_file, secret_file, output = sys.argv[1:]
role_id = pathlib.Path(role_file).read_text().strip()
secret_id = pathlib.Path(secret_file).read_text().strip()
json.dump({"role_id": role_id, "secret_id": secret_id}, open(output, "w"))
PY
chmod 0600 "$payload"
local code
code="$(
curl --silent --show-error --cacert "$TLS_CA" \
--request POST \
--header 'Content-Type: application/json' \
--data-binary "@$payload" \
--output "$response" \
--write-out '%{http_code}' \
--connect-timeout 10 --max-time 60 \
'https://127.0.0.1:8200/v1/auth/approle/login'
)" || code="000"
shred -u "$secret_file" "$payload" 2>/dev/null || rm -f "$secret_file" "$payload"
[[ "$code" == "200" ]] || {
rm -f "$response"
return 1
}
python3 - "$response" "$token_file.tmp" <<'PY'
import json, pathlib, sys
response, output = sys.argv[1:]
x=json.load(open(response))
auth=x.get("auth") or {}
token=auth.get("client_token")
policies=set(auth.get("policies") or auth.get("token_policies") or [])
if not isinstance(token, str) or not token:
raise SystemExit("AppRole login returned no token")
if "aios-autonomy" not in policies:
raise SystemExit("AppRole token lacks aios-autonomy policy")
pathlib.Path(output).write_text(token)
PY
chmod 0600 "$token_file.tmp"
mv -f "$token_file.tmp" "$token_file"
shred -u "$response" 2>/dev/null || rm -f "$response"
OPENBAO_APPROLE_VERIFIED=true
}
wait_openbao_ready() {
local timeout_seconds="${1:-$BAO_READY_TIMEOUT}"
local deadline=$((SECONDS + timeout_seconds))
local health="$TEMP_DIR/openbao-health-$RANDOM.json"
while ((SECONDS < deadline)); do
if curl --silent --show-error --cacert "$TLS_CA" \
--connect-timeout 3 --max-time 10 \
"${BAO_HEALTH_URL}?standbyok=true&perfstandbyok=true" \
-o "$health" 2>/dev/null; then
if python3 - "$health" <<'PY'
import json, sys
try:
x=json.load(open(sys.argv[1]))
except Exception:
raise SystemExit(1)
raise SystemExit(0 if x.get("initialized") is True and x.get("sealed") is False else 1)
PY
then
OPENBAO_INITIALIZED_VERIFIED=true
OPENBAO_UNSEALED_VERIFIED=true
SERVICES_HEALTHY=true
return 0
fi
fi
sleep 3
done
return 1
}
configure_openbao_runtime_and_charter() {
PHASE="openbao-runtime-configuration"
local token="$TEMP_DIR/openbao-autonomy.token"
local mounts="$TEMP_DIR/openbao-mounts.json"
local auths="$TEMP_DIR/openbao-auths.json"
local audits="$TEMP_DIR/openbao-audits.json"
local policy="$TEMP_DIR/openbao-policy.json"
local runtime_key="$TEMP_DIR/openbao-runtime-key.json"
local charter_key="$TEMP_DIR/openbao-charter-key.json"
local charter_payload="$TEMP_DIR/charter-kv-payload.json"
local charter_response="$TEMP_DIR/charter-kv-response.json"
local sign_payload="$TEMP_DIR/charter-sign-payload.json"
local sign_response="$TEMP_DIR/charter-sign-response.json"
local verify_payload="$TEMP_DIR/charter-verify-payload.json"
local verify_response="$TEMP_DIR/charter-verify-response.json"
local signature_payload="$TEMP_DIR/charter-signature-kv-payload.json"
local signature_response="$TEMP_DIR/charter-signature-kv-response.json"
local readback="$TEMP_DIR/charter-readback.json"
openbao_login_to "$token" || die "AppRole login failed after OpenBao self-initialization"
openbao_token_curl "$token" GET "sys/mounts" "$mounts" ||
die "unable to read OpenBao mounts"
python3 - "$mounts" <<'PY'
import json, sys
x=(json.load(open(sys.argv[1])).get("data") or {})
if "transit/" not in x or "kv/" not in x:
raise SystemExit("required Transit or KV mount is missing")
if str((x["kv/"].get("options") or {}).get("version")) != "2":
raise SystemExit("kv/ is not KV v2")
PY
OPENBAO_TRANSIT_VERIFIED=true
OPENBAO_KV_VERIFIED=true
openbao_token_curl "$token" GET "sys/auth" "$auths" ||
die "unable to read OpenBao auth methods"
python3 - "$auths" <<'PY'
import json, sys
x=(json.load(open(sys.argv[1])).get("data") or {})
raise SystemExit(0 if "approle/" in x else "AppRole auth mount is missing")
PY
openbao_token_curl "$token" GET "sys/audit" "$audits" ||
die "unable to read OpenBao audit devices"
python3 - "$audits" <<'PY'
import json, sys
x=(json.load(open(sys.argv[1])).get("data") or {})
raise SystemExit(0 if "aios/" in x else "AIOS audit device is missing")
PY
OPENBAO_AUDIT_VERIFIED=true
openbao_token_curl "$token" GET "sys/policies/acl/aios-autonomy" "$policy" ||
die "unable to read autonomy policy"
python3 - "$policy" <<'PY'
import json, sys
x=(json.load(open(sys.argv[1])).get("data") or {})
p=x.get("policy") or x.get("rules") or ""
required=("transit/sign/aios-charter","kv/data/aios/*","sys/storage/raft/snapshot")
raise SystemExit(0 if all(item in p for item in required) else "autonomy policy is incomplete")
PY
OPENBAO_POLICY_VERIFIED=true
openbao_token_curl "$token" GET "transit/keys/aios-runtime" "$runtime_key" ||
die "runtime Transit key is not readable"
openbao_token_curl "$token" GET "transit/keys/aios-charter" "$charter_key" ||
die "charter Transit key is not readable"
python3 - "$runtime_key" "$charter_key" <<'PY'
import json, sys
runtime=(json.load(open(sys.argv[1])).get("data") or {})
charter=(json.load(open(sys.argv[2])).get("data") or {})
if runtime.get("type") != "aes256-gcm96":
raise SystemExit("runtime Transit key type mismatch")
if charter.get("type") != "ed25519":
raise SystemExit("charter Transit key type mismatch")
PY
python3 - "$CHARTER_FILE" "$charter_payload" "$sign_payload" <<'PY'
import base64, hashlib, json, pathlib, sys, time
charter_file, kv_out, sign_out = sys.argv[1:]
raw=pathlib.Path(charter_file).read_bytes()
charter=json.loads(raw)
sha=hashlib.sha256(raw).hexdigest()
json.dump({"data":{
"charter":charter,
"charter_sha256":sha,
"stored_at":time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}}, open(kv_out,"w"), sort_keys=True)
json.dump({"input":base64.b64encode(raw).decode()}, open(sign_out,"w"))
PY
openbao_token_curl "$token" POST "kv/data/aios/charter" "$charter_response" "$charter_payload" ||
die "unable to store autonomy charter in KV v2"
OPENBAO_CHARTER_STORED=true
openbao_token_curl "$token" POST "transit/sign/aios-charter" "$sign_response" "$sign_payload" ||
die "unable to sign autonomy charter with Transit"
python3 - "$sign_response" "$sign_payload" "$verify_payload" "$CHARTER_FILE" "$signature_payload" "$CHARTER_SIGNATURE_FILE.tmp" <<'PY'
import hashlib, json, pathlib, sys, time
sign_response, sign_payload, verify_out, charter_file, kv_out, local_out = sys.argv[1:]
sig=(json.load(open(sign_response)).get("data") or {}).get("signature")
if not isinstance(sig,str) or not sig.startswith(("vault:v","bao:v")):
raise SystemExit("invalid Transit signature")
inp=json.load(open(sign_payload))["input"]
json.dump({"input":inp,"signature":sig},open(verify_out,"w"))
raw=pathlib.Path(charter_file).read_bytes()
record={
"schema":"AIOS_AUTONOMY_CHARTER_SIGNATURE_V1",
"charter_sha256":hashlib.sha256(raw).hexdigest(),
"key":"transit/keys/aios-charter",
"signature":sig,
"signed_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
json.dump({"data":record},open(kv_out,"w"),sort_keys=True)
pathlib.Path(local_out).write_text(json.dumps(record,sort_keys=True)+"\n")
PY
chown root:aios-autonomy "$CHARTER_SIGNATURE_FILE.tmp"
chmod 0640 "$CHARTER_SIGNATURE_FILE.tmp"
mv -f "$CHARTER_SIGNATURE_FILE.tmp" "$CHARTER_SIGNATURE_FILE"
OPENBAO_CHARTER_SIGNED=true
openbao_token_curl "$token" POST "transit/verify/aios-charter" "$verify_response" "$verify_payload" ||
die "unable to verify autonomy charter signature"
python3 - "$verify_response" <<'PY'
import json, sys
x=(json.load(open(sys.argv[1])).get("data") or {})
raise SystemExit(0 if x.get("valid") is True else "Transit charter signature is invalid")
PY
OPENBAO_CHARTER_SIGNATURE_VERIFIED=true
openbao_token_curl "$token" POST "kv/data/aios/charter-signature" "$signature_response" "$signature_payload" ||
die "unable to store charter signature record"
openbao_token_curl "$token" GET "kv/data/aios/charter" "$readback" ||
die "unable to read back stored autonomy charter"
python3 - "$readback" "$CHARTER_FILE" <<'PY'
import hashlib, json, pathlib, sys
x=((json.load(open(sys.argv[1])).get("data") or {}).get("data") or {})
sha=hashlib.sha256(pathlib.Path(sys.argv[2]).read_bytes()).hexdigest()
raise SystemExit(0 if x.get("charter_sha256")==sha else "stored charter hash mismatch")
PY
# Revoke the short-lived bootstrap session. The persistent credential is the
# host-bound AppRole SecretID, not this token.
openbao_token_curl "$token" POST "auth/token/revoke-self" "$TEMP_DIR/revoke-self.json" ||
die "unable to revoke bootstrap AppRole token"
shred -u "$token" 2>/dev/null || rm -f "$token"
python3 - "$OPENBAO_INIT_MARKER.tmp" "$SELECTED_VERSION" "$CHARTER_FILE" <<'PY'
import hashlib, json, pathlib, sys, time
out, version, charter = sys.argv[1:]
raw=pathlib.Path(charter).read_bytes()
print(json.dumps({
"schema":"AIOS_OPENBAO_SELF_INIT_COMPLETE_V1",
"completed_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
"openbao_version":version,
"audit_verified":True,
"transit_verified":True,
"kv_v2_verified":True,
"approle_verified":True,
"policy_verified":True,
"charter_sha256":hashlib.sha256(raw).hexdigest(),
"charter_signature_verified":True,
},sort_keys=True),file=open(out,"w"))
PY
chown root:aios-autonomy "$OPENBAO_INIT_MARKER.tmp"
chmod 0640 "$OPENBAO_INIT_MARKER.tmp"
mv -f "$OPENBAO_INIT_MARKER.tmp" "$OPENBAO_INIT_MARKER"
}
remove_self_init_secrets_and_restart() {
PHASE="openbao-remove-self-init-secrets"
render_openbao_config
grep -q '^initialize "' "$OPENBAO_CONFIG" &&
die "self-initialization stanza remained after successful initialization"
local secret="$TEMP_DIR/secret-removal-check"
load_approle_secret "$secret"
if grep -Fq "$(cat "$secret")" "$OPENBAO_CONFIG"; then
shred -u "$secret" 2>/dev/null || rm -f "$secret"
die "AppRole SecretID remained in persistent OpenBao configuration"
fi
shred -u "$secret" 2>/dev/null || rm -f "$secret"
OPENBAO_INIT_SECRET_REMOVED=true
systemctl restart "$BAO_SERVICE"
wait_openbao_ready "$BAO_READY_TIMEOUT" ||
die "OpenBao did not recover after removal of self-initialization secrets"
local token="$TEMP_DIR/post-restart-approle.token"
openbao_login_to "$token" || die "AppRole login failed after OpenBao restart"
local sig="$TEMP_DIR/post-restart-signature.json"
openbao_token_curl "$token" GET "kv/data/aios/charter-signature" "$sig" ||
die "charter signature record is unavailable after OpenBao restart"
python3 - "$sig" <<'PY'
import json, sys
x=((json.load(open(sys.argv[1])).get("data") or {}).get("data") or {})
raise SystemExit(0 if x.get("signature") and x.get("charter_sha256") else 1)
PY
openbao_token_curl "$token" POST "auth/token/revoke-self" "$TEMP_DIR/revoke-post-restart.json" ||
die "unable to revoke post-restart validation token"
shred -u "$token" 2>/dev/null || rm -f "$token"
SEAL_REBOOT_READ_VERIFIED=true
RELEASE_RUNTIME_READY_VERIFIED=true
SERVICES_HEALTHY=true
mark_release_runtime_ready
}
create_temp_gitea_admin() {
PHASE="gitea-temporary-admin"
TEMP_ADMIN_USER="aios-bootstrap-$(date +%s)-$(python3 - <<'PY'
import secrets
print(secrets.token_hex(4))
PY
)"
gitea_cli admin user create \
--username "$TEMP_ADMIN_USER" \
--random-password \
--random-password-length 64 \
--email "$TEMP_ADMIN_USER@localhost" \
--admin \
--must-change-password=false >/dev/null
gitea_user_exists "$TEMP_ADMIN_USER" || die "temporary Gitea administrator was not created"
GITEA_MUTATED=true
TEMP_ADMIN_TOKEN="$(
gitea_cli admin user generate-access-token \
--username "$TEMP_ADMIN_USER" \
--token-name "aios-bootstrap-v1-$(date +%s)" \
--scopes "write:repository,read:user,read:organization" \
--raw
)"
[[ "$TEMP_ADMIN_TOKEN" =~ ^[A-Za-z0-9._~-]+$ ]] || die "temporary Gitea token output was invalid"
}
machine_api_config() {
local output="$1"
curl_config_token "$output" "$GITEA_TOKEN_FILE"
}
temp_admin_api_config() {
local output="$1"
local token_file="$TEMP_DIR/temp-admin.token"
printf '%s' "$TEMP_ADMIN_TOKEN" >"$token_file"
chmod 0600 "$token_file"
curl_config_token "$output" "$token_file"
}
verify_machine_token() {
[[ -s "$GITEA_TOKEN_FILE" && ! -L "$GITEA_TOKEN_FILE" ]] || return 1
[[ "$(stat -c '%U:%G:%a' "$GITEA_TOKEN_FILE")" == "root:aios-autonomy:440" ]] || return 1
local cfg="$TEMP_DIR/machine-verify.curl"
local out="$TEMP_DIR/machine-user.json"
machine_api_config "$cfg"
api_with_config "$cfg" GET "/user" "$out" || return 1
python3 - "$out" "$GITEA_MACHINE_USER" <<'PY'
import json, sys
x = json.load(open(sys.argv[1]))
raise SystemExit(0 if x.get("login") == sys.argv[2] else 1)
PY
}
create_or_rotate_machine_credential() {
PHASE="gitea-machine-credential"
if verify_machine_token; then
MACHINE_CREDENTIAL_STORAGE="protected-file"
MACHINE_CREDENTIAL_PERSISTED=true
MACHINE_AUTHENTICATED=true
log "reusing verified Gitea machine credential"
return
fi
# A dedicated machine identity with an unavailable or invalid credential is
# recreated rather than accumulating unknown long-lived tokens.
if gitea_user_exists "$GITEA_MACHINE_USER"; then
gitea_cli admin user delete --username "$GITEA_MACHINE_USER" >/dev/null
gitea_user_exists "$GITEA_MACHINE_USER" &&
die "unable to remove stale dedicated Gitea machine identity"
GITEA_MUTATED=true
fi
gitea_cli admin user create \
--username "$GITEA_MACHINE_USER" \
--random-password \
--random-password-length 64 \
--email "$GITEA_MACHINE_EMAIL" \
--must-change-password=false >/dev/null
gitea_user_exists "$GITEA_MACHINE_USER" || die "Gitea machine identity was not created"
MACHINE_IDENTITY_CREATED=true
GITEA_MUTATED=true
local machine_token
machine_token="$(
gitea_cli admin user generate-access-token \
--username "$GITEA_MACHINE_USER" \
--token-name "aios-autonomy-v1-$(date -u +%Y%m%dT%H%M%SZ)" \
--scopes "write:repository,read:user" \
--raw
)"
[[ "$machine_token" =~ ^[A-Za-z0-9._~-]+$ ]] || die "machine token output was invalid"
install -m 0440 -o root -g aios-autonomy /dev/null "$GITEA_TOKEN_FILE.tmp"
printf '%s' "$machine_token" >"$GITEA_TOKEN_FILE.tmp"
mv -f "$GITEA_TOKEN_FILE.tmp" "$GITEA_TOKEN_FILE"
unset machine_token
[[ "$(stat -c '%U:%G:%a' "$GITEA_TOKEN_FILE")" == "root:aios-autonomy:440" ]] ||
die "Gitea machine credential ownership or mode is invalid"
MACHINE_CREDENTIAL_STORAGE="protected-file"
MACHINE_CREDENTIAL_PERSISTED=true
verify_machine_token || die "persisted Gitea machine credential failed authentication"
MACHINE_AUTHENTICATED=true
}
grant_and_verify_collaborator() {
PHASE="gitea-collaborator"
local admin_cfg="$TEMP_DIR/temp-admin-api.curl"
local machine_cfg="$TEMP_DIR/machine-api.curl"
local payload="$TEMP_DIR/collaborator.json"
local out="$TEMP_DIR/collaborator-response.json"
local permission="$TEMP_DIR/collaborator-permission.json"
local repo="$TEMP_DIR/repository.json"
temp_admin_api_config "$admin_cfg"
machine_api_config "$machine_cfg"
printf '%s\n' '{"permission":"write"}' >"$payload"
api_with_config "$admin_cfg" PUT "/repos/$GITEA_OWNER/$GITEA_REPO/collaborators/$GITEA_MACHINE_USER" "$out" "$payload"
GITEA_MUTATED=true
api_with_config "$admin_cfg" GET "/repos/$GITEA_OWNER/$GITEA_REPO/collaborators/$GITEA_MACHINE_USER/permission" "$permission"
python3 - "$permission" <<'PY'
import json, sys
x=json.load(open(sys.argv[1]))
value=str(x.get("permission") or x.get("role_name") or "").lower()
raise SystemExit(0 if value in {"write","admin","owner"} else 1)
PY
MACHINE_WRITE_PERMISSION_VERIFIED=true
api_with_config "$machine_cfg" GET "/repos/$GITEA_OWNER/$GITEA_REPO" "$repo"
DEFAULT_BRANCH="$(python3 - "$repo" <<'PY'
import json, sys
x=json.load(open(sys.argv[1]))
branch=x.get("default_branch")
if not branch:
raise SystemExit("missing default branch")
print(branch)
PY
)"
}
verify_default_branch_protection() {
PHASE="gitea-branch-protection"
local machine_cfg="$TEMP_DIR/machine-branch.curl"
local admin_cfg="$TEMP_DIR/admin-branch-protection.curl"
local branch_out="$TEMP_DIR/default-branch.json"
local protection_out="$TEMP_DIR/default-branch-protection.json"
local payload="$TEMP_DIR/default-branch-protection-request.json"
local encoded_branch
encoded_branch="$(python3 - "$DEFAULT_BRANCH" <<'PY'
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=""))
PY
)"
machine_api_config "$machine_cfg"
temp_admin_api_config "$admin_cfg"
branch_is_safely_protected() {
api_with_config "$machine_cfg" GET \
"/repos/$GITEA_OWNER/$GITEA_REPO/branches/$encoded_branch" \
"$branch_out" &&
python3 - "$branch_out" <<'PY'
import json, sys
x=json.load(open(sys.argv[1]))
# Evaluated as the machine identity. protected=true plus
# user_can_push=false proves repository Write cannot bypass the default branch.
raise SystemExit(
0 if x.get("protected") is True and x.get("user_can_push") is False else 1
)
PY
}
if branch_is_safely_protected; then
DEFAULT_BRANCH_PROTECTION_VERIFIED=true
return
fi
local protection_code
protection_code="$(
curl --config "$admin_cfg" --request GET \
--output "$protection_out" --write-out '%{http_code}' \
"${GITEA_URL%/}/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections/$encoded_branch" \
2>/dev/null
)"
if [[ "$protection_code" == "404" ]]; then
python3 - "$payload" "$DEFAULT_BRANCH" <<'PY'
import json, sys
json.dump({
"branch_name": sys.argv[2],
"enable_push": False,
"enable_force_push": False,
}, open(sys.argv[1], "w"))
PY
api_with_config "$admin_cfg" POST \
"/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections" \
"$protection_out" "$payload"
GITEA_MUTATED=true
elif [[ "$protection_code" == "200" ]]; then
# Existing owner policy is only tightened: direct pushes and force pushes
# are disabled. All omitted fields remain under the existing policy.
printf '%s\n' '{"enable_push":false,"enable_force_push":false}' >"$payload"
api_with_config "$admin_cfg" PATCH \
"/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections/$encoded_branch" \
"$protection_out" "$payload"
GITEA_MUTATED=true
else
die "unable to inspect default branch protection (HTTP $protection_code)"
fi
local deadline=$((SECONDS + 30))
while ((SECONDS < deadline)); do
if branch_is_safely_protected; then
DEFAULT_BRANCH_PROTECTION_VERIFIED=true
break
fi
sleep 2
done
[[ "$DEFAULT_BRANCH_PROTECTION_VERIFIED" == true ]] ||
die "default branch protection was not enforced for the machine identity"
}
verify_tag_protection() {
PHASE="gitea-tag-protection"
local admin_cfg="$TEMP_DIR/admin-tag-protection.curl"
local protections="$TEMP_DIR/tag-protections.json"
local refreshed="$TEMP_DIR/tag-protections-refreshed.json"
local payload="$TEMP_DIR/tag-protection-payload.json"
local response="$TEMP_DIR/tag-protection-response.json"
local team_list="$TEMP_DIR/gitea-owner-teams.json"
local team_member_out="$TEMP_DIR/gitea-team-member.json"
temp_admin_api_config "$admin_cfg"
api_with_config "$admin_cfg" GET \
"/repos/$GITEA_OWNER/$GITEA_REPO/tag_protections" \
"$protections"
python3 - "$protections" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
if not isinstance(items, list):
raise SystemExit("invalid tag-protection list response")
PY
# Remove only the dedicated machine identity from direct tag-protection
# allowlists. Existing owner/team policy is otherwise preserved.
while IFS=$'\t' read -r protection_id payload_b64; do
[[ -n "$protection_id" && "$protection_id" =~ ^[0-9]+$ ]] ||
die "invalid Gitea tag-protection identifier"
printf '%s' "$payload_b64" | base64 -d >"$payload"
api_with_config "$admin_cfg" PATCH \
"/repos/$GITEA_OWNER/$GITEA_REPO/tag_protections/$protection_id" \
"$response" "$payload"
GITEA_MUTATED=true
done < <(
python3 - "$protections" "$GITEA_MACHINE_USER" <<'PY'
import base64, json, sys
items = json.load(open(sys.argv[1]))
machine = sys.argv[2]
if not isinstance(items, list):
raise SystemExit("invalid tag-protection response")
for item in items:
users = item.get("whitelist_usernames") or []
teams = item.get("whitelist_teams") or []
if machine not in users:
continue
pid = item.get("id")
pattern = item.get("name_pattern")
if not isinstance(pid, int) or not isinstance(pattern, str) or not pattern:
raise SystemExit("invalid tag-protection object")
payload = {
"name_pattern": pattern,
"whitelist_usernames": [u for u in users if u != machine],
"whitelist_teams": teams,
}
encoded = base64.b64encode(
json.dumps(payload, separators=(",", ":")).encode()
).decode()
print(f"{pid}\t{encoded}")
PY
)
# An organization team allowlisted by a matching rule could still grant the
# machine identity tag rights. A freshly created machine identity should not
# be in any team; fail closed if that assumption is false.
local owner_probe="$TEMP_DIR/gitea-owner-org-probe.json"
local teams_code
teams_code="$(
curl --config "$admin_cfg" --request GET \
--output "$owner_probe" --write-out '%{http_code}' \
"${GITEA_URL%/}/api/v1/orgs/$GITEA_OWNER" \
2>/dev/null
)"
if [[ "$teams_code" == "200" ]]; then
fetch_paginated_json_array "$admin_cfg" \
"/orgs/$GITEA_OWNER/teams" \
"$team_list"
while IFS= read -r team_id; do
[[ "$team_id" =~ ^[0-9]+$ ]] || die "invalid Gitea team identifier"
local member_code
member_code="$(
curl --config "$admin_cfg" --request GET \
--output "$team_member_out" --write-out '%{http_code}' \
"${GITEA_URL%/}/api/v1/teams/$team_id/members/$GITEA_MACHINE_USER" \
2>/dev/null
)"
case "$member_code" in
404) ;;
200|204)
die "machine identity belongs to an organization team; tag denial cannot be proven safely"
;;
*)
die "unable to verify Gitea team membership (HTTP $member_code)"
;;
esac
done < <(
python3 - "$team_list" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
if not isinstance(items, list):
raise SystemExit("invalid organization-team response")
for item in items:
tid = item.get("id")
if not isinstance(tid, int):
raise SystemExit("invalid organization-team object")
print(tid)
PY
)
elif [[ "$teams_code" != "404" ]]; then
die "unable to determine whether Gitea owner is an organization (HTTP $teams_code)"
fi
# Add a fail-closed wildcard protection when none exists. Empty allowlists
# mean no account may create or mutate a matching tag until a separate,
# owner-approved release workflow changes policy.
if ! python3 - "$protections" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
raise SystemExit(
0 if any(
x.get("name_pattern") == "*"
and not (x.get("whitelist_usernames") or [])
and not (x.get("whitelist_teams") or [])
for x in items
) else 1
)
PY
then
printf '%s\n' \
'{"name_pattern":"*","whitelist_usernames":[],"whitelist_teams":[]}' \
>"$payload"
api_with_config "$admin_cfg" POST \
"/repos/$GITEA_OWNER/$GITEA_REPO/tag_protections" \
"$response" "$payload"
GITEA_MUTATED=true
fi
api_with_config "$admin_cfg" GET \
"/repos/$GITEA_OWNER/$GITEA_REPO/tag_protections" \
"$refreshed"
python3 - "$refreshed" "$GITEA_MACHINE_USER" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
machine = sys.argv[2]
if not isinstance(items, list):
raise SystemExit("invalid tag-protection response")
if any(machine in (x.get("whitelist_usernames") or []) for x in items):
raise SystemExit("machine identity remains directly allowlisted for tags")
if not any(
x.get("name_pattern") == "*"
and not (x.get("whitelist_usernames") or [])
and not (x.get("whitelist_teams") or [])
for x in items
):
raise SystemExit("fail-closed wildcard tag protection is absent")
PY
TAG_PROTECTION_VERIFIED=true
}
verify_non_main_git_push() {
PHASE="gitea-git-push-verification"
local work="$TEMP_DIR/git-check"
local askpass="$TEMP_DIR/git-askpass.sh"
local branch="auto/bootstrap-verification-$(date +%s)-$(python3 - <<'PY'
import secrets
print(secrets.token_hex(4))
PY
)"
install -d -m 0700 -o aios-autonomy -g aios-autonomy "$work"
cat >"$askpass" <<'SH'
#!/bin/sh
case "$1" in
*sername*) printf '%s\n' "${AIOS_GIT_USERNAME:?}" ;;
*assword*) cat "${AIOS_GIT_TOKEN_FILE:?}" ;;
*) exit 1 ;;
esac
SH
chown root:aios-autonomy "$askpass"
chmod 0550 "$askpass"
local common_env=(
HOME="$work/home"
GIT_TERMINAL_PROMPT=0
GIT_ASKPASS="$askpass"
AIOS_GIT_USERNAME="$GITEA_MACHINE_USER"
AIOS_GIT_TOKEN_FILE="$GITEA_TOKEN_FILE"
)
install -d -m 0700 -o aios-autonomy -g aios-autonomy "$work/home"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" init -q
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" config user.name "AIOS Autonomy Agent"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" config user.email "$GITEA_MACHINE_EMAIL"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" remote add origin "$GITEA_GIT_URL"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" fetch -q --depth=1 origin "$DEFAULT_BRANCH"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" checkout -q -b "$branch" FETCH_HEAD
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" commit -q --allow-empty -m "chore: verify autonomous non-main push"
local oid
oid="$(runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" rev-parse HEAD)"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" push -q origin "HEAD:refs/heads/$branch"
local remote_oid
remote_oid="$(
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" ls-remote origin "refs/heads/$branch" |
awk 'NR==1{print $1}'
)"
[[ "$remote_oid" == "$oid" ]] || die "remote non-main branch OID verification failed"
runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" push -q origin ":refs/heads/$branch"
if runuser -u aios-autonomy -- env "${common_env[@]}" git -C "$work" ls-remote --exit-code origin "refs/heads/$branch" >/dev/null 2>&1; then
die "temporary verification branch was not removed"
fi
MACHINE_NON_MAIN_PUSH_VERIFIED=true
GITEA_MUTATED=true
}
revoke_and_remove_temp_admin() {
PHASE="gitea-temporary-admin-cleanup"
cleanup_gitea_credentials
[[ "$TEMP_ADMIN_TOKEN_REVOKED" == true ]] || die "temporary Gitea token revocation could not be verified"
[[ "$TEMP_ADMIN_REMOVED" == true ]] || die "temporary Gitea administrator removal could not be verified"
}
install_services() {
PHASE="service-installation"
# Shared OpenBao client helper. Tokens are held only in mode-0600 runtime
# files and curl reads headers from a temporary config file, not argv.
cat >"$CLIENT_LIB.tmp" <<'LIB'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
aios_require_client_env() {
: "${AIOS_OPENBAO_ADDR:?}"
: "${AIOS_OPENBAO_ROLE_ID_FILE:?}"
: "${AIOS_OPENBAO_SECRET_ID_FILE:?}"
}
aios_curl() {
local args=(--silent --show-error --connect-timeout 10 --max-time 180)
if [[ -n "${AIOS_OPENBAO_CACERT:-}" ]]; then
args+=(--cacert "$AIOS_OPENBAO_CACERT")
fi
curl "${args[@]}" "$@"
}
aios_login_token() {
local output="$1"
local work payload response code
work="$(mktemp -d /run/aios-openbao-client.XXXXXX)"
chmod 0700 "$work"
payload="$work/login.json"
response="$work/response.json"
python3 - "$AIOS_OPENBAO_ROLE_ID_FILE" "$AIOS_OPENBAO_SECRET_ID_FILE" "$payload" <<'PY'
import json, pathlib, sys
role=pathlib.Path(sys.argv[1]).read_text().strip()
secret=pathlib.Path(sys.argv[2]).read_text().strip()
if not role or not secret:
raise SystemExit("missing AppRole credential")
json.dump({"role_id":role,"secret_id":secret},open(sys.argv[3],"w"))
PY
code="$(
aios_curl \
--request POST \
--header 'Content-Type: application/json' \
--data-binary "@$payload" \
--output "$response" \
--write-out '%{http_code}' \
"${AIOS_OPENBAO_ADDR%/}/v1/auth/approle/login"
)" || code=000
if [[ "$code" != 200 ]]; then
rm -rf "$work"
return 1
fi
python3 - "$response" "$output.tmp" <<'PY'
import json, pathlib, sys
x=json.load(open(sys.argv[1]))
auth=x.get("auth") or {}
token=auth.get("client_token")
policies=set(auth.get("policies") or auth.get("token_policies") or [])
if not token or "aios-autonomy" not in policies:
raise SystemExit("invalid AppRole response")
pathlib.Path(sys.argv[2]).write_text(token)
PY
chmod 0600 "$output.tmp"
mv -f "$output.tmp" "$output"
rm -rf "$work"
}
aios_token_curl() {
local token_file="$1" method="$2" path="$3" output="$4"
local payload="${5:-}"
local cfg code
cfg="$(mktemp /run/aios-openbao-token.XXXXXX)"
chmod 0600 "$cfg"
printf 'header = "X-Vault-Token: %s"\n' "$(cat "$token_file")" >"$cfg"
local args=(
--config "$cfg"
--request "$method"
--output "$output"
--write-out '%{http_code}'
)
if [[ -n "$payload" ]]; then
args+=(--header 'Content-Type: application/json' --data-binary "@$payload")
fi
code="$(aios_curl "${args[@]}" "${AIOS_OPENBAO_ADDR%/}/v1/$path")" || code=000
rm -f "$cfg"
[[ "$code" =~ ^2[0-9][0-9]$ ]]
}
aios_health_ready() {
local output="$1"
local code
code="$(
aios_curl \
--output "$output" \
--write-out '%{http_code}' \
"${AIOS_OPENBAO_ADDR%/}/v1/sys/health?standbyok=true&perfstandbyok=true"
)" || code=000
[[ "$code" =~ ^(200|429|472|473)$ ]] || return 1
python3 - "$output" <<'PY'
import json, sys
x=json.load(open(sys.argv[1]))
raise SystemExit(0 if x.get("initialized") is True and x.get("sealed") is False else 1)
PY
}
aios_revoke_self() {
local token_file="$1"
local out
out="$(mktemp /run/aios-openbao-revoke.XXXXXX)"
aios_token_curl "$token_file" POST "auth/token/revoke-self" "$out"
rm -f "$out" "$token_file"
}
LIB
install -m 0555 -o root -g root "$CLIENT_LIB.tmp" "$CLIENT_LIB"
rm -f "$CLIENT_LIB.tmp"
cat >"$CONTROLLER_BIN.tmp" <<'CONTROLLER'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
source /usr/local/libexec/aios-openbao-client-lib
aios_require_client_env
: "${AIOS_AUTONOMY_STATUS:?}"
: "${AIOS_CHARTER_FILE:?}"
: "${AIOS_CONTROLLER_INTERVAL_FILE:?}"
write_status() {
local state="$1" reason="$2" charter_ok="$3"
python3 - "$AIOS_AUTONOMY_STATUS.tmp" "$state" "$reason" "$charter_ok" <<'PY'
import json, pathlib, sys, time
out,state,reason,charter_ok=sys.argv[1:]
obj={
"schema":"AIOS_AUTONOMY_STATUS_V1",
"state":state,
"reason":reason,
"openbao_initialized": state=="RUNNING",
"openbao_unsealed": state=="RUNNING",
"charter_signature_verified": charter_ok=="true",
"human_action_required":False,
"production_change_in_progress":False,
"last_health_check":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
"updated_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
pathlib.Path(out).write_text(json.dumps(obj,sort_keys=True)+"\n")
PY
chmod 0640 "$AIOS_AUTONOMY_STATUS.tmp"
mv -f "$AIOS_AUTONOMY_STATUS.tmp" "$AIOS_AUTONOMY_STATUS"
}
while :; do
work="$(mktemp -d /run/aios-autonomy-controller.XXXXXX)"
chmod 0700 "$work"
state="DEGRADED"
reason="health check failed"
charter_ok=false
if aios_health_ready "$work/health.json" &&
aios_login_token "$work/token"; then
python3 - "$AIOS_CHARTER_FILE" "$work/verify.json" <<'PY'
import base64,json,pathlib,sys
raw=pathlib.Path(sys.argv[1]).read_bytes()
json.dump({"input":base64.b64encode(raw).decode()},open(sys.argv[2],"w"))
PY
if aios_token_curl "$work/token" GET "kv/data/aios/charter-signature" "$work/signature.json"; then
python3 - "$work/signature.json" "$work/verify.json" <<'PY'
import json,sys
x=((json.load(open(sys.argv[1])).get("data") or {}).get("data") or {})
p=json.load(open(sys.argv[2]))
sig=x.get("signature")
if not sig:
raise SystemExit(1)
p["signature"]=sig
json.dump(p,open(sys.argv[2],"w"))
PY
if aios_token_curl "$work/token" POST "transit/verify/aios-charter" "$work/verified.json" "$work/verify.json" &&
python3 - "$work/verified.json" <<'PY'
import json,sys
raise SystemExit(0 if (json.load(open(sys.argv[1])).get("data") or {}).get("valid") is True else 1)
PY
then
charter_ok=true
fi
fi
if [[ "$charter_ok" == true ]]; then
state="RUNNING"
reason="OpenBao ready; AppRole and charter signature verified"
else
reason="OpenBao ready but charter verification failed"
fi
aios_revoke_self "$work/token" || true
fi
write_status "$state" "$reason" "$charter_ok"
rm -rf "$work"
interval=60
if [[ -s "$AIOS_CONTROLLER_INTERVAL_FILE" ]]; then
candidate="$(tr -dc '0-9' <"$AIOS_CONTROLLER_INTERVAL_FILE")"
if [[ "$candidate" =~ ^[0-9]+$ ]] && ((candidate >= 30 && candidate <= 120)); then
interval="$candidate"
fi
fi
sleep "$interval"
done
CONTROLLER
install -m 0555 -o root -g root "$CONTROLLER_BIN.tmp" "$CONTROLLER_BIN"
rm -f "$CONTROLLER_BIN.tmp"
cat >"$BACKUP_BIN.tmp" <<'BACKUP'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
source /usr/local/libexec/aios-openbao-client-lib
aios_require_client_env
: "${AIOS_BACKUP_DIR:?}"
: "${AIOS_BACKUP_STATUS:?}"
: "${AIOS_BAO_BIN:?}"
work="$(mktemp -d /run/aios-openbao-backup.XXXXXX)"
trap 'rm -rf "$work"' EXIT
token="$work/token"
aios_login_token "$token"
ts="$(date -u +%Y%m%dT%H%M%SZ)"
tmp="$AIOS_BACKUP_DIR/openbao-$ts.snap.tmp"
final="$AIOS_BACKUP_DIR/openbao-$ts.snap"
aios_token_curl "$token" GET "sys/storage/raft/snapshot" "$tmp"
[[ -s "$tmp" ]]
"$AIOS_BAO_BIN" operator raft snapshot inspect "$tmp" >"$work/inspect.txt"
sha="$(sha256sum "$tmp" | awk '{print $1}')"
chmod 0440 "$tmp"
chgrp aios-openbao "$tmp"
mv -f "$tmp" "$final"
aios_revoke_self "$token"
python3 - "$AIOS_BACKUP_STATUS.tmp" "$final" "$sha" <<'PY'
import json,pathlib,sys,time
out,path,sha=sys.argv[1:]
obj={
"schema":"AIOS_OPENBAO_BACKUP_STATUS_V1",
"passed":True,
"snapshot":path,
"sha256":sha,
"completed_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
pathlib.Path(out).write_text(json.dumps(obj,sort_keys=True)+"\n")
PY
chmod 0640 "$AIOS_BACKUP_STATUS.tmp"
mv -f "$AIOS_BACKUP_STATUS.tmp" "$AIOS_BACKUP_STATUS"
find "$AIOS_BACKUP_DIR" -maxdepth 1 -type f -name 'openbao-*.snap' -mtime +14 -delete
BACKUP
install -m 0555 -o root -g root "$BACKUP_BIN.tmp" "$BACKUP_BIN"
rm -f "$BACKUP_BIN.tmp"
cat >"$RESTORE_TEST_BIN.tmp" <<'RESTORE'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
source /usr/local/libexec/aios-openbao-client-lib
: "${AIOS_BACKUP_DIR:?}"
: "${AIOS_RESTORE_STATUS:?}"
: "${AIOS_BAO_BIN:?}"
: "${AIOS_SEAL_KEY_FILE:?}"
: "${AIOS_SEAL_KEY_ID_FILE:?}"
: "${AIOS_CHARTER_FILE:?}"
: "${AIOS_OPENBAO_ROLE_ID_FILE:?}"
: "${AIOS_OPENBAO_SECRET_ID_FILE:?}"
snapshot="$(find "$AIOS_BACKUP_DIR" -maxdepth 1 -type f -name 'openbao-*.snap' -printf '%T@ %p\n' | sort -nr | awk 'NR==1{$1="";sub(/^ /,"");print}')"
[[ -n "$snapshot" && -s "$snapshot" ]]
"$AIOS_BAO_BIN" operator raft snapshot inspect "$snapshot" >/dev/null
work="$(mktemp -d "${AIOS_RESTORE_ROOT:?}/restore.XXXXXX")"
chmod 0700 "$work"
pid=""
cleanup() {
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
rm -rf "$work"
}
trap cleanup EXIT
base=$((19000 + ($$ % 500) * 2))
api_port="$base"
cluster_port=$((base + 1))
addr="http://127.0.0.1:$api_port"
key_id="$(cat "$AIOS_SEAL_KEY_ID_FILE")"
cat >"$work/openbao.hcl" <<EOF
api_addr = "$addr"
cluster_addr = "http://127.0.0.1:$cluster_port"
ui = false
storage "raft" {
path = "$work/raft"
node_id = "restore-test"
}
listener "tcp" {
address = "127.0.0.1:$api_port"
cluster_address = "127.0.0.1:$cluster_port"
tls_disable = true
}
seal "static" {
current_key_id = "$key_id"
current_key = "file://$AIOS_SEAL_KEY_FILE"
}
EOF
mkdir -p "$work/raft"
start_server() {
"$AIOS_BAO_BIN" server -config="$work/openbao.hcl" >"$work/server.log" 2>&1 &
pid=$!
}
wait_http() {
local deadline=$((SECONDS+60))
while ((SECONDS<deadline)); do
if curl --silent --output "$work/health.json" "$addr/v1/sys/health" 2>/dev/null; then
return 0
fi
sleep 1
done
return 1
}
stop_server() {
kill -TERM "$pid"
wait "$pid" || true
pid=""
}
start_server
wait_http
BAO_ADDR="$addr" "$AIOS_BAO_BIN" operator init \
-format=json -recovery-shares=1 -recovery-threshold=1 >"$work/init.json"
root_token="$(python3 - "$work/init.json" <<'PY'
import json,sys
x=json.load(open(sys.argv[1]))
t=x.get("root_token")
if not t: raise SystemExit(1)
print(t)
PY
)"
BAO_ADDR="$addr" BAO_TOKEN="$root_token" "$AIOS_BAO_BIN" operator raft snapshot restore -force "$snapshot"
unset root_token
stop_server
start_server
deadline=$((SECONDS+90))
ready=false
while ((SECONDS<deadline)); do
if curl --silent --output "$work/restored-health.json" "$addr/v1/sys/health" 2>/dev/null &&
python3 - "$work/restored-health.json" <<'PY'
import json,sys
x=json.load(open(sys.argv[1]))
raise SystemExit(0 if x.get("initialized") is True and x.get("sealed") is False else 1)
PY
then
ready=true
break
fi
sleep 2
done
[[ "$ready" == true ]]
export AIOS_OPENBAO_ADDR="$addr"
export AIOS_OPENBAO_CACERT=""
aios_login_token "$work/restored-token"
aios_token_curl "$work/restored-token" GET "kv/data/aios/charter-signature" "$work/restored-signature.json"
python3 - "$AIOS_CHARTER_FILE" "$work/restored-signature.json" "$work/verify.json" <<'PY'
import base64,json,pathlib,sys
raw=pathlib.Path(sys.argv[1]).read_bytes()
x=((json.load(open(sys.argv[2])).get("data") or {}).get("data") or {})
sig=x.get("signature")
if not sig: raise SystemExit(1)
json.dump({"input":base64.b64encode(raw).decode(),"signature":sig},open(sys.argv[3],"w"))
PY
aios_token_curl "$work/restored-token" POST "transit/verify/aios-charter" "$work/verified.json" "$work/verify.json"
python3 - "$work/verified.json" <<'PY'
import json,sys
raise SystemExit(0 if (json.load(open(sys.argv[1])).get("data") or {}).get("valid") is True else 1)
PY
aios_revoke_self "$work/restored-token"
python3 - "$AIOS_RESTORE_STATUS.tmp" "$snapshot" <<'PY'
import hashlib,json,pathlib,sys,time
out,snapshot=sys.argv[1:]
raw=pathlib.Path(snapshot).read_bytes()
obj={
"schema":"AIOS_OPENBAO_RESTORE_TEST_V1",
"passed":True,
"isolated_restore":True,
"source_snapshot":snapshot,
"source_sha256":hashlib.sha256(raw).hexdigest(),
"restored_initialized":True,
"restored_unsealed":True,
"approle_login_verified":True,
"charter_signature_verified":True,
"completed_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
pathlib.Path(out).write_text(json.dumps(obj,sort_keys=True)+"\n")
PY
chgrp aios-autonomy "$AIOS_RESTORE_STATUS.tmp"
chmod 0640 "$AIOS_RESTORE_STATUS.tmp"
mv -f "$AIOS_RESTORE_STATUS.tmp" "$AIOS_RESTORE_STATUS"
RESTORE
install -m 0555 -o root -g root "$RESTORE_TEST_BIN.tmp" "$RESTORE_TEST_BIN"
rm -f "$RESTORE_TEST_BIN.tmp"
cat >"$FAULT_TEST_BIN.tmp" <<'FAULT'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
source /usr/local/libexec/aios-openbao-client-lib
aios_require_client_env
: "${AIOS_FAULT_STATUS:?}"
: "${AIOS_OPENBAO_SERVICE:?}"
: "${AIOS_GITEA_URL:?}"
: "${AIOS_BAO_BIN:?}"
work="$(mktemp -d /run/aios-openbao-fault.XXXXXX)"
trap 'rm -rf "$work"' EXIT
process_recovery=false
revoked_token_rejected=false
gitea_outage_detected=false
missing_seal_rejected=false
systemctl kill --kill-who=main --signal=SIGKILL "$AIOS_OPENBAO_SERVICE"
deadline=$((SECONDS+120))
while ((SECONDS<deadline)); do
if systemctl is-active --quiet "$AIOS_OPENBAO_SERVICE" &&
aios_health_ready "$work/health.json"; then
process_recovery=true
break
fi
sleep 2
done
[[ "$process_recovery" == true ]]
aios_login_token "$work/token"
aios_revoke_self "$work/token"
python3 - "$AIOS_OPENBAO_ROLE_ID_FILE" "$AIOS_OPENBAO_SECRET_ID_FILE" "$work/login.json" <<'PY'
import json,pathlib,sys
json.dump({
"role_id":pathlib.Path(sys.argv[1]).read_text().strip(),
"secret_id":pathlib.Path(sys.argv[2]).read_text().strip(),
},open(sys.argv[3],"w"))
PY
aios_login_token "$work/revoked-test-token"
cfg="$work/revoked.curl"
printf 'header = "X-Vault-Token: %s"\n' "$(cat "$work/revoked-test-token")" >"$cfg"
aios_revoke_self "$work/revoked-test-token"
code="$(aios_curl --config "$cfg" --output "$work/revoked.json" --write-out '%{http_code}' "${AIOS_OPENBAO_ADDR%/}/v1/auth/token/lookup-self" || true)"
rm -f "$cfg"
[[ "$code" == 403 ]] && revoked_token_rejected=true
[[ "$revoked_token_rejected" == true ]]
if ! curl --silent --show-error --connect-timeout 2 --max-time 3 \
"http://127.0.0.1:9/api/v1/version" >/dev/null 2>&1; then
gitea_outage_detected=true
fi
[[ "$gitea_outage_detected" == true ]]
cat >"$work/missing-seal.hcl" <<EOF
storage "raft" { path = "$work/missing-raft" node_id = "missing-seal" }
listener "tcp" { address = "127.0.0.1:19820" tls_disable = true }
seal "static" {
current_key_id = "missing"
current_key = "file://$work/does-not-exist"
}
EOF
mkdir -p "$work/missing-raft"
set +e
timeout 8 "$AIOS_BAO_BIN" server -config="$work/missing-seal.hcl" >"$work/missing-seal.log" 2>&1
missing_seal_rc=$?
set -e
if [[ "$missing_seal_rc" -ne 0 && "$missing_seal_rc" -ne 124 ]] &&
grep -Eqi 'seal|key|file|no such' "$work/missing-seal.log"; then
missing_seal_rejected=true
fi
[[ "$missing_seal_rejected" == true ]]
python3 - "$AIOS_FAULT_STATUS.tmp" "$process_recovery" "$revoked_token_rejected" "$gitea_outage_detected" "$missing_seal_rejected" <<'PY'
import json,pathlib,sys,time
out,*vals=sys.argv[1:]
names=["openbao_process_auto_recovered","revoked_token_rejected","gitea_outage_detected","missing_seal_failed_closed"]
checks={k:(v=="true") for k,v in zip(names,vals)}
obj={
"schema":"AIOS_OPENBAO_FAULT_INJECTION_V1",
"passed":all(checks.values()),
"checks":checks,
"completed_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
pathlib.Path(out).write_text(json.dumps(obj,sort_keys=True)+"\n")
PY
chown root:aios-autonomy "$AIOS_FAULT_STATUS.tmp"
chmod 0640 "$AIOS_FAULT_STATUS.tmp"
mv -f "$AIOS_FAULT_STATUS.tmp" "$AIOS_FAULT_STATUS"
FAULT
install -m 0555 -o root -g root "$FAULT_TEST_BIN.tmp" "$FAULT_TEST_BIN"
rm -f "$FAULT_TEST_BIN.tmp"
cat >"$OPTIMIZE_BIN.tmp" <<'OPTIMIZE'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
: "${AIOS_OPENBAO_ADDR:?}"
: "${AIOS_OPENBAO_CACERT:?}"
: "${AIOS_CONTROLLER_INTERVAL_FILE:?}"
: "${AIOS_OPTIMIZATION_LEDGER:?}"
current=60
[[ -s "$AIOS_CONTROLLER_INTERVAL_FILE" ]] && current="$(tr -dc '0-9' <"$AIOS_CONTROLLER_INTERVAL_FILE")"
[[ "$current" =~ ^[0-9]+$ ]] || current=60
samples="$(mktemp /run/aios-optimize.XXXXXX)"
trap 'rm -f "$samples"' EXIT
for _ in 1 2 3 4 5; do
curl --silent --show-error --cacert "$AIOS_OPENBAO_CACERT" \
--output /dev/null --write-out '%{time_total}\n' \
--connect-timeout 3 --max-time 10 \
"${AIOS_OPENBAO_ADDR%/}/v1/sys/health?standbyok=true&perfstandbyok=true" >>"$samples"
done
read -r p95 average < <(python3 - "$samples" <<'PY'
import statistics,sys
xs=sorted(float(x) for x in open(sys.argv[1]) if x.strip())
p95=xs[min(len(xs)-1,max(0,int(len(xs)*.95)-1))]
print(f"{p95:.6f} {statistics.mean(xs):.6f}")
PY
)
new="$current"
reason="interval retained"
if python3 - "$p95" <<'PY'
import sys
raise SystemExit(0 if float(sys.argv[1]) < 0.20 else 1)
PY
then
((current < 120)) && new=$((current+15))
((new>120)) && new=120
reason="low health latency; reduced monitoring overhead"
elif python3 - "$p95" <<'PY'
import sys
raise SystemExit(0 if float(sys.argv[1]) > 1.0 else 1)
PY
then
new=30
reason="high health latency; increased observation frequency"
fi
printf '%s\n' "$new" >"$AIOS_CONTROLLER_INTERVAL_FILE.tmp"
chmod 0640 "$AIOS_CONTROLLER_INTERVAL_FILE.tmp"
mv -f "$AIOS_CONTROLLER_INTERVAL_FILE.tmp" "$AIOS_CONTROLLER_INTERVAL_FILE"
python3 - "$AIOS_OPTIMIZATION_LEDGER" "$current" "$new" "$p95" "$average" "$reason" <<'PY'
import json,sys,time
path,current,new,p95,avg,reason=sys.argv[1:]
entry={
"schema":"AIOS_AUTONOMY_OPTIMIZATION_EVENT_V1",
"candidate_id":"controller-interval-"+time.strftime("%Y%m%dT%H%M%SZ",time.gmtime()),
"risk":"low",
"metric":"openbao_health_latency_seconds",
"baseline":{"controller_interval_seconds":int(current),"p95":float(p95),"average":float(avg)},
"change":{"controller_interval_seconds":int(new)},
"result":"accepted",
"reason":reason,
"rollback_condition":"health state becomes DEGRADED",
"created_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
with open(path,"a") as f:
f.write(json.dumps(entry,sort_keys=True)+"\n")
PY
OPTIMIZE
install -m 0555 -o root -g root "$OPTIMIZE_BIN.tmp" "$OPTIMIZE_BIN"
rm -f "$OPTIMIZE_BIN.tmp"
cat >"$ACCEPTANCE_BIN.tmp" <<'ACCEPTANCE'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
: "${AIOS_ACCEPTANCE_START_FILE:?}"
: "${AIOS_ACCEPTANCE_SCRIPT_FINGERPRINT:?}"
: "${AIOS_ACCEPTANCE_STATUS:?}"
: "${AIOS_BOOTSTRAP_EVIDENCE:?}"
: "${AIOS_BACKUP_STATUS:?}"
: "${AIOS_RESTORE_STATUS:?}"
: "${AIOS_FAULT_STATUS:?}"
: "${AIOS_OPTIMIZATION_LEDGER:?}"
: "${AIOS_AUTONOMY_STATUS:?}"
: "${AIOS_OPENBAO_ADDR:?}"
: "${AIOS_OPENBAO_CACERT:?}"
now="$(date +%s)"
started="$(cat "$AIOS_ACCEPTANCE_START_FILE")"
[[ "$started" =~ ^[0-9]+$ ]]
elapsed=$((now-started))
ready=false
reason="72-hour window incomplete"
if ((elapsed >= 259200)); then
if curl --silent --show-error --cacert "$AIOS_OPENBAO_CACERT" \
--output /run/aios-acceptance-health.json \
"${AIOS_OPENBAO_ADDR%/}/v1/sys/health?standbyok=true&perfstandbyok=true" &&
python3 - \
"$AIOS_BOOTSTRAP_EVIDENCE" \
"$AIOS_BACKUP_STATUS" \
"$AIOS_RESTORE_STATUS" \
"$AIOS_FAULT_STATUS" \
"$AIOS_OPTIMIZATION_LEDGER" \
/run/aios-acceptance-health.json \
"$started" \
"$AIOS_ACCEPTANCE_SCRIPT_FINGERPRINT" <<'PY'
import datetime,json,pathlib,sys,time
e,b,r,f,ledger,health,started,fingerprint=sys.argv[1:]
start_epoch=int(started)
for p in (e,b,r,f,health):
if not pathlib.Path(p).is_file():
raise SystemExit(1)
ev=json.load(open(e))
if ev.get("execution_status")!="PARTIALLY_COMPLETED":
raise SystemExit(1)
expected_sha=pathlib.Path(fingerprint).read_text().strip()
if not expected_sha or ev.get("script_sha256") != expected_sha:
raise SystemExit(1)
def epoch(value):
if not isinstance(value, str):
raise SystemExit(1)
return int(datetime.datetime.fromisoformat(value.replace("Z","+00:00")).timestamp())
for status_path in (b,r,f):
status=json.load(open(status_path))
if status.get("passed") is not True:
raise SystemExit(1)
completed=epoch(status.get("completed_at"))
if completed < start_epoch or completed < int(time.time())-129600:
raise SystemExit(1)
ledger_events=[]
for raw in pathlib.Path(ledger).read_text().splitlines():
if raw.strip():
ledger_events.append(json.loads(raw))
if not any(epoch(x.get("created_at")) >= start_epoch for x in ledger_events):
raise SystemExit(1)
g=ev.get("gitea") or {}
if not all(g.get(k) is True for k in (
"machine_credential_persisted","machine_authenticated",
"write_permission_verified","non_main_push_verified",
"default_branch_protection_verified","tag_protection_verified",
"temporary_token_revoked","temporary_admin_removed")):
raise SystemExit(1)
o=ev.get("openbao") or {}
if not all(o.get(k) is True for k in (
"initialized_verified","unsealed_verified","audit_verified",
"transit_verified","kv_v2_verified","approle_verified",
"policy_verified","charter_signature_verified",
"self_init_secret_removed")):
raise SystemExit(1)
h=json.load(open(health))
if h.get("initialized") is not True or h.get("sealed") is not False:
raise SystemExit(1)
PY
then
ready=true
reason="72-hour unattended acceptance passed"
else
reason="one or more unattended acceptance gates failed"
fi
fi
rm -f /run/aios-acceptance-health.json
python3 - "$AIOS_ACCEPTANCE_STATUS.tmp" "$ready" "$reason" "$elapsed" <<'PY'
import json,pathlib,sys,time
out,ready,reason,elapsed=sys.argv[1:]
ok=ready=="true"
obj={
"schema":"AIOS_OPENBAO_UNATTENDED_ACCEPTANCE_V1",
"target_state":"OPENBAO_AUTONOMY_BOOTSTRAP_READY",
"passed":ok,
"elapsed_seconds":int(elapsed),
"reason":reason,
"evaluated_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
}
pathlib.Path(out).write_text(json.dumps(obj,sort_keys=True)+"\n")
PY
chmod 0640 "$AIOS_ACCEPTANCE_STATUS.tmp"
mv -f "$AIOS_ACCEPTANCE_STATUS.tmp" "$AIOS_ACCEPTANCE_STATUS"
if [[ "$ready" == true ]]; then
python3 - "$AIOS_AUTONOMY_STATUS.tmp" <<'PY'
import json,pathlib,time,os
path=os.environ["AIOS_AUTONOMY_STATUS"]
x={}
if pathlib.Path(path).is_file():
x=json.load(open(path))
x.update({
"state":"RUNNING",
"reason":"OPENBAO_AUTONOMY_BOOTSTRAP_READY",
"OPENBAO_AUTONOMY_BOOTSTRAP_READY":True,
"human_action_required":False,
"updated_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
})
pathlib.Path(os.environ["AIOS_AUTONOMY_STATUS"]+".tmp").write_text(json.dumps(x,sort_keys=True)+"\n")
PY
chmod 0640 "$AIOS_AUTONOMY_STATUS.tmp"
mv -f "$AIOS_AUTONOMY_STATUS.tmp" "$AIOS_AUTONOMY_STATUS"
fi
ACCEPTANCE
install -m 0555 -o root -g root "$ACCEPTANCE_BIN.tmp" "$ACCEPTANCE_BIN"
rm -f "$ACCEPTANCE_BIN.tmp"
cat >/etc/systemd/system/aios-openbao.service <<EOF
[Unit]
Description=AIOS OpenBao Autonomy Runtime
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=8
[Service]
Type=simple
User=aios-openbao
Group=aios-openbao
ExecStart=${BAO_LINK} server -config=${OPENBAO_CONFIG}
Restart=on-failure
RestartSec=5s
TimeoutStartSec=180
TimeoutStopSec=45
KillSignal=SIGINT
LimitNOFILE=65536
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=${BAO_ETC}
ReadWritePaths=${BAO_ROOT} ${AUDIT_DIR}
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/systemd/system/aios-autonomy-controller.service <<EOF
[Unit]
Description=AIOS OpenBao Autonomy Controller
After=aios-openbao.service
Requires=aios-openbao.service
[Service]
Type=simple
User=aios-autonomy
Group=aios-autonomy
ExecStart=${CONTROLLER_BIN}
Restart=always
RestartSec=10s
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
ReadOnlyPaths=${BAO_ETC} ${CREDENTIAL_ROOT}
ReadWritePaths=${ROOT}
Environment=AIOS_OPENBAO_ADDR=https://127.0.0.1:8200
Environment=AIOS_OPENBAO_CACERT=${TLS_CA}
Environment=AIOS_OPENBAO_ROLE_ID_FILE=${APPROLE_ROLE_ID_FILE}
Environment=AIOS_OPENBAO_SECRET_ID_FILE=${APPROLE_SECRET_RUNTIME_PATH}
Environment=AIOS_AUTONOMY_STATUS=${STATUS_FILE}
Environment=AIOS_CHARTER_FILE=${CHARTER_FILE}
Environment=AIOS_CONTROLLER_INTERVAL_FILE=${CONTROLLER_INTERVAL_FILE}
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/systemd/system/aios-openbao-backup.service <<EOF
[Unit]
Description=AIOS OpenBao Raft Snapshot
After=aios-openbao.service
Requires=aios-openbao.service
[Service]
Type=oneshot
User=aios-autonomy
Group=aios-autonomy
SupplementaryGroups=aios-openbao
ExecStart=${BACKUP_BIN}
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadOnlyPaths=${BAO_ETC} ${CREDENTIAL_ROOT}
ReadWritePaths=${ROOT}
Environment=AIOS_OPENBAO_ADDR=https://127.0.0.1:8200
Environment=AIOS_OPENBAO_CACERT=${TLS_CA}
Environment=AIOS_OPENBAO_ROLE_ID_FILE=${APPROLE_ROLE_ID_FILE}
Environment=AIOS_OPENBAO_SECRET_ID_FILE=${APPROLE_BACKUP_RUNTIME_PATH}
Environment=AIOS_BACKUP_DIR=${ROOT}/backups
Environment=AIOS_BACKUP_STATUS=${BACKUP_STATUS}
Environment=AIOS_BAO_BIN=${BAO_LINK}
EOF
cat >/etc/systemd/system/aios-openbao-backup.timer <<'EOF'
[Unit]
Description=Schedule AIOS OpenBao Raft Snapshots
[Timer]
OnBootSec=10min
OnUnitActiveSec=6h
RandomizedDelaySec=5min
Persistent=true
Unit=aios-openbao-backup.service
[Install]
WantedBy=timers.target
EOF
cat >/etc/systemd/system/aios-openbao-restore-test.service <<EOF
[Unit]
Description=AIOS OpenBao Isolated Restore Test
After=aios-openbao-backup.service
Requires=aios-openbao.service
[Service]
Type=oneshot
User=aios-openbao
Group=aios-openbao
SupplementaryGroups=aios-autonomy
ExecStart=${RESTORE_TEST_BIN}
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadOnlyPaths=${BAO_ETC} ${CREDENTIAL_ROOT}
ReadWritePaths=${ROOT}
Environment=AIOS_BACKUP_DIR=${ROOT}/backups
Environment=AIOS_RESTORE_ROOT=${ROOT}/restore-tests
Environment=AIOS_RESTORE_STATUS=${RESTORE_STATUS}
Environment=AIOS_BAO_BIN=${BAO_LINK}
Environment=AIOS_SEAL_KEY_FILE=/run/credentials/aios-openbao-restore-test.service/seal-key
Environment=AIOS_SEAL_KEY_ID_FILE=${SEAL_KEY_ID_FILE}
Environment=AIOS_CHARTER_FILE=${CHARTER_FILE}
Environment=AIOS_OPENBAO_ROLE_ID_FILE=${APPROLE_ROLE_ID_FILE}
Environment=AIOS_OPENBAO_SECRET_ID_FILE=${APPROLE_RESTORE_RUNTIME_PATH}
EOF
cat >/etc/systemd/system/aios-openbao-restore-test.timer <<'EOF'
[Unit]
Description=Schedule AIOS OpenBao Isolated Restore Tests
[Timer]
OnBootSec=25min
OnUnitActiveSec=24h
RandomizedDelaySec=10min
Persistent=true
Unit=aios-openbao-restore-test.service
[Install]
WantedBy=timers.target
EOF
cat >/etc/systemd/system/aios-openbao-fault-injection.service <<EOF
[Unit]
Description=AIOS OpenBao Controlled Fault Injection
After=aios-openbao.service
Requires=aios-openbao.service
[Service]
Type=oneshot
ExecStart=${FAULT_TEST_BIN}
Environment=AIOS_OPENBAO_ADDR=https://127.0.0.1:8200
Environment=AIOS_OPENBAO_CACERT=${TLS_CA}
Environment=AIOS_OPENBAO_ROLE_ID_FILE=${APPROLE_ROLE_ID_FILE}
Environment=AIOS_OPENBAO_SECRET_ID_FILE=${APPROLE_FAULT_RUNTIME_PATH}
Environment=AIOS_FAULT_STATUS=${FAULT_STATUS}
Environment=AIOS_OPENBAO_SERVICE=${BAO_SERVICE}
Environment=AIOS_GITEA_URL=${GITEA_URL}
Environment=AIOS_BAO_BIN=${BAO_LINK}
EOF
cat >/etc/systemd/system/aios-openbao-fault-injection.timer <<'EOF'
[Unit]
Description=Schedule AIOS OpenBao Controlled Fault Injection
[Timer]
OnBootSec=40min
OnUnitActiveSec=24h
RandomizedDelaySec=10min
Persistent=true
Unit=aios-openbao-fault-injection.service
[Install]
WantedBy=timers.target
EOF
cat >/etc/systemd/system/aios-autonomy-optimize.service <<EOF
[Unit]
Description=AIOS Low-Risk Runtime Optimization
After=aios-openbao.service
Requires=aios-openbao.service
[Service]
Type=oneshot
User=aios-autonomy
Group=aios-autonomy
ExecStart=${OPTIMIZE_BIN}
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadOnlyPaths=${BAO_ETC}
ReadWritePaths=${ROOT}
Environment=AIOS_OPENBAO_ADDR=https://127.0.0.1:8200
Environment=AIOS_OPENBAO_CACERT=${TLS_CA}
Environment=AIOS_CONTROLLER_INTERVAL_FILE=${CONTROLLER_INTERVAL_FILE}
Environment=AIOS_OPTIMIZATION_LEDGER=${OPTIMIZATION_LEDGER}
EOF
cat >/etc/systemd/system/aios-autonomy-optimize.timer <<'EOF'
[Unit]
Description=Schedule AIOS Low-Risk Runtime Optimization
[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
RandomizedDelaySec=5min
Persistent=true
Unit=aios-autonomy-optimize.service
[Install]
WantedBy=timers.target
EOF
cat >/etc/systemd/system/aios-autonomy-acceptance.service <<EOF
[Unit]
Description=Evaluate AIOS OpenBao 72-Hour Unattended Acceptance
After=aios-openbao.service aios-autonomy-controller.service
Requires=aios-openbao.service
[Service]
Type=oneshot
User=aios-autonomy
Group=aios-autonomy
ExecStart=${ACCEPTANCE_BIN}
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadOnlyPaths=${BAO_ETC}
ReadWritePaths=${ROOT} /run
Environment=AIOS_ACCEPTANCE_START_FILE=${ACCEPTANCE_START_FILE}
Environment=AIOS_ACCEPTANCE_SCRIPT_FINGERPRINT=${ACCEPTANCE_SCRIPT_FINGERPRINT}
Environment=AIOS_ACCEPTANCE_STATUS=${ACCEPTANCE_STATUS}
Environment=AIOS_BOOTSTRAP_EVIDENCE=${EVIDENCE}
Environment=AIOS_BACKUP_STATUS=${BACKUP_STATUS}
Environment=AIOS_RESTORE_STATUS=${RESTORE_STATUS}
Environment=AIOS_FAULT_STATUS=${FAULT_STATUS}
Environment=AIOS_OPTIMIZATION_LEDGER=${OPTIMIZATION_LEDGER}
Environment=AIOS_AUTONOMY_STATUS=${STATUS_FILE}
Environment=AIOS_OPENBAO_ADDR=https://127.0.0.1:8200
Environment=AIOS_OPENBAO_CACERT=${TLS_CA}
EOF
cat >/etc/systemd/system/aios-autonomy-acceptance.timer <<'EOF'
[Unit]
Description=Schedule AIOS OpenBao Unattended Acceptance Evaluation
[Timer]
OnBootSec=1h
OnUnitActiveSec=1h
RandomizedDelaySec=5min
Persistent=true
Unit=aios-autonomy-acceptance.service
[Install]
WantedBy=timers.target
EOF
chmod 0644 /etc/systemd/system/aios-*.service /etc/systemd/system/aios-*.timer
install -d -m 0755 "/etc/systemd/system/$BAO_SERVICE.d"
rm -f "/etc/systemd/system/$BAO_SERVICE.d/"20-static-seal-*.conf
if [[ "$SEAL_STORAGE" == "systemd-credential" ]]; then
cat >"/etc/systemd/system/$BAO_SERVICE.d/20-static-seal-credential.conf" <<EOF
[Service]
LoadCredentialEncrypted=seal-key:${SEAL_CRED_FILE}
EOF
fi
local credential_units=(
"$CONTROLLER_SERVICE"
"aios-openbao-backup.service"
"aios-openbao-fault-injection.service"
)
local unit
if [[ "$APPROLE_SECRET_STORAGE" == "systemd-credential" ]]; then
for unit in "${credential_units[@]}"; do
install -d -m 0755 "/etc/systemd/system/$unit.d"
cat >"/etc/systemd/system/$unit.d/20-approle-credential.conf" <<EOF
[Service]
LoadCredentialEncrypted=openbao-approle-secret-id:${APPROLE_SECRET_ID_CRED}
EOF
done
fi
install -d -m 0755 /etc/systemd/system/aios-openbao-restore-test.service.d
: > /etc/systemd/system/aios-openbao-restore-test.service.d/20-credentials.conf
if [[ "$SEAL_STORAGE" == "systemd-credential" ]]; then
printf '[Service]\nLoadCredentialEncrypted=seal-key:%s\n' "$SEAL_CRED_FILE" \
>>/etc/systemd/system/aios-openbao-restore-test.service.d/20-credentials.conf
else
sed -i "s|Environment=AIOS_SEAL_KEY_FILE=.*|Environment=AIOS_SEAL_KEY_FILE=${SEAL_KEY_FILE}|" \
/etc/systemd/system/aios-openbao-restore-test.service
fi
if [[ "$APPROLE_SECRET_STORAGE" == "systemd-credential" ]]; then
if ! grep -q '^\[Service\]' /etc/systemd/system/aios-openbao-restore-test.service.d/20-credentials.conf; then
printf '[Service]\n' >>/etc/systemd/system/aios-openbao-restore-test.service.d/20-credentials.conf
fi
printf 'LoadCredentialEncrypted=openbao-approle-secret-id:%s\n' "$APPROLE_SECRET_ID_CRED" \
>>/etc/systemd/system/aios-openbao-restore-test.service.d/20-credentials.conf
else
sed -i "s|Environment=AIOS_OPENBAO_SECRET_ID_FILE=.*|Environment=AIOS_OPENBAO_SECRET_ID_FILE=${APPROLE_SECRET_ID_FILE}|" \
/etc/systemd/system/aios-openbao-restore-test.service
fi
python3 - "$MANAGED_MARKER.tmp" "$SCRIPT_VERSION" <<'PY'
import json,pathlib,sys,time
pathlib.Path(sys.argv[1]).write_text(json.dumps({
"schema":"AIOS_OPENBAO_MANAGED_MARKER_V1",
"script_version":sys.argv[2],
"created_at":time.strftime("%Y-%m-%dT%H:%M:%SZ",time.gmtime()),
},sort_keys=True)+"\n")
PY
chown root:root "$MANAGED_MARKER.tmp"
chmod 0640 "$MANAGED_MARKER.tmp"
mv -f "$MANAGED_MARKER.tmp" "$MANAGED_MARKER"
systemctl daemon-reload
systemctl enable \
"$BAO_SERVICE" "$CONTROLLER_SERVICE" \
aios-openbao-backup.timer \
aios-openbao-restore-test.timer \
aios-openbao-fault-injection.timer \
aios-autonomy-optimize.timer \
aios-autonomy-acceptance.timer >/dev/null
SERVICES_ENABLED=true
HOST_MUTATED=true
}
start_and_verify_services() {
PHASE="service-start-and-health"
systemctl restart "$BAO_SERVICE"
SERVICES_STARTED=true
PRODUCTION_SERVICE_STARTED=true
HOST_MUTATED=true
systemctl is-active --quiet "$BAO_SERVICE" || die "$BAO_SERVICE is not active"
wait_openbao_ready "$BAO_READY_TIMEOUT" ||
die "OpenBao did not self-initialize and unseal within timeout"
configure_openbao_runtime_and_charter
remove_self_init_secrets_and_restart
systemctl restart "$CONTROLLER_SERVICE"
systemctl is-active --quiet "$CONTROLLER_SERVICE" ||
die "$CONTROLLER_SERVICE is not active"
# Run the first evidence cycle synchronously. A failure here blocks bootstrap
# instead of deferring a known-bad installation into the 72-hour window.
systemctl start aios-openbao-backup.service
systemctl start aios-openbao-restore-test.service
systemctl start aios-openbao-fault-injection.service
systemctl start aios-autonomy-optimize.service
systemctl start \
aios-openbao-backup.timer \
aios-openbao-restore-test.timer \
aios-openbao-fault-injection.timer \
aios-autonomy-optimize.timer \
aios-autonomy-acceptance.timer
python3 - "$BACKUP_STATUS" "$RESTORE_STATUS" "$FAULT_STATUS" "$OPTIMIZATION_LEDGER" <<'PY'
import json,pathlib,sys
backup,restore,fault,ledger=sys.argv[1:]
for p in (backup,restore,fault):
if not pathlib.Path(p).is_file():
raise SystemExit(f"missing evidence: {p}")
if json.load(open(backup)).get("passed") is not True:
raise SystemExit("initial backup failed")
if json.load(open(restore)).get("passed") is not True:
raise SystemExit("initial isolated restore failed")
if json.load(open(fault)).get("passed") is not True:
raise SystemExit("initial fault injection failed")
if not pathlib.Path(ledger).is_file() or pathlib.Path(ledger).stat().st_size == 0:
raise SystemExit("optimization ledger is empty")
PY
RELEASE_RUNTIME_READY_VERIFIED=true
SERVICES_HEALTHY=true
if [[ "$SEAL_STORAGE" == "systemd-credential" ]]; then
SEAL_RUNTIME_READ_VERIFIED=true
fi
if [[ "$SERVICES_HEALTHY" == true && "$PENDING_LEGACY_SEAL_KEY_REMOVAL" == true && -f "$SEAL_KEY_FILE" ]]; then
shred -u "$SEAL_KEY_FILE" 2>/dev/null || rm -f "$SEAL_KEY_FILE"
PENDING_LEGACY_SEAL_KEY_REMOVAL=false
fi
}
start_unattended_acceptance() {
PHASE="unattended-acceptance"
systemctl is-active --quiet "$CONTROLLER_SERVICE" ||
die "autonomy controller is not active"
systemctl is-active --quiet aios-autonomy-acceptance.timer ||
die "unattended acceptance timer is not active"
local reset_window=false
if [[ ! -s "$ACCEPTANCE_SCRIPT_FINGERPRINT" ]] ||
[[ "$(tr -d '\r\n' <"$ACCEPTANCE_SCRIPT_FINGERPRINT")" != "$SCRIPT_SHA256" ]]; then
reset_window=true
fi
if [[ "$reset_window" == true || ! -s "$ACCEPTANCE_START_FILE" ]]; then
date +%s >"$ACCEPTANCE_START_FILE.tmp"
chown aios-autonomy:aios-autonomy "$ACCEPTANCE_START_FILE.tmp"
chmod 0640 "$ACCEPTANCE_START_FILE.tmp"
mv -f "$ACCEPTANCE_START_FILE.tmp" "$ACCEPTANCE_START_FILE"
rm -f "$ACCEPTANCE_STATUS"
fi
printf '%s\n' "$SCRIPT_SHA256" >"$ACCEPTANCE_SCRIPT_FINGERPRINT.tmp"
chown aios-autonomy:aios-autonomy "$ACCEPTANCE_SCRIPT_FINGERPRINT.tmp"
chmod 0640 "$ACCEPTANCE_SCRIPT_FINGERPRINT.tmp"
mv -f "$ACCEPTANCE_SCRIPT_FINGERPRINT.tmp" "$ACCEPTANCE_SCRIPT_FINGERPRINT"
ACCEPTANCE_STARTED=true
write_status "RUNNING" "bootstrap verified; 72-hour unattended acceptance in progress"
}
main() {
preflight
install_identities_and_directories
capture_environment
select_release
download_and_verify_release
install_release
provision_static_seal
provision_tls
provision_openbao_bootstrap_identity
render_openbao_config
create_temp_gitea_admin
create_or_rotate_machine_credential
grant_and_verify_collaborator
verify_default_branch_protection
verify_tag_protection
verify_non_main_git_push
revoke_and_remove_temp_admin
install_services
start_and_verify_services
start_unattended_acceptance
PHASE="complete"
FINAL_RC=0
}
self_test() {
local script
script="$(readlink -f "${BASH_SOURCE[0]}")"
local report
report="$(mktemp)"
chmod 0600 "$report"
set +e
python3 - "$script" "$report" <<'PY'
import hashlib
import json
import pathlib
import re
import subprocess
import sys
import tempfile
script, report = sys.argv[1:]
path = pathlib.Path(script)
text = path.read_text()
body = text.split("\nself_test() {", 1)[0]
def embedded_shell_syntax_ok():
markers = ("SH", "LIB", "CONTROLLER", "BACKUP", "RESTORE",
"FAULT", "OPTIMIZE", "ACCEPTANCE")
for marker in markers:
matches = re.findall(
r"<<'" + re.escape(marker) + r"'\n(.*?)\n" + re.escape(marker) + r"\n",
body,
flags=re.S,
)
if len(matches) != 1:
return False
with tempfile.NamedTemporaryFile("w", delete=False) as handle:
handle.write(matches[0])
candidate = handle.name
try:
result = subprocess.run(
["bash", "-n", candidate],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if result.returncode != 0:
return False
finally:
pathlib.Path(candidate).unlink(missing_ok=True)
return True
def checksum_count(manifest, asset):
pattern = re.compile(r"^([0-9a-fA-F]{64}) ([ *])(.+)$")
return sum(
1
for raw in manifest.splitlines()
if (match := pattern.match(raw)) and match.group(3) == asset
)
def version_at_least(actual, minimum):
def parse(value):
parts = value.split(".")
if len(parts) != 3 or any(not p.isdigit() for p in parts):
raise ValueError(value)
return tuple(int(p) for p in parts)
return parse(actual) >= parse(minimum)
asset = "openbao_2.6.1_linux_amd64.tar.gz"
good_manifest = f"{'0' * 64} {asset}\n"
duplicate_manifest = good_manifest + good_manifest
missing_manifest = f"{'0' * 64} another.tar.gz\n"
checks = {
"outer_bash_syntax": subprocess.run(
["bash", "-n", script],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode == 0,
"embedded_shell_syntax": embedded_shell_syntax_ok(),
"self_contained_no_sibling_preflight":
"package missing openbao.hcl.template" not in body
and "package missing systemd/" not in body,
"declarative_self_initialization":
'initialize "secret_mounts"' in body
and 'initialize "approle"' in body,
"audit_implemented":
'audit "file" "aios"' in body
and 'file_path = "${AUDIT_LOG}"' in body,
"transit_runtime_key_implemented":
'path = "transit/keys/aios-runtime"' in body,
"transit_charter_key_implemented":
'path = "transit/keys/aios-charter"' in body,
"kv_v2_implemented":
'path = "sys/mounts/kv"' in body
and 'version = "2"' in body,
"approle_implemented":
'path = "sys/auth/approle"' in body
and 'custom-secret-id' in body,
"policy_implemented":
'path = "sys/policies/acl/aios-autonomy"' in body,
"charter_store_sign_verify":
'kv/data/aios/charter' in body
and 'transit/sign/aios-charter' in body
and 'transit/verify/aios-charter' in body,
"self_init_secret_removal_implemented":
'OPENBAO_INIT_SECRET_REMOVED=true' in body
and 'self-initialization stanza remained after successful initialization' in body,
"root_token_not_persisted":
"root_token" not in body.split(
'cat >"$RESTORE_TEST_BIN.tmp"', 1
)[0],
"openbao_ready_gate_not_bypassable":
'cannot disable the OpenBao readiness gate' in body
and 'if is_true "$REQUIRE_OPENBAO_READY"' not in body,
"branch_protection_gate_not_bypassable":
'cannot disable branch protection' in body
and 'if ! is_true "$REQUIRE_BRANCH_PROTECTION"' not in body,
"gitea_tag_api_version_gate":
'GITEA_MIN_TAG_PROTECTION_API_VERSION="1.23.0"' in body
and 'GITEA_TAG_PROTECTION_API_UNAVAILABLE' in body
and 'actual >= minimum' in body,
"gitea_1_22_6_rejected":
not version_at_least("1.22.6", "1.23.0"),
"gitea_1_23_0_accepted":
version_at_least("1.23.0", "1.23.0"),
"tag_protection_gate_implemented":
'/tag_protections' in body
and '"name_pattern":"*","whitelist_usernames":[],"whitelist_teams":[]'
in body
and '"tag_protection_verified"' in body,
"machine_credential_persisted":
'MACHINE_CREDENTIAL_STORAGE="protected-file"' in body
and 'machine_credential_persisted' in body,
"backup_implemented":
'sys/storage/raft/snapshot' in body
and 'operator raft snapshot inspect' in body,
"isolated_restore_implemented":
'operator raft snapshot restore -force' in body,
"fault_injection_implemented":
'systemctl kill --kill-who=main --signal=SIGKILL' in body,
"continuous_optimization_implemented":
'AIOS_AUTONOMY_OPTIMIZATION_EVENT_V1' in body,
"72h_acceptance_gate":
'elapsed >= 259200' in body
and '"tag_protection_verified"' in body,
"exact_checksum_good_fixture":
checksum_count(good_manifest, asset) == 1,
"exact_checksum_duplicate_rejected":
checksum_count(duplicate_manifest, asset) == 2,
"exact_checksum_missing_rejected":
checksum_count(missing_manifest, asset) == 0,
"no_git_force_push":
re.search(
r'\bgit\b[^\n]*\bpush\b[^\n]*--force(?:-with-lease)?\b',
body,
) is None,
"no_git_tag_mutation":
re.search(
r'\bgit\b[^\n]*\b(push|tag)\b[^\n]*refs/tags',
body,
) is None,
"ready_not_predeclared":
'"value": False' in body
and 'OPENBAO_AUTONOMY_BOOTSTRAP_READY":True' in body,
}
out = {
"schema": "AIOS_OPENBAO_BOOTSTRAP_STATIC_SELF_TEST_V3",
"scope": "static_and_fixture_only",
"runtime_claims": False,
"script_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
"passed": sum(checks.values()),
"total": len(checks),
"checks": checks,
}
pathlib.Path(report).write_text(json.dumps(out, sort_keys=True) + "\n")
raise SystemExit(0 if all(checks.values()) else 1)
PY
local rc=$?
set -e
cat "$report"
rm -f "$report"
return "$rc"
}
if [[ "${1:-}" == "--self-test" ]]; then
trap - ERR EXIT INT TERM
self_test
exit $?
fi
main "$@"