From 69e70ea8e8b2be646efafa63221b55e6053af578 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Tue, 19 May 2026 18:39:02 -0300 Subject: [PATCH 01/22] infra: add script to rent server on scaleway --- infra/rent_baremetal.sh | 150 ++++++++++++++++++++++++++++++++++++++++ infra/user_data.yaml | 10 +++ 2 files changed, 160 insertions(+) create mode 100755 infra/rent_baremetal.sh create mode 100644 infra/user_data.yaml diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh new file mode 100755 index 00000000..e06b2d95 --- /dev/null +++ b/infra/rent_baremetal.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Rent a Scaleway Elastic Metal server with HOURLY billing. +# +# Usage: infra/rent_baremetal.sh +# +# Env var overrides (all optional): +# SCW_ZONE default: fr-par-2 +# SCW_TYPE default: EM-I320E-NVME +# USER_DATA_FILE default: /infra/user_data.yaml +# READY_TIMEOUT default: 1800 (seconds) +# +# Requires: scw, jq. +# To stop hourly charges when done: scw baremetal server delete zone= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +SCW_ZONE="${SCW_ZONE:-fr-par-2}" +SCW_TYPE="${SCW_TYPE:-EM-I320E-NVME}" +USER_DATA_FILE="${USER_DATA_FILE:-$SCRIPT_DIR/user_data.yaml}" +READY_TIMEOUT="${READY_TIMEOUT:-1800}" + +err() { echo -e "${RED}error:${NC} $*" >&2; } +info() { echo -e "${BOLD}$*${NC}"; } +ok() { echo -e "${GREEN}$*${NC}"; } + +if [ $# -lt 1 ] || [ -z "${1:-}" ]; then + err "missing " + echo "Usage: $0 " >&2 + exit 2 +fi +SERVER_NAME="$1" + +if ! command -v scw >/dev/null 2>&1; then + err "scw CLI not found on PATH. Install: https://github.com/scaleway/scaleway-cli" + exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + err "jq not found on PATH." + exit 1 +fi +if [ ! -r "$USER_DATA_FILE" ]; then + err "user-data file not found or unreadable: $USER_DATA_FILE" + exit 1 +fi + +EXPECTED_PROFILE="vm" +if [ -n "${SCW_PROFILE:-}" ]; then + ACTIVE_PROFILE="$SCW_PROFILE" +elif [ -r "$HOME/.config/scw/config.yaml" ]; then + ACTIVE_PROFILE=$(grep -E '^active_profile:' "$HOME/.config/scw/config.yaml" \ + | head -n1 | awk '{print $2}' | tr -d '"' | tr -d "'") +else + ACTIVE_PROFILE="" +fi +if [ "$ACTIVE_PROFILE" != "$EXPECTED_PROFILE" ]; then + err "active scw profile is '${ACTIVE_PROFILE:-}', expected '$EXPECTED_PROFILE'" + echo " Switch with: export SCW_PROFILE=$EXPECTED_PROFILE" >&2 + echo " Or set 'active_profile: $EXPECTED_PROFILE' in ~/.config/scw/config.yaml" >&2 + exit 1 +fi +ok "Using scw profile: $EXPECTED_PROFILE" + +info "Resolving hourly offer for type=$SCW_TYPE zone=$SCW_ZONE..." +OFFER_JSON=$(scw baremetal offer list \ + zone="$SCW_ZONE" \ + name="$SCW_TYPE" \ + subscription-period=hourly \ + -o json) +OFFER_ID=$(echo "$OFFER_JSON" | jq -r '.[0].id // empty') + +if [ -z "$OFFER_ID" ]; then + err "no hourly offer for $SCW_TYPE in $SCW_ZONE — refusing to create a monthly server" + echo "Offers returned:" >&2 + echo "$OFFER_JSON" >&2 + exit 1 +fi +ok "Resolved hourly offer ID: $OFFER_ID" + +USER_DATA=$(cat "$USER_DATA_FILE") + +info "Creating server name=$SERVER_NAME..." +CREATE_JSON=$(scw baremetal server create \ + zone="$SCW_ZONE" \ + type="$OFFER_ID" \ + name="$SERVER_NAME" \ + user-data="$USER_DATA" \ + -o json) + +SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.id // empty') +if [ -z "$SERVER_ID" ]; then + err "server create did not return an id. Raw response:" + echo "$CREATE_JSON" >&2 + exit 1 +fi +ok "Created server id=$SERVER_ID. Waiting up to ${READY_TIMEOUT}s for status=ready..." + +DEADLINE=$(( $(date +%s) + READY_TIMEOUT )) +LAST_STATUS="" +while [ "$(date +%s)" -lt "$DEADLINE" ]; do + GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) + STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') + if [ "$STATUS" != "$LAST_STATUS" ]; then + echo -e " status: ${YELLOW}$STATUS${NC}" + LAST_STATUS="$STATUS" + else + echo -n "." + fi + if [ "$STATUS" = "ready" ]; then + echo + break + fi + if [ "$STATUS" = "error" ] || [ "$STATUS" = "locked" ]; then + echo + err "server entered terminal status: $STATUS" + echo "$GET_JSON" >&2 + exit 1 + fi + sleep 15 +done + +GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) +FINAL_STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') +if [ "$FINAL_STATUS" != "ready" ]; then + err "timed out after ${READY_TIMEOUT}s — last status=$FINAL_STATUS" + exit 1 +fi + +PUBLIC_IP=$(echo "$GET_JSON" | jq -r '.ips[] | select(.version == "IPv4") | .address' | head -n1) + +echo +ok "Server ready." +echo " id: $SERVER_ID" +echo " name: $SERVER_NAME" +echo " zone: $SCW_ZONE" +echo " type: $SCW_TYPE (hourly offer $OFFER_ID)" +echo " ip: $PUBLIC_IP" +echo +echo " ssh app@$PUBLIC_IP" +echo +echo "To stop hourly charges:" +echo " scw baremetal server delete $SERVER_ID zone=$SCW_ZONE" diff --git a/infra/user_data.yaml b/infra/user_data.yaml new file mode 100644 index 00000000..e1b4e3f7 --- /dev/null +++ b/infra/user_data.yaml @@ -0,0 +1,10 @@ +#cloud-config +users: + - name: app + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6mrcWIyU+/LrNZLivNIOYr6ld/CXefoq1hyXLsHDfV it +runcmd: + - mkdir -p /repos + - chown app:app /repos From 34675ea6f0abc22c165dd586a840883306f37be7 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Tue, 19 May 2026 18:56:58 -0300 Subject: [PATCH 02/22] fix user-data --- infra/rent_baremetal.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index e06b2d95..a2f4403e 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -92,7 +92,7 @@ CREATE_JSON=$(scw baremetal server create \ zone="$SCW_ZONE" \ type="$OFFER_ID" \ name="$SERVER_NAME" \ - user-data="$USER_DATA" \ + user-data.cloud-init="$USER_DATA" \ -o json) SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.id // empty') From e28da0951e01c34927977acddcfb08fbb1225f60 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 11:43:36 -0300 Subject: [PATCH 03/22] install os and add ssh keys --- infra/rent_baremetal.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index a2f4403e..37fd40ff 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -25,6 +25,7 @@ NC='\033[0m' SCW_ZONE="${SCW_ZONE:-fr-par-2}" SCW_TYPE="${SCW_TYPE:-EM-I320E-NVME}" +SCW_OS_ID="${SCW_OS_ID:-83640d93-a0b8-45ad-9c9f-30cae48380a4}" # Debian USER_DATA_FILE="${USER_DATA_FILE:-$SCRIPT_DIR/user_data.yaml}" READY_TIMEOUT="${READY_TIMEOUT:-1800}" @@ -85,14 +86,28 @@ if [ -z "$OFFER_ID" ]; then fi ok "Resolved hourly offer ID: $OFFER_ID" -USER_DATA=$(cat "$USER_DATA_FILE") +info "Enumerating SSH keys from scw iam ssh-key list..." +SSH_KEYS_JSON=$(scw iam ssh-key list -o json) +mapfile -t SSH_KEY_IDS < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') +if [ ${#SSH_KEY_IDS[@]} -eq 0 ]; then + err "no SSH keys found in scw iam ssh-key list — register one first ('scw iam ssh-key create')" + exit 1 +fi +ok "Found ${#SSH_KEY_IDS[@]} SSH key(s) to install" + +SSH_KEY_ARGS=() +for i in "${!SSH_KEY_IDS[@]}"; do + SSH_KEY_ARGS+=("install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") +done -info "Creating server name=$SERVER_NAME..." +info "Creating server name=$SERVER_NAME (user-data skipped for now)..." CREATE_JSON=$(scw baremetal server create \ zone="$SCW_ZONE" \ type="$OFFER_ID" \ name="$SERVER_NAME" \ - user-data.cloud-init="$USER_DATA" \ + install.os-id="$SCW_OS_ID" \ + install.hostname="$SERVER_NAME" \ + "${SSH_KEY_ARGS[@]}" \ -o json) SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.id // empty') From 6c0b667d963e9b259587a65b5442409e2a791cd4 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 11:45:15 -0300 Subject: [PATCH 04/22] do not use mapfile --- infra/rent_baremetal.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 37fd40ff..4db85519 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -88,7 +88,10 @@ ok "Resolved hourly offer ID: $OFFER_ID" info "Enumerating SSH keys from scw iam ssh-key list..." SSH_KEYS_JSON=$(scw iam ssh-key list -o json) -mapfile -t SSH_KEY_IDS < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') +SSH_KEY_IDS=() +while IFS= read -r line; do + [ -n "$line" ] && SSH_KEY_IDS+=("$line") +done < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') if [ ${#SSH_KEY_IDS[@]} -eq 0 ]; then err "no SSH keys found in scw iam ssh-key list — register one first ('scw iam ssh-key create')" exit 1 From d8407f068d282668005a79d8dd858bf893131b3a Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 11:48:24 -0300 Subject: [PATCH 05/22] filter ssh keys by project --- infra/rent_baremetal.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 4db85519..debf2ef3 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -86,14 +86,22 @@ if [ -z "$OFFER_ID" ]; then fi ok "Resolved hourly offer ID: $OFFER_ID" -info "Enumerating SSH keys from scw iam ssh-key list..." -SSH_KEYS_JSON=$(scw iam ssh-key list -o json) +info "Resolving active project ID..." +PROJECT_ID=$(scw config get default_project_id 2>/dev/null | tr -d '[:space:]') +if [ -z "$PROJECT_ID" ]; then + err "could not resolve default_project_id from scw config (profile $EXPECTED_PROFILE)" + exit 1 +fi +ok "Project ID: $PROJECT_ID" + +info "Enumerating SSH keys in project $PROJECT_ID..." +SSH_KEYS_JSON=$(scw iam ssh-key list project-id="$PROJECT_ID" -o json) SSH_KEY_IDS=() while IFS= read -r line; do [ -n "$line" ] && SSH_KEY_IDS+=("$line") done < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') if [ ${#SSH_KEY_IDS[@]} -eq 0 ]; then - err "no SSH keys found in scw iam ssh-key list — register one first ('scw iam ssh-key create')" + err "no SSH keys found in project $PROJECT_ID — register one first ('scw iam ssh-key create')" exit 1 fi ok "Found ${#SSH_KEY_IDS[@]} SSH key(s) to install" From ea1b3808432862e61c4485363b8cab533ab13a30 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 11:51:35 -0300 Subject: [PATCH 06/22] set project id --- infra/rent_baremetal.sh | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index debf2ef3..048dc5af 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -26,6 +26,7 @@ NC='\033[0m' SCW_ZONE="${SCW_ZONE:-fr-par-2}" SCW_TYPE="${SCW_TYPE:-EM-I320E-NVME}" SCW_OS_ID="${SCW_OS_ID:-83640d93-a0b8-45ad-9c9f-30cae48380a4}" # Debian +SCW_PROJECT_ID="${SCW_PROJECT_ID:-946cfb34-d351-48c4-8566-127e7727e15f}" USER_DATA_FILE="${USER_DATA_FILE:-$SCRIPT_DIR/user_data.yaml}" READY_TIMEOUT="${READY_TIMEOUT:-1800}" @@ -86,22 +87,14 @@ if [ -z "$OFFER_ID" ]; then fi ok "Resolved hourly offer ID: $OFFER_ID" -info "Resolving active project ID..." -PROJECT_ID=$(scw config get default_project_id 2>/dev/null | tr -d '[:space:]') -if [ -z "$PROJECT_ID" ]; then - err "could not resolve default_project_id from scw config (profile $EXPECTED_PROFILE)" - exit 1 -fi -ok "Project ID: $PROJECT_ID" - -info "Enumerating SSH keys in project $PROJECT_ID..." -SSH_KEYS_JSON=$(scw iam ssh-key list project-id="$PROJECT_ID" -o json) +info "Enumerating SSH keys in project $SCW_PROJECT_ID..." +SSH_KEYS_JSON=$(scw iam ssh-key list project-id="$SCW_PROJECT_ID" -o json) SSH_KEY_IDS=() while IFS= read -r line; do [ -n "$line" ] && SSH_KEY_IDS+=("$line") done < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') if [ ${#SSH_KEY_IDS[@]} -eq 0 ]; then - err "no SSH keys found in project $PROJECT_ID — register one first ('scw iam ssh-key create')" + err "no SSH keys found in project $SCW_PROJECT_ID — register one first ('scw iam ssh-key create')" exit 1 fi ok "Found ${#SSH_KEY_IDS[@]} SSH key(s) to install" From 9cd43292b0924e563a6e58a84211d9c81d5be4c2 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 12:08:35 -0300 Subject: [PATCH 07/22] use batch-create --- infra/rent_baremetal.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 048dc5af..97dbc58d 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -71,21 +71,19 @@ if [ "$ACTIVE_PROFILE" != "$EXPECTED_PROFILE" ]; then fi ok "Using scw profile: $EXPECTED_PROFILE" -info "Resolving hourly offer for type=$SCW_TYPE zone=$SCW_ZONE..." +info "Resolving hourly offer ID for type=$SCW_TYPE zone=$SCW_ZONE..." OFFER_JSON=$(scw baremetal offer list \ zone="$SCW_ZONE" \ name="$SCW_TYPE" \ subscription-period=hourly \ -o json) -OFFER_ID=$(echo "$OFFER_JSON" | jq -r '.[0].id // empty') - +OFFER_ID=$(echo "$OFFER_JSON" | jq -r --arg n "$SCW_TYPE" '[.[] | select(.name == $n)] | .[0].id // empty') if [ -z "$OFFER_ID" ]; then - err "no hourly offer for $SCW_TYPE in $SCW_ZONE — refusing to create a monthly server" - echo "Offers returned:" >&2 + err "no hourly offer named '$SCW_TYPE' in $SCW_ZONE — refusing to create a non-hourly server" echo "$OFFER_JSON" >&2 exit 1 fi -ok "Resolved hourly offer ID: $OFFER_ID" +ok "Hourly offer ID: $OFFER_ID" info "Enumerating SSH keys in project $SCW_PROJECT_ID..." SSH_KEYS_JSON=$(scw iam ssh-key list project-id="$SCW_PROJECT_ID" -o json) @@ -101,20 +99,22 @@ ok "Found ${#SSH_KEY_IDS[@]} SSH key(s) to install" SSH_KEY_ARGS=() for i in "${!SSH_KEY_IDS[@]}"; do - SSH_KEY_ARGS+=("install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") + SSH_KEY_ARGS+=("common-configuration.install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") done -info "Creating server name=$SERVER_NAME (user-data skipped for now)..." -CREATE_JSON=$(scw baremetal server create \ +info "Creating server name=$SERVER_NAME via batch-create (user-data skipped for now)..." +CREATE_JSON=$(scw baremetal server batch-create \ zone="$SCW_ZONE" \ - type="$OFFER_ID" \ - name="$SERVER_NAME" \ - install.os-id="$SCW_OS_ID" \ - install.hostname="$SERVER_NAME" \ + common-configuration.offer-id="$OFFER_ID" \ + common-configuration.project-id="$SCW_PROJECT_ID" \ + common-configuration.name="$SERVER_NAME" \ + common-configuration.install.os-id="$SCW_OS_ID" \ + common-configuration.install.hostname="$SERVER_NAME" \ "${SSH_KEY_ARGS[@]}" \ + servers.0.hostname="$SERVER_NAME" \ -o json) -SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.id // empty') +SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.servers[0].id // empty') if [ -z "$SERVER_ID" ]; then err "server create did not return an id. Raw response:" echo "$CREATE_JSON" >&2 From 29b96afbe5915cb1bca01c44d755abfd6bb88938 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 12:14:59 -0300 Subject: [PATCH 08/22] add user-data --- infra/rent_baremetal.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 97dbc58d..1dce04cd 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -102,7 +102,9 @@ for i in "${!SSH_KEY_IDS[@]}"; do SSH_KEY_ARGS+=("common-configuration.install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") done -info "Creating server name=$SERVER_NAME via batch-create (user-data skipped for now)..." +USER_DATA=$(cat "$USER_DATA_FILE") + +info "Creating server name=$SERVER_NAME via batch-create..." CREATE_JSON=$(scw baremetal server batch-create \ zone="$SCW_ZONE" \ common-configuration.offer-id="$OFFER_ID" \ @@ -110,6 +112,7 @@ CREATE_JSON=$(scw baremetal server batch-create \ common-configuration.name="$SERVER_NAME" \ common-configuration.install.os-id="$SCW_OS_ID" \ common-configuration.install.hostname="$SERVER_NAME" \ + common-configuration.user-data="$USER_DATA" \ "${SSH_KEY_ARGS[@]}" \ servers.0.hostname="$SERVER_NAME" \ -o json) From 222b0c20ec9cfd32fb87961c8967a2405614e222 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 12:27:21 -0300 Subject: [PATCH 09/22] wait until os is installed --- infra/rent_baremetal.sh | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 1dce04cd..757f5073 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -123,20 +123,22 @@ if [ -z "$SERVER_ID" ]; then echo "$CREATE_JSON" >&2 exit 1 fi -ok "Created server id=$SERVER_ID. Waiting up to ${READY_TIMEOUT}s for status=ready..." +ok "Created server id=$SERVER_ID. Waiting up to ${READY_TIMEOUT}s for status=ready AND install.status=completed..." DEADLINE=$(( $(date +%s) + READY_TIMEOUT )) -LAST_STATUS="" +LAST_STATE="" while [ "$(date +%s)" -lt "$DEADLINE" ]; do GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') - if [ "$STATUS" != "$LAST_STATUS" ]; then - echo -e " status: ${YELLOW}$STATUS${NC}" - LAST_STATUS="$STATUS" + INSTALL_STATUS=$(echo "$GET_JSON" | jq -r '.install.status // "pending"') + STATE="$STATUS / install=$INSTALL_STATUS" + if [ "$STATE" != "$LAST_STATE" ]; then + echo -e " ${YELLOW}$STATE${NC}" + LAST_STATE="$STATE" else echo -n "." fi - if [ "$STATUS" = "ready" ]; then + if [ "$STATUS" = "ready" ] && [ "$INSTALL_STATUS" = "completed" ]; then echo break fi @@ -146,13 +148,20 @@ while [ "$(date +%s)" -lt "$DEADLINE" ]; do echo "$GET_JSON" >&2 exit 1 fi + if [ "$INSTALL_STATUS" = "error" ]; then + echo + err "install entered terminal status: $INSTALL_STATUS" + echo "$GET_JSON" >&2 + exit 1 + fi sleep 15 done GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) FINAL_STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') -if [ "$FINAL_STATUS" != "ready" ]; then - err "timed out after ${READY_TIMEOUT}s — last status=$FINAL_STATUS" +FINAL_INSTALL=$(echo "$GET_JSON" | jq -r '.install.status // "pending"') +if [ "$FINAL_STATUS" != "ready" ] || [ "$FINAL_INSTALL" != "completed" ]; then + err "timed out after ${READY_TIMEOUT}s — last status=$FINAL_STATUS install=$FINAL_INSTALL" exit 1 fi From 41156085b25b88448236f37a2661f76a72737463 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 13:12:55 -0300 Subject: [PATCH 10/22] add provision script --- infra/provision.sh | 89 +++++++++++++++++++++++++++++++++++++++++ infra/rent_baremetal.sh | 39 +++++++++++++----- infra/user_data.yaml | 10 ----- 3 files changed, 118 insertions(+), 20 deletions(-) create mode 100755 infra/provision.sh delete mode 100644 infra/user_data.yaml diff --git a/infra/provision.sh b/infra/provision.sh new file mode 100755 index 00000000..962b1978 --- /dev/null +++ b/infra/provision.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# Provision a freshly rented Scaleway Elastic Metal Debian server. +# Invoked remotely from infra/rent_baremetal.sh as: +# ssh root@ 'bash -s' < infra/provision.sh +# +# Idempotent where reasonable so a re-run on the same box does not break things. + +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive +APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) + +echo "==> apt update + upgrade" +apt-get update -y +apt-get upgrade "${APT_OPTS[@]}" + +echo "==> Installing packages" +apt-get install "${APT_OPTS[@]}" \ + ca-certificates \ + curl \ + wget \ + gnupg \ + vim \ + git \ + zip \ + unzip \ + openssl \ + libssl-dev \ + build-essential \ + rsyslog \ + htop \ + rsync \ + pkg-config \ + locales \ + ufw + +echo "==> Creating users: admin (sudo), app (no sudo)" +for u in admin app; do + if ! id "$u" >/dev/null 2>&1; then + useradd -m -s /bin/bash "$u" + fi +done + +echo 'admin ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-admin +chmod 0440 /etc/sudoers.d/90-admin + +echo "==> Propagating root's authorized_keys to admin and app" +if [ ! -s /root/.ssh/authorized_keys ]; then + echo "ERROR: /root/.ssh/authorized_keys missing or empty — refusing to harden sshd, you would lose access." >&2 + exit 1 +fi +for u in admin app; do + install -d -m 0700 -o "$u" -g "$u" "/home/$u/.ssh" + install -m 0600 -o "$u" -g "$u" /root/.ssh/authorized_keys "/home/$u/.ssh/authorized_keys" +done + +echo "==> Writing /etc/environment" +cat > /etc/environment <<'EOF' +LANG=en_US.UTF-8 +LC_ALL=C +LANGUAGE=en_US.UTF-8 +LC_TYPE=en_US.UTF-8 +LC_CTYPE=en_US.UTF-8 +EOF +locale-gen en_US.UTF-8 + +echo "==> Writing /etc/ssh/sshd_config.d/99-hardening.conf" +cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' +PermitRootLogin no +PasswordAuthentication no +AllowAgentForwarding no +AllowTcpForwarding no +PubkeyAuthentication yes +MaxAuthTries 5 +LoginGraceTime 30 +ClientAliveInterval 300 +ClientAliveCountMax 2 +X11Forwarding no +PermitEmptyPasswords no +PermitUserEnvironment no +LogLevel VERBOSE +EOF +chmod 0644 /etc/ssh/sshd_config.d/99-hardening.conf + +echo "==> Validating sshd config and reloading" +sshd -t +systemctl reload ssh + +echo "==> Done. From now on log in as admin@ (sudo) or app@ (no sudo). Root SSH is disabled." diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 757f5073..c6168dae 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -6,10 +6,10 @@ # Env var overrides (all optional): # SCW_ZONE default: fr-par-2 # SCW_TYPE default: EM-I320E-NVME -# USER_DATA_FILE default: /infra/user_data.yaml +# PROVISION_FILE default: /infra/provision.sh # READY_TIMEOUT default: 1800 (seconds) # -# Requires: scw, jq. +# Requires: scw, jq, ssh. # To stop hourly charges when done: scw baremetal server delete zone= set -euo pipefail @@ -27,7 +27,7 @@ SCW_ZONE="${SCW_ZONE:-fr-par-2}" SCW_TYPE="${SCW_TYPE:-EM-I320E-NVME}" SCW_OS_ID="${SCW_OS_ID:-83640d93-a0b8-45ad-9c9f-30cae48380a4}" # Debian SCW_PROJECT_ID="${SCW_PROJECT_ID:-946cfb34-d351-48c4-8566-127e7727e15f}" -USER_DATA_FILE="${USER_DATA_FILE:-$SCRIPT_DIR/user_data.yaml}" +PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" READY_TIMEOUT="${READY_TIMEOUT:-1800}" err() { echo -e "${RED}error:${NC} $*" >&2; } @@ -49,8 +49,12 @@ if ! command -v jq >/dev/null 2>&1; then err "jq not found on PATH." exit 1 fi -if [ ! -r "$USER_DATA_FILE" ]; then - err "user-data file not found or unreadable: $USER_DATA_FILE" +if [ ! -r "$PROVISION_FILE" ]; then + err "provision script not found or unreadable: $PROVISION_FILE" + exit 1 +fi +if ! command -v ssh >/dev/null 2>&1; then + err "ssh not found on PATH." exit 1 fi @@ -102,8 +106,6 @@ for i in "${!SSH_KEY_IDS[@]}"; do SSH_KEY_ARGS+=("common-configuration.install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") done -USER_DATA=$(cat "$USER_DATA_FILE") - info "Creating server name=$SERVER_NAME via batch-create..." CREATE_JSON=$(scw baremetal server batch-create \ zone="$SCW_ZONE" \ @@ -112,7 +114,6 @@ CREATE_JSON=$(scw baremetal server batch-create \ common-configuration.name="$SERVER_NAME" \ common-configuration.install.os-id="$SCW_OS_ID" \ common-configuration.install.hostname="$SERVER_NAME" \ - common-configuration.user-data="$USER_DATA" \ "${SSH_KEY_ARGS[@]}" \ servers.0.hostname="$SERVER_NAME" \ -o json) @@ -167,15 +168,33 @@ fi PUBLIC_IP=$(echo "$GET_JSON" | jq -r '.ips[] | select(.version == "IPv4") | .address' | head -n1) +info "Waiting for sshd on $PUBLIC_IP..." +SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes) +for attempt in 1 2 3 4 5 6 7 8 9 10; do + if ssh "${SSH_OPTS[@]}" "root@$PUBLIC_IP" true 2>/dev/null; then + ok "ssh root@$PUBLIC_IP ready (attempt $attempt)" + break + fi + if [ "$attempt" -eq 10 ]; then + err "ssh root@$PUBLIC_IP did not accept connections after 10 attempts" + exit 1 + fi + sleep 10 +done + +info "Running $PROVISION_FILE on the server (this also disables root ssh at the end)..." +ssh "${SSH_OPTS[@]}" "root@$PUBLIC_IP" 'bash -s' < "$PROVISION_FILE" + echo -ok "Server ready." +ok "Server ready and provisioned." echo " id: $SERVER_ID" echo " name: $SERVER_NAME" echo " zone: $SCW_ZONE" echo " type: $SCW_TYPE (hourly offer $OFFER_ID)" echo " ip: $PUBLIC_IP" echo -echo " ssh app@$PUBLIC_IP" +echo " ssh admin@$PUBLIC_IP # sudo" +echo " ssh app@$PUBLIC_IP # no sudo" echo echo "To stop hourly charges:" echo " scw baremetal server delete $SERVER_ID zone=$SCW_ZONE" diff --git a/infra/user_data.yaml b/infra/user_data.yaml deleted file mode 100644 index e1b4e3f7..00000000 --- a/infra/user_data.yaml +++ /dev/null @@ -1,10 +0,0 @@ -#cloud-config -users: - - name: app - sudo: ALL=(ALL) NOPASSWD:ALL - shell: /bin/bash - ssh_authorized_keys: - - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6mrcWIyU+/LrNZLivNIOYr6ld/CXefoq1hyXLsHDfV it -runcmd: - - mkdir -p /repos - - chown app:app /repos From 7ed63a91f8d0d8217700ffd33a2f913f93172cd5 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 15:27:50 -0300 Subject: [PATCH 11/22] separate provisioning from renting --- infra/provision.sh | 187 +++++++++++++++++++++++++++++++------- infra/provision_server.sh | 93 +++++++++++++++++++ infra/rent_baremetal.sh | 43 ++++----- 3 files changed, 265 insertions(+), 58 deletions(-) create mode 100755 infra/provision_server.sh diff --git a/infra/provision.sh b/infra/provision.sh index 962b1978..10bd7001 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -1,60 +1,182 @@ #!/bin/bash # Provision a freshly rented Scaleway Elastic Metal Debian server. -# Invoked remotely from infra/rent_baremetal.sh as: -# ssh root@ 'bash -s' < infra/provision.sh +# Invoked remotely from infra/provision_server.sh as: +# ssh root@ bash -s < infra/provision.sh # -# Idempotent where reasonable so a re-run on the same box does not break things. +# Optional input (placed by provision_server.sh before SSH'ing in): +# /tmp/lambda_vm_read_only_key GitHub deploy key. If present it is installed +# to /home/app/.ssh/lambda_vm_read_only and used to +# clone yetanotherco/lambda_vm. If absent the +# clone is skipped. +# +# Idempotent — safe to re-run. set -euo pipefail +log() { printf '\n=== %s ===\n' "$*"; } + +# --- 1. apt update + upgrade ------------------------------------------------- +log "apt update + upgrade" export DEBIAN_FRONTEND=noninteractive APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) - -echo "==> apt update + upgrade" apt-get update -y apt-get upgrade "${APT_OPTS[@]}" -echo "==> Installing packages" +# --- 2. apt packages --------------------------------------------------------- +log "apt install base packages + clang/lld/llvm + xz-utils" apt-get install "${APT_OPTS[@]}" \ - ca-certificates \ - curl \ - wget \ - gnupg \ - vim \ - git \ - zip \ - unzip \ - openssl \ - libssl-dev \ - build-essential \ - rsyslog \ - htop \ - rsync \ - pkg-config \ - locales \ - ufw - -echo "==> Creating users: admin (sudo), app (no sudo)" + ca-certificates curl wget gnupg vim git zip unzip openssl libssl-dev \ + build-essential rsyslog htop rsync pkg-config locales ufw \ + clang lld llvm xz-utils + +# --- 3. users: admin (sudo) + app (no sudo) ---------------------------------- +log "users: admin (sudo) + app (no sudo)" for u in admin app; do if ! id "$u" >/dev/null 2>&1; then useradd -m -s /bin/bash "$u" fi done - echo 'admin ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-admin chmod 0440 /etc/sudoers.d/90-admin -echo "==> Propagating root's authorized_keys to admin and app" +# --- 4. authorized_keys for admin and app ------------------------------------ +log "authorized_keys: propagate root's keys + append hardcoded team keys" if [ ! -s /root/.ssh/authorized_keys ]; then - echo "ERROR: /root/.ssh/authorized_keys missing or empty — refusing to harden sshd, you would lose access." >&2 + echo "ERROR: /root/.ssh/authorized_keys missing or empty — refusing to harden sshd." >&2 exit 1 fi +TEAM_KEYS=( + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFzvQKhE/xqRxHbit/dZNej7T5eVLmF8CAGL7to6o3QY joaquin@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2GAeixuqP4XwujuSK9KDgdmyglGzlQQsXztnve+bra gabriel@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQnPPUb4gzmsmjDP98mNKXbpHrp9bIIL7QiRjyWEG6f julian@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6mrcWIyU+/LrNZLivNIOYr6ld/CXefoq1hyXLsHDfV it" +) for u in admin app; do install -d -m 0700 -o "$u" -g "$u" "/home/$u/.ssh" install -m 0600 -o "$u" -g "$u" /root/.ssh/authorized_keys "/home/$u/.ssh/authorized_keys" + AUTH_FILE="/home/$u/.ssh/authorized_keys" + if [ -n "$(tail -c 1 "$AUTH_FILE")" ]; then + printf '\n' >> "$AUTH_FILE" + fi + for key in "${TEAM_KEYS[@]}"; do + if ! grep -qxF "$key" "$AUTH_FILE"; then + printf '%s\n' "$key" >> "$AUTH_FILE" + fi + done + chown "$u:$u" "$AUTH_FILE" done -echo "==> Writing /etc/environment" +# --- 5. /workspace owned by app --------------------------------------------- +log "/workspace owned by app" +mkdir -p /workspace +chown app:app /workspace + +# --- 6. GitHub CLI (gh) ----------------------------------------------------- +if ! command -v gh >/dev/null 2>&1; then + log "installing gh (GitHub CLI)" + mkdir -p -m 755 /etc/apt/keyrings + out=$(mktemp) + wget -nv -O "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg + cat "$out" > /etc/apt/keyrings/githubcli-archive-keyring.gpg + chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg + mkdir -p -m 755 /etc/apt/sources.list.d + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list + apt-get update -y + apt-get install "${APT_OPTS[@]}" gh +fi + +# --- 7. Rust toolchain for app (1.94.0 default + nightly-2026-02-01 + src) --- +log "Rust 1.94.0 + nightly-2026-02-01 (rust-src) for app" +sudo -u app -H bash -se <<'APP_RUST' +set -euo pipefail +if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain 1.94.0 --profile default +fi +export PATH="$HOME/.cargo/bin:$PATH" +grep -q 'cargo/env' "$HOME/.bashrc" 2>/dev/null \ + || echo '. "$HOME/.cargo/env"' >> "$HOME/.bashrc" +rustup toolchain install nightly-2026-02-01 --profile minimal --component rust-src +rustup component add rust-analyzer +APP_RUST + +# --- 8. Claude Code for app ------------------------------------------------- +log "Claude Code for app" +sudo -u app -H bash -se <<'APP_CLAUDE' +set -euo pipefail +export PATH="$HOME/.local/bin:$PATH" +if ! command -v claude >/dev/null 2>&1; then + curl -fsSL https://claude.ai/install.sh | bash +fi +PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' +grep -qxF "$PATH_LINE" "$HOME/.bashrc" 2>/dev/null \ + || printf '%s\n' "$PATH_LINE" >> "$HOME/.bashrc" +APP_CLAUDE + +# --- 9. lambda-vm sysroot (rv64im) ------------------------------------------ +SYSROOT_DIR=/opt/lambda-vm-sysroot +SYSROOT_URL=https://lambda.alignedlayer.com/lambda-vm-sysroot-rv64im.tar.gz +if [ ! -d "$SYSROOT_DIR" ]; then + log "downloading sysroot to $SYSROOT_DIR" + curl -L "$SYSROOT_URL" -o /tmp/sysroot.tar.gz + mkdir -p /opt + tar -xzf /tmp/sysroot.tar.gz -C /opt + rm /tmp/sysroot.tar.gz +fi + +# --- 10. GitHub deploy key for app (from /tmp/lambda_vm_read_only_key) --------- +GH_SSH_KEY=/home/app/.ssh/lambda_vm_read_only +STAGED_KEY=/tmp/lambda_vm_read_only_key +if [ ! -f "$GH_SSH_KEY" ] && [ -s "$STAGED_KEY" ]; then + log "installing $STAGED_KEY -> $GH_SSH_KEY" + install -d -m 0700 -o app -g app /home/app/.ssh + install -m 0600 -o app -g app "$STAGED_KEY" "$GH_SSH_KEY" + rm -f "$STAGED_KEY" +fi + +if [ -f "$GH_SSH_KEY" ]; then + SSH_CONFIG=/home/app/.ssh/config + if ! grep -q '^Host github.com' "$SSH_CONFIG" 2>/dev/null; then + cat >> "$SSH_CONFIG" </dev/null 2>&1; then + ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> "$KNOWN_HOSTS" 2>/dev/null || true + log "added github.com to known_hosts" + fi +fi + +# --- 11. Clone lambda_vm (as app) ------------------------------------------- +REPO_DIR=/workspace/lambda_vm +REPO_URL=git@github.com:yetanotherco/lambda_vm.git +if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then + log "cloning lambda_vm to $REPO_DIR (as app)" + sudo -u app -H git clone "$REPO_URL" "$REPO_DIR" +fi + +# --- 12. ethrex test fixture ------------------------------------------------ +ETHREX_FILE=/workspace/lambda_vm/executor/tests/ethrex_hoodi.bin +ETHREX_URL=https://lambda.alignedlayer.com/ethrex_hoodi.bin +if [ -d /workspace/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then + log "downloading ethrex_hoodi.bin" + sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" +fi + +# --- 13. /etc/environment + locale ------------------------------------------ +log "writing /etc/environment" cat > /etc/environment <<'EOF' LANG=en_US.UTF-8 LC_ALL=C @@ -64,7 +186,8 @@ LC_CTYPE=en_US.UTF-8 EOF locale-gen en_US.UTF-8 -echo "==> Writing /etc/ssh/sshd_config.d/99-hardening.conf" +# --- 14. sshd hardening (last; reload won't drop existing session) ---------- +log "writing /etc/ssh/sshd_config.d/99-hardening.conf" cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' PermitRootLogin no PasswordAuthentication no @@ -81,9 +204,7 @@ PermitUserEnvironment no LogLevel VERBOSE EOF chmod 0644 /etc/ssh/sshd_config.d/99-hardening.conf - -echo "==> Validating sshd config and reloading" sshd -t systemctl reload ssh -echo "==> Done. From now on log in as admin@ (sudo) or app@ (no sudo). Root SSH is disabled." +log "Done. Log in as admin@ (sudo) or app@ (no sudo). Root SSH is disabled." diff --git a/infra/provision_server.sh b/infra/provision_server.sh new file mode 100755 index 00000000..6a9248ab --- /dev/null +++ b/infra/provision_server.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Run infra/provision.sh on a remote Scaleway baremetal server over SSH. +# Safe to run standalone after rent_baremetal.sh, or to re-provision an +# existing server. +# +# Usage: infra/provision_server.sh +# +# Env var overrides (all optional): +# SSH_USER default: root +# First-run servers accept root SSH; once provision.sh +# has hardened sshd, re-run as: SSH_USER=admin ... +# PROVISION_FILE default: /provision.sh +# GITHUB_SSH_KEY_FILE default: $HOME/.ssh/lambda_vm_read_only +# Local path to a GitHub deploy key for +# yetanotherco/lambda_vm. If the file exists it is +# scp'd to /tmp/lambda_vm_read_only_key on the server +# before provision.sh runs; provision.sh then moves +# it to /home/app/.ssh/lambda_vm_read_only. If the local +# file is missing, the lambda_vm clone is skipped. +# +# SSH wait is indefinite — Ctrl+C to abort. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +SSH_USER="${SSH_USER:-root}" +PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" +GITHUB_SSH_KEY_FILE="${GITHUB_SSH_KEY_FILE:-$HOME/.ssh/lambda_vm_read_only}" +REMOTE_KEY_PATH=/tmp/lambda_vm_read_only_key + +err() { echo -e "${RED}error:${NC} $*" >&2; } +info() { echo -e "${BOLD}$*${NC}"; } +ok() { echo -e "${GREEN}$*${NC}"; } + +if [ $# -lt 1 ] || [ -z "${1:-}" ]; then + err "missing " + echo "Usage: $0 " >&2 + exit 2 +fi +IP="$1" + +if [ ! -r "$PROVISION_FILE" ]; then + err "provision script not found or unreadable: $PROVISION_FILE" + exit 1 +fi +if ! command -v ssh >/dev/null 2>&1; then + err "ssh not found on PATH." + exit 1 +fi + +if [ ! -r "$GITHUB_SSH_KEY_FILE" ]; then + info "GITHUB_SSH_KEY_FILE not found at $GITHUB_SSH_KEY_FILE — provision.sh will skip the lambda_vm clone." +fi + +SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes) + +info "Waiting for sshd on $SSH_USER@$IP (indefinite — Ctrl+C to abort)..." +attempt=1 +while ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" true 2>/dev/null; do + if [ $((attempt % 6)) -eq 0 ]; then + echo -e " ${YELLOW}still waiting (attempt $attempt, ~$((attempt * 10))s elapsed)${NC}" + fi + attempt=$((attempt + 1)) + sleep 10 +done +ok "sshd reachable on $SSH_USER@$IP (attempt $attempt)" + +if [ -r "$GITHUB_SSH_KEY_FILE" ]; then + info "Copying $GITHUB_SSH_KEY_FILE to $SSH_USER@$IP:$REMOTE_KEY_PATH..." + scp "${SSH_OPTS[@]}" "$GITHUB_SSH_KEY_FILE" "$SSH_USER@$IP:$REMOTE_KEY_PATH" + ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" "chmod 600 $REMOTE_KEY_PATH" +fi + +if [ "$SSH_USER" = "root" ]; then + REMOTE_CMD="bash -s" +else + REMOTE_CMD="sudo bash -s" +fi + +info "Running $PROVISION_FILE on $SSH_USER@$IP..." +ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" "$REMOTE_CMD" < "$PROVISION_FILE" + +echo +ok "Provisioning complete." +echo " ssh admin@$IP # sudo" +echo " ssh app@$IP # no sudo" diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index c6168dae..c31e87fa 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -4,12 +4,17 @@ # Usage: infra/rent_baremetal.sh # # Env var overrides (all optional): -# SCW_ZONE default: fr-par-2 -# SCW_TYPE default: EM-I320E-NVME -# PROVISION_FILE default: /infra/provision.sh -# READY_TIMEOUT default: 1800 (seconds) +# SCW_ZONE default: fr-par-2 +# SCW_TYPE default: EM-I320E-NVME +# PROVISION_FILE default: /infra/provision.sh +# READY_TIMEOUT default: 1800 (seconds) +# GITHUB_SSH_KEY_FILE default: $HOME/.ssh/lambda_vm_read_only +# Local path to a GitHub deploy key for +# yetanotherco/lambda_vm. If the file exists, +# provision_server.sh scp's it to the server before +# provision.sh runs. If missing, the clone is skipped. # -# Requires: scw, jq, ssh. +# Requires: scw, jq, ssh, scp. # To stop hourly charges when done: scw baremetal server delete zone= set -euo pipefail @@ -168,33 +173,21 @@ fi PUBLIC_IP=$(echo "$GET_JSON" | jq -r '.ips[] | select(.version == "IPv4") | .address' | head -n1) -info "Waiting for sshd on $PUBLIC_IP..." -SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes) -for attempt in 1 2 3 4 5 6 7 8 9 10; do - if ssh "${SSH_OPTS[@]}" "root@$PUBLIC_IP" true 2>/dev/null; then - ok "ssh root@$PUBLIC_IP ready (attempt $attempt)" - break - fi - if [ "$attempt" -eq 10 ]; then - err "ssh root@$PUBLIC_IP did not accept connections after 10 attempts" - exit 1 - fi - sleep 10 -done - -info "Running $PROVISION_FILE on the server (this also disables root ssh at the end)..." -ssh "${SSH_OPTS[@]}" "root@$PUBLIC_IP" 'bash -s' < "$PROVISION_FILE" - echo -ok "Server ready and provisioned." +ok "Server ready." echo " id: $SERVER_ID" echo " name: $SERVER_NAME" echo " zone: $SCW_ZONE" echo " type: $SCW_TYPE (hourly offer $OFFER_ID)" echo " ip: $PUBLIC_IP" echo -echo " ssh admin@$PUBLIC_IP # sudo" -echo " ssh app@$PUBLIC_IP # no sudo" + +info "Handing off to provision_server.sh (Ctrl+C to skip and provision later)..." +PROVISION_FILE="$PROVISION_FILE" SSH_USER=root "$SCRIPT_DIR/provision_server.sh" "$PUBLIC_IP" + echo echo "To stop hourly charges:" echo " scw baremetal server delete $SERVER_ID zone=$SCW_ZONE" +echo +echo "To re-provision this server later (sshd is hardened, so admin + sudo):" +echo " SSH_USER=admin infra/provision_server.sh $PUBLIC_IP" From 59a7a73ad6b1ea342309bfb79ebb6b2bf1ad1601 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 15:41:56 -0300 Subject: [PATCH 12/22] disable update --- infra/provision.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/provision.sh b/infra/provision.sh index 10bd7001..1745f765 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -19,8 +19,8 @@ log() { printf '\n=== %s ===\n' "$*"; } log "apt update + upgrade" export DEBIAN_FRONTEND=noninteractive APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) -apt-get update -y -apt-get upgrade "${APT_OPTS[@]}" +#apt-get update -y +#apt-get upgrade "${APT_OPTS[@]}" # --- 2. apt packages --------------------------------------------------------- log "apt install base packages + clang/lld/llvm + xz-utils" From 507cebc883faac7dca1c34b916644586abc07388 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 16:02:11 -0300 Subject: [PATCH 13/22] enable apt-get update --- infra/provision.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/provision.sh b/infra/provision.sh index 1745f765..052c12ea 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -19,7 +19,7 @@ log() { printf '\n=== %s ===\n' "$*"; } log "apt update + upgrade" export DEBIAN_FRONTEND=noninteractive APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) -#apt-get update -y +apt-get update -y #apt-get upgrade "${APT_OPTS[@]}" # --- 2. apt packages --------------------------------------------------------- From 2827909dea927293118a6a07d6402e5a87a33d86 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 16:09:35 -0300 Subject: [PATCH 14/22] add readme remove known hosts --- infra/README.md | 111 ++++++++++++++++++++++++++++++++++++++++ infra/rent_baremetal.sh | 3 ++ 2 files changed, 114 insertions(+) create mode 100644 infra/README.md diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 00000000..dfa45075 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,111 @@ +# infra/ + +Scripts for renting and provisioning a Scaleway Elastic Metal server for +benchmark / feature-testing work. + +| Script | Runs on | Purpose | +|---|---|---| +| `rent_baremetal.sh ` | local | Creates the server (hourly billing, Debian, fr-par-2) and hands off to `provision_server.sh`. | +| `provision_server.sh ` | local | Waits for sshd, `scp`s the GitHub deploy key, runs `provision.sh` on the server over SSH. Re-runnable. | +| `provision.sh` | remote | Installs toolchain, creates `admin`/`app` users, clones `lambda_vm`, hardens sshd. | + +## Prerequisites + +- `scw` CLI configured with the **`vm`** profile active (the script refuses + to run on any other profile). Check with `scw config get active-profile` + or set with `export SCW_PROFILE=vm`. +- `jq`, `ssh`, `scp` on `$PATH`. +- Your SSH key registered in the Scaleway project so it gets installed on + the server: `scw iam ssh-key list project-id=`. +- A GitHub deploy key with read access to `yetanotherco/lambda_vm` at + `~/.ssh/lambda_vm_read_only`. Create one with: + + ```bash + ssh-keygen -t ed25519 -f ~/.ssh/lambda_vm_read_only \ + -C "lambda_vm read-only deploy key" -N "" + ``` + + Then paste `~/.ssh/lambda_vm_read_only.pub` into **GitHub → repo Settings + → Deploy keys → Add deploy key** (leave "Allow write access" unchecked). + +## Rent + provision a new server + +```bash +infra/rent_baremetal.sh test-1 +``` + +End to end in one command — creates the server, waits for both +`status=ready` and `install.status=completed`, then provisions it (apt +packages, `admin`/`app` users, Rust toolchain, gh CLI, Claude Code, +lambda-vm sysroot, repo clone, ssh hardening). + +Use a unique name (Scaleway rejects duplicates): + +```bash +infra/rent_baremetal.sh bench-$(date +%s) +``` + +After it finishes, log in as: + +```bash +ssh admin@ # passwordless sudo +ssh app@ # workload user, no sudo, has /workspace/lambda_vm cloned +``` + +Root SSH is disabled at the end of provisioning. + +## Re-provision an existing server + +If `provision.sh` failed partway or you want to re-apply changes, point +`provision_server.sh` at the IP directly. It's idempotent. + +```bash +# Before hardening (root still works): +infra/provision_server.sh + +# After hardening (root SSH is dead, use admin): +SSH_USER=admin infra/provision_server.sh +``` + +The wrapper switches to `sudo bash -s` automatically when `SSH_USER` isn't +root. + +### Stuck on "Waiting for sshd"? + +Scaleway recycles IPs across rentals. If you've SSH'd to that IP before, +your `~/.ssh/known_hosts` has the previous server's host key, the new +server returns a different one, and `ssh -o StrictHostKeyChecking=accept-new` +refuses the connection — so the retry loop runs forever. `rent_baremetal.sh` +handles this automatically; for a standalone `provision_server.sh` run, +clear the stale entry first: + +```bash +ssh-keygen -R +infra/provision_server.sh +``` + +## Tear down + +Hourly billing keeps ticking until you delete the server: + +```bash +scw baremetal server delete zone=fr-par-2 +``` + +`rent_baremetal.sh` prints this line with the right ID at the end of a +successful run. + +## Configuration + +Everything has a working default; override via env var only when needed. + +| Var | Default | Used by | +|---|---|---| +| `SCW_ZONE` | `fr-par-2` | `rent_baremetal.sh` | +| `SCW_TYPE` | `EM-I320E-NVME` | `rent_baremetal.sh` | +| `SCW_OS_ID` | Debian 12 UUID | `rent_baremetal.sh` | +| `SCW_PROJECT_ID` | team project UUID | `rent_baremetal.sh` | +| `READY_TIMEOUT` | `1800` (s) | `rent_baremetal.sh` | +| `PROVISION_FILE` | `infra/provision.sh` | both wrappers | +| `GITHUB_SSH_KEY_FILE` | `~/.ssh/lambda_vm_read_only` | `provision_server.sh` | +| `SSH_USER` | `root` | `provision_server.sh` | diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index c31e87fa..291a5509 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -182,6 +182,9 @@ echo " type: $SCW_TYPE (hourly offer $OFFER_ID)" echo " ip: $PUBLIC_IP" echo +info "Wiping any stale known_hosts entry for $PUBLIC_IP (Scaleway recycles IPs)..." +ssh-keygen -R "$PUBLIC_IP" >/dev/null 2>&1 || true + info "Handing off to provision_server.sh (Ctrl+C to skip and provision later)..." PROVISION_FILE="$PROVISION_FILE" SSH_USER=root "$SCRIPT_DIR/provision_server.sh" "$PUBLIC_IP" From 29a380d82124f2c6a2cf93da843270e9e06008ff Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 16:25:31 -0300 Subject: [PATCH 15/22] remove workspace dir --- infra/README.md | 2 +- infra/provision.sh | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/infra/README.md b/infra/README.md index dfa45075..f3f6a2c4 100644 --- a/infra/README.md +++ b/infra/README.md @@ -49,7 +49,7 @@ After it finishes, log in as: ```bash ssh admin@ # passwordless sudo -ssh app@ # workload user, no sudo, has /workspace/lambda_vm cloned +ssh app@ # workload user, no sudo, has ~/lambda_vm cloned ``` Root SSH is disabled at the end of provisioning. diff --git a/infra/provision.sh b/infra/provision.sh index 052c12ea..08bebcf5 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -66,11 +66,6 @@ for u in admin app; do chown "$u:$u" "$AUTH_FILE" done -# --- 5. /workspace owned by app --------------------------------------------- -log "/workspace owned by app" -mkdir -p /workspace -chown app:app /workspace - # --- 6. GitHub CLI (gh) ----------------------------------------------------- if ! command -v gh >/dev/null 2>&1; then log "installing gh (GitHub CLI)" @@ -160,7 +155,7 @@ EOF fi # --- 11. Clone lambda_vm (as app) ------------------------------------------- -REPO_DIR=/workspace/lambda_vm +REPO_DIR=/home/app/lambda_vm REPO_URL=git@github.com:yetanotherco/lambda_vm.git if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then log "cloning lambda_vm to $REPO_DIR (as app)" @@ -168,9 +163,9 @@ if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then fi # --- 12. ethrex test fixture ------------------------------------------------ -ETHREX_FILE=/workspace/lambda_vm/executor/tests/ethrex_hoodi.bin +ETHREX_FILE=/home/app/lambda_vm/executor/tests/ethrex_hoodi.bin ETHREX_URL=https://lambda.alignedlayer.com/ethrex_hoodi.bin -if [ -d /workspace/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then +if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then log "downloading ethrex_hoodi.bin" sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" fi From 315dd96143001f4ebb7259ecdc873f8f19ba6617 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 16:47:28 -0300 Subject: [PATCH 16/22] setup ufw --- infra/provision.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/infra/provision.sh b/infra/provision.sh index 08bebcf5..0829c18b 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -170,7 +170,15 @@ if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" fi -# --- 13. /etc/environment + locale ------------------------------------------ +# --- 13. ufw firewall (default deny in, allow out, only ssh in) ------------- +log "ufw: default deny in / allow out, allow ssh (22/tcp) only" +ufw --force reset >/dev/null +ufw default deny incoming +ufw default allow outgoing +ufw allow 22/tcp +ufw --force enable + +# --- 14. /etc/environment + locale ------------------------------------------ log "writing /etc/environment" cat > /etc/environment <<'EOF' LANG=en_US.UTF-8 @@ -181,7 +189,7 @@ LC_CTYPE=en_US.UTF-8 EOF locale-gen en_US.UTF-8 -# --- 14. sshd hardening (last; reload won't drop existing session) ---------- +# --- 15. sshd hardening (last; reload won't drop existing session) ---------- log "writing /etc/ssh/sshd_config.d/99-hardening.conf" cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' PermitRootLogin no From 9be432e4f025dc9f95a857f1b168416d29e1e0d7 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 16:56:37 -0300 Subject: [PATCH 17/22] enable upgrade --- infra/provision.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/infra/provision.sh b/infra/provision.sh index 0829c18b..f92878c4 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -19,8 +19,15 @@ log() { printf '\n=== %s ===\n' "$*"; } log "apt update + upgrade" export DEBIAN_FRONTEND=noninteractive APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) + +# Scaleway baremetal Debian ships grub-cloud-amd64; its postinst (fired as a +# trigger by initramfs-tools / shim-signed / kernel upgrades) runs grub-install +# against an ext2 root and fails ("will not proceed with blocklists"). The +# package isn't load-bearing on UEFI baremetal — purge it before any upgrade. +apt-get purge -y grub-cloud-amd64 2>/dev/null || true + apt-get update -y -#apt-get upgrade "${APT_OPTS[@]}" +apt-get upgrade "${APT_OPTS[@]}" # --- 2. apt packages --------------------------------------------------------- log "apt install base packages + clang/lld/llvm + xz-utils" From 4a027f30caf40ddb70b99aed764d0324e8569943 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 17:11:00 -0300 Subject: [PATCH 18/22] apply code review fixes --- infra/provision.sh | 32 ++++++++++++++------------------ infra/provision_server.sh | 10 ++++++---- infra/rent_baremetal.sh | 1 - 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/infra/provision.sh b/infra/provision.sh index f92878c4..bbb3f78c 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -3,12 +3,6 @@ # Invoked remotely from infra/provision_server.sh as: # ssh root@ bash -s < infra/provision.sh # -# Optional input (placed by provision_server.sh before SSH'ing in): -# /tmp/lambda_vm_read_only_key GitHub deploy key. If present it is installed -# to /home/app/.ssh/lambda_vm_read_only and used to -# clone yetanotherco/lambda_vm. If absent the -# clone is skipped. -# # Idempotent — safe to re-run. set -euo pipefail @@ -73,7 +67,7 @@ for u in admin app; do chown "$u:$u" "$AUTH_FILE" done -# --- 6. GitHub CLI (gh) ----------------------------------------------------- +# --- 5. GitHub CLI (gh) ----------------------------------------------------- if ! command -v gh >/dev/null 2>&1; then log "installing gh (GitHub CLI)" mkdir -p -m 755 /etc/apt/keyrings @@ -88,7 +82,7 @@ if ! command -v gh >/dev/null 2>&1; then apt-get install "${APT_OPTS[@]}" gh fi -# --- 7. Rust toolchain for app (1.94.0 default + nightly-2026-02-01 + src) --- +# --- 6. Rust toolchain for app (1.94.0 default + nightly-2026-02-01 + src) --- log "Rust 1.94.0 + nightly-2026-02-01 (rust-src) for app" sudo -u app -H bash -se <<'APP_RUST' set -euo pipefail @@ -103,7 +97,7 @@ rustup toolchain install nightly-2026-02-01 --profile minimal --component rust-s rustup component add rust-analyzer APP_RUST -# --- 8. Claude Code for app ------------------------------------------------- +# --- 7. Claude Code for app ------------------------------------------------- log "Claude Code for app" sudo -u app -H bash -se <<'APP_CLAUDE' set -euo pipefail @@ -116,7 +110,7 @@ grep -qxF "$PATH_LINE" "$HOME/.bashrc" 2>/dev/null \ || printf '%s\n' "$PATH_LINE" >> "$HOME/.bashrc" APP_CLAUDE -# --- 9. lambda-vm sysroot (rv64im) ------------------------------------------ +# --- 8. lambda-vm sysroot (rv64im) ------------------------------------------ SYSROOT_DIR=/opt/lambda-vm-sysroot SYSROOT_URL=https://lambda.alignedlayer.com/lambda-vm-sysroot-rv64im.tar.gz if [ ! -d "$SYSROOT_DIR" ]; then @@ -127,15 +121,17 @@ if [ ! -d "$SYSROOT_DIR" ]; then rm /tmp/sysroot.tar.gz fi -# --- 10. GitHub deploy key for app (from /tmp/lambda_vm_read_only_key) --------- +# --- 9. GitHub deploy key for app (from /tmp/keydir/lambda_vm_read_only_key) - GH_SSH_KEY=/home/app/.ssh/lambda_vm_read_only -STAGED_KEY=/tmp/lambda_vm_read_only_key +STAGED_DIR=/tmp/keydir +STAGED_KEY=$STAGED_DIR/lambda_vm_read_only_key if [ ! -f "$GH_SSH_KEY" ] && [ -s "$STAGED_KEY" ]; then log "installing $STAGED_KEY -> $GH_SSH_KEY" install -d -m 0700 -o app -g app /home/app/.ssh install -m 0600 -o app -g app "$STAGED_KEY" "$GH_SSH_KEY" - rm -f "$STAGED_KEY" fi +# Always clean up the staging area, even if the key was already installed. +rm -rf "$STAGED_DIR" if [ -f "$GH_SSH_KEY" ]; then SSH_CONFIG=/home/app/.ssh/config @@ -161,7 +157,7 @@ EOF fi fi -# --- 11. Clone lambda_vm (as app) ------------------------------------------- +# --- 10. Clone lambda_vm (as app) ------------------------------------------- REPO_DIR=/home/app/lambda_vm REPO_URL=git@github.com:yetanotherco/lambda_vm.git if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then @@ -169,7 +165,7 @@ if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then sudo -u app -H git clone "$REPO_URL" "$REPO_DIR" fi -# --- 12. ethrex test fixture ------------------------------------------------ +# --- 11. ethrex test fixture ------------------------------------------------ ETHREX_FILE=/home/app/lambda_vm/executor/tests/ethrex_hoodi.bin ETHREX_URL=https://lambda.alignedlayer.com/ethrex_hoodi.bin if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then @@ -177,7 +173,7 @@ if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" fi -# --- 13. ufw firewall (default deny in, allow out, only ssh in) ------------- +# --- 12. ufw firewall (default deny in, allow out, only ssh in) ------------- log "ufw: default deny in / allow out, allow ssh (22/tcp) only" ufw --force reset >/dev/null ufw default deny incoming @@ -185,7 +181,7 @@ ufw default allow outgoing ufw allow 22/tcp ufw --force enable -# --- 14. /etc/environment + locale ------------------------------------------ +# --- 13. /etc/environment + locale ------------------------------------------ log "writing /etc/environment" cat > /etc/environment <<'EOF' LANG=en_US.UTF-8 @@ -196,7 +192,7 @@ LC_CTYPE=en_US.UTF-8 EOF locale-gen en_US.UTF-8 -# --- 15. sshd hardening (last; reload won't drop existing session) ---------- +# --- 14. sshd hardening (last; reload won't drop existing session) ---------- log "writing /etc/ssh/sshd_config.d/99-hardening.conf" cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' PermitRootLogin no diff --git a/infra/provision_server.sh b/infra/provision_server.sh index 6a9248ab..5d1f9d7a 100755 --- a/infra/provision_server.sh +++ b/infra/provision_server.sh @@ -33,7 +33,8 @@ NC='\033[0m' SSH_USER="${SSH_USER:-root}" PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" GITHUB_SSH_KEY_FILE="${GITHUB_SSH_KEY_FILE:-$HOME/.ssh/lambda_vm_read_only}" -REMOTE_KEY_PATH=/tmp/lambda_vm_read_only_key +REMOTE_KEY_DIR=/tmp/keydir +REMOTE_KEY_PATH=$REMOTE_KEY_DIR/lambda_vm_read_only_key err() { echo -e "${RED}error:${NC} $*" >&2; } info() { echo -e "${BOLD}$*${NC}"; } @@ -73,9 +74,10 @@ done ok "sshd reachable on $SSH_USER@$IP (attempt $attempt)" if [ -r "$GITHUB_SSH_KEY_FILE" ]; then - info "Copying $GITHUB_SSH_KEY_FILE to $SSH_USER@$IP:$REMOTE_KEY_PATH..." - scp "${SSH_OPTS[@]}" "$GITHUB_SSH_KEY_FILE" "$SSH_USER@$IP:$REMOTE_KEY_PATH" - ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" "chmod 600 $REMOTE_KEY_PATH" + info "Staging $GITHUB_SSH_KEY_FILE -> $SSH_USER@$IP:$REMOTE_KEY_PATH (mode 0600 atomically)..." + ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" \ + "install -d -m 0700 $REMOTE_KEY_DIR && install -m 0600 /dev/stdin $REMOTE_KEY_PATH" \ + < "$GITHUB_SSH_KEY_FILE" fi if [ "$SSH_USER" = "root" ]; then diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 291a5509..17ae1d72 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -20,7 +20,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" RED='\033[0;31m' GREEN='\033[0;32m' From a72134a431754473060a8670c125d7ef304288a4 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Wed, 20 May 2026 17:42:56 -0300 Subject: [PATCH 19/22] update readme --- infra/README.md | 65 +++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/infra/README.md b/infra/README.md index f3f6a2c4..a75b5aff 100644 --- a/infra/README.md +++ b/infra/README.md @@ -3,30 +3,28 @@ Scripts for renting and provisioning a Scaleway Elastic Metal server for benchmark / feature-testing work. -| Script | Runs on | Purpose | -|---|---|---| -| `rent_baremetal.sh ` | local | Creates the server (hourly billing, Debian, fr-par-2) and hands off to `provision_server.sh`. | -| `provision_server.sh ` | local | Waits for sshd, `scp`s the GitHub deploy key, runs `provision.sh` on the server over SSH. Re-runnable. | -| `provision.sh` | remote | Installs toolchain, creates `admin`/`app` users, clones `lambda_vm`, hardens sshd. | +| Script | Runs on | Purpose | +|----------------------------|---------|--------------------------------------------------------------------------------------------------------| +| `rent_baremetal.sh ` | local | Creates the server (hourly billing, Debian, fr-par-2) and hands off to `provision_server.sh`. | +| `provision_server.sh ` | local | Waits for sshd, `scp`s the GitHub deploy key, runs `provision.sh` on the server over SSH. Re-runnable. | +| `provision.sh` | remote | Installs toolchain, creates `admin`/`app` users, clones `lambda_vm`, hardens sshd. | ## Prerequisites -- `scw` CLI configured with the **`vm`** profile active (the script refuses - to run on any other profile). Check with `scw config get active-profile` - or set with `export SCW_PROFILE=vm`. -- `jq`, `ssh`, `scp` on `$PATH`. -- Your SSH key registered in the Scaleway project so it gets installed on - the server: `scw iam ssh-key list project-id=`. -- A GitHub deploy key with read access to `yetanotherco/lambda_vm` at - `~/.ssh/lambda_vm_read_only`. Create one with: +1. Install `scw` and `jq`: + ```bash + brew install scw jq # macOS + ``` - ```bash - ssh-keygen -t ed25519 -f ~/.ssh/lambda_vm_read_only \ - -C "lambda_vm read-only deploy key" -N "" - ``` +2. Create the `vm` scw profile (script refuses any other profile name): + ```bash + scw init --profile vm + ``` - Then paste `~/.ssh/lambda_vm_read_only.pub` into **GitHub → repo Settings - → Deploy keys → Add deploy key** (leave "Allow write access" unchecked). +3. Create the GitHub deploy key and add the `.pub` to **GitHub repo → Settings → Deploy keys** (read-only): + ```bash + ssh-keygen -t ed25519 -f ~/.ssh/lambda_vm_read_only -N "" + ``` ## Rent + provision a new server @@ -42,7 +40,7 @@ lambda-vm sysroot, repo clone, ssh hardening). Use a unique name (Scaleway rejects duplicates): ```bash -infra/rent_baremetal.sh bench-$(date +%s) +infra/rent_baremetal.sh ``` After it finishes, log in as: @@ -56,7 +54,7 @@ Root SSH is disabled at the end of provisioning. ## Re-provision an existing server -If `provision.sh` failed partway or you want to re-apply changes, point +If `provision.sh` failed partway, or you want to re-apply changes, point `provision_server.sh` at the IP directly. It's idempotent. ```bash @@ -70,31 +68,6 @@ SSH_USER=admin infra/provision_server.sh The wrapper switches to `sudo bash -s` automatically when `SSH_USER` isn't root. -### Stuck on "Waiting for sshd"? - -Scaleway recycles IPs across rentals. If you've SSH'd to that IP before, -your `~/.ssh/known_hosts` has the previous server's host key, the new -server returns a different one, and `ssh -o StrictHostKeyChecking=accept-new` -refuses the connection — so the retry loop runs forever. `rent_baremetal.sh` -handles this automatically; for a standalone `provision_server.sh` run, -clear the stale entry first: - -```bash -ssh-keygen -R -infra/provision_server.sh -``` - -## Tear down - -Hourly billing keeps ticking until you delete the server: - -```bash -scw baremetal server delete zone=fr-par-2 -``` - -`rent_baremetal.sh` prints this line with the right ID at the end of a -successful run. - ## Configuration Everything has a working default; override via env var only when needed. From bfd9d2e655bea8090d36beb28e84b9bd461cf564 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Fri, 22 May 2026 13:56:55 -0300 Subject: [PATCH 20/22] remove ssh key for github --- infra/README.md | 8 +------ infra/provision.sh | 50 ++++++--------------------------------- infra/provision_server.sh | 21 ---------------- infra/rent_baremetal.sh | 7 +----- 4 files changed, 9 insertions(+), 77 deletions(-) diff --git a/infra/README.md b/infra/README.md index a75b5aff..0f2581ba 100644 --- a/infra/README.md +++ b/infra/README.md @@ -6,7 +6,7 @@ benchmark / feature-testing work. | Script | Runs on | Purpose | |----------------------------|---------|--------------------------------------------------------------------------------------------------------| | `rent_baremetal.sh ` | local | Creates the server (hourly billing, Debian, fr-par-2) and hands off to `provision_server.sh`. | -| `provision_server.sh ` | local | Waits for sshd, `scp`s the GitHub deploy key, runs `provision.sh` on the server over SSH. Re-runnable. | +| `provision_server.sh ` | local | Waits for sshd, runs `provision.sh` on the server over SSH. Re-runnable. | | `provision.sh` | remote | Installs toolchain, creates `admin`/`app` users, clones `lambda_vm`, hardens sshd. | ## Prerequisites @@ -21,11 +21,6 @@ benchmark / feature-testing work. scw init --profile vm ``` -3. Create the GitHub deploy key and add the `.pub` to **GitHub repo → Settings → Deploy keys** (read-only): - ```bash - ssh-keygen -t ed25519 -f ~/.ssh/lambda_vm_read_only -N "" - ``` - ## Rent + provision a new server ```bash @@ -80,5 +75,4 @@ Everything has a working default; override via env var only when needed. | `SCW_PROJECT_ID` | team project UUID | `rent_baremetal.sh` | | `READY_TIMEOUT` | `1800` (s) | `rent_baremetal.sh` | | `PROVISION_FILE` | `infra/provision.sh` | both wrappers | -| `GITHUB_SSH_KEY_FILE` | `~/.ssh/lambda_vm_read_only` | `provision_server.sh` | | `SSH_USER` | `root` | `provision_server.sh` | diff --git a/infra/provision.sh b/infra/provision.sh index bbb3f78c..74b02f96 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -121,51 +121,15 @@ if [ ! -d "$SYSROOT_DIR" ]; then rm /tmp/sysroot.tar.gz fi -# --- 9. GitHub deploy key for app (from /tmp/keydir/lambda_vm_read_only_key) - -GH_SSH_KEY=/home/app/.ssh/lambda_vm_read_only -STAGED_DIR=/tmp/keydir -STAGED_KEY=$STAGED_DIR/lambda_vm_read_only_key -if [ ! -f "$GH_SSH_KEY" ] && [ -s "$STAGED_KEY" ]; then - log "installing $STAGED_KEY -> $GH_SSH_KEY" - install -d -m 0700 -o app -g app /home/app/.ssh - install -m 0600 -o app -g app "$STAGED_KEY" "$GH_SSH_KEY" -fi -# Always clean up the staging area, even if the key was already installed. -rm -rf "$STAGED_DIR" - -if [ -f "$GH_SSH_KEY" ]; then - SSH_CONFIG=/home/app/.ssh/config - if ! grep -q '^Host github.com' "$SSH_CONFIG" 2>/dev/null; then - cat >> "$SSH_CONFIG" </dev/null 2>&1; then - ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> "$KNOWN_HOSTS" 2>/dev/null || true - log "added github.com to known_hosts" - fi -fi - -# --- 10. Clone lambda_vm (as app) ------------------------------------------- +# --- 9. Clone lambda_vm (as app, public repo over HTTPS) --------------------- REPO_DIR=/home/app/lambda_vm -REPO_URL=git@github.com:yetanotherco/lambda_vm.git -if [ ! -d "$REPO_DIR/.git" ] && [ -f "$GH_SSH_KEY" ]; then +REPO_URL=https://github.com/yetanotherco/lambda_vm.git +if [ ! -d "$REPO_DIR/.git" ]; then log "cloning lambda_vm to $REPO_DIR (as app)" sudo -u app -H git clone "$REPO_URL" "$REPO_DIR" fi -# --- 11. ethrex test fixture ------------------------------------------------ +# --- 10. ethrex test fixture ------------------------------------------------ ETHREX_FILE=/home/app/lambda_vm/executor/tests/ethrex_hoodi.bin ETHREX_URL=https://lambda.alignedlayer.com/ethrex_hoodi.bin if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then @@ -173,7 +137,7 @@ if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" fi -# --- 12. ufw firewall (default deny in, allow out, only ssh in) ------------- +# --- 11. ufw firewall (default deny in, allow out, only ssh in) ------------- log "ufw: default deny in / allow out, allow ssh (22/tcp) only" ufw --force reset >/dev/null ufw default deny incoming @@ -181,7 +145,7 @@ ufw default allow outgoing ufw allow 22/tcp ufw --force enable -# --- 13. /etc/environment + locale ------------------------------------------ +# --- 12. /etc/environment + locale ------------------------------------------ log "writing /etc/environment" cat > /etc/environment <<'EOF' LANG=en_US.UTF-8 @@ -192,7 +156,7 @@ LC_CTYPE=en_US.UTF-8 EOF locale-gen en_US.UTF-8 -# --- 14. sshd hardening (last; reload won't drop existing session) ---------- +# --- 13. sshd hardening (last; reload won't drop existing session) ---------- log "writing /etc/ssh/sshd_config.d/99-hardening.conf" cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' PermitRootLogin no diff --git a/infra/provision_server.sh b/infra/provision_server.sh index 5d1f9d7a..110455c3 100755 --- a/infra/provision_server.sh +++ b/infra/provision_server.sh @@ -10,13 +10,6 @@ # First-run servers accept root SSH; once provision.sh # has hardened sshd, re-run as: SSH_USER=admin ... # PROVISION_FILE default: /provision.sh -# GITHUB_SSH_KEY_FILE default: $HOME/.ssh/lambda_vm_read_only -# Local path to a GitHub deploy key for -# yetanotherco/lambda_vm. If the file exists it is -# scp'd to /tmp/lambda_vm_read_only_key on the server -# before provision.sh runs; provision.sh then moves -# it to /home/app/.ssh/lambda_vm_read_only. If the local -# file is missing, the lambda_vm clone is skipped. # # SSH wait is indefinite — Ctrl+C to abort. @@ -32,9 +25,6 @@ NC='\033[0m' SSH_USER="${SSH_USER:-root}" PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" -GITHUB_SSH_KEY_FILE="${GITHUB_SSH_KEY_FILE:-$HOME/.ssh/lambda_vm_read_only}" -REMOTE_KEY_DIR=/tmp/keydir -REMOTE_KEY_PATH=$REMOTE_KEY_DIR/lambda_vm_read_only_key err() { echo -e "${RED}error:${NC} $*" >&2; } info() { echo -e "${BOLD}$*${NC}"; } @@ -56,10 +46,6 @@ if ! command -v ssh >/dev/null 2>&1; then exit 1 fi -if [ ! -r "$GITHUB_SSH_KEY_FILE" ]; then - info "GITHUB_SSH_KEY_FILE not found at $GITHUB_SSH_KEY_FILE — provision.sh will skip the lambda_vm clone." -fi - SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes) info "Waiting for sshd on $SSH_USER@$IP (indefinite — Ctrl+C to abort)..." @@ -73,13 +59,6 @@ while ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" true 2>/dev/null; do done ok "sshd reachable on $SSH_USER@$IP (attempt $attempt)" -if [ -r "$GITHUB_SSH_KEY_FILE" ]; then - info "Staging $GITHUB_SSH_KEY_FILE -> $SSH_USER@$IP:$REMOTE_KEY_PATH (mode 0600 atomically)..." - ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" \ - "install -d -m 0700 $REMOTE_KEY_DIR && install -m 0600 /dev/stdin $REMOTE_KEY_PATH" \ - < "$GITHUB_SSH_KEY_FILE" -fi - if [ "$SSH_USER" = "root" ]; then REMOTE_CMD="bash -s" else diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh index 17ae1d72..858318a4 100755 --- a/infra/rent_baremetal.sh +++ b/infra/rent_baremetal.sh @@ -8,13 +8,8 @@ # SCW_TYPE default: EM-I320E-NVME # PROVISION_FILE default: /infra/provision.sh # READY_TIMEOUT default: 1800 (seconds) -# GITHUB_SSH_KEY_FILE default: $HOME/.ssh/lambda_vm_read_only -# Local path to a GitHub deploy key for -# yetanotherco/lambda_vm. If the file exists, -# provision_server.sh scp's it to the server before -# provision.sh runs. If missing, the clone is skipped. # -# Requires: scw, jq, ssh, scp. +# Requires: scw, jq, ssh. # To stop hourly charges when done: scw baremetal server delete zone= set -euo pipefail From cfaf0710de6480aa810d0691c65f254a1205a752 Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Fri, 22 May 2026 13:58:33 -0300 Subject: [PATCH 21/22] add mauro ssh key --- infra/provision.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/provision.sh b/infra/provision.sh index 74b02f96..082e9617 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -50,6 +50,7 @@ TEAM_KEYS=( "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFzvQKhE/xqRxHbit/dZNej7T5eVLmF8CAGL7to6o3QY joaquin@mail.com" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2GAeixuqP4XwujuSK9KDgdmyglGzlQQsXztnve+bra gabriel@mail.com" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQnPPUb4gzmsmjDP98mNKXbpHrp9bIIL7QiRjyWEG6f julian@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzniAUYGJXguBjfz2+uGUUC7XLVmk58FhCsEBMx2r5k mauro@mail.com" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6mrcWIyU+/LrNZLivNIOYr6ld/CXefoq1hyXLsHDfV it" ) for u in admin app; do From 72945067c852a301639eb0262dabf438fe4bf98b Mon Sep 17 00:00:00 2001 From: Julian Arce <52429267+JuArce@users.noreply.github.com> Date: Fri, 22 May 2026 20:12:58 -0300 Subject: [PATCH 22/22] update README add jq to provision.sh --- infra/README.md | 37 ++++++++++++++++++++++++++++--------- infra/provision.sh | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/infra/README.md b/infra/README.md index 0f2581ba..5d000b26 100644 --- a/infra/README.md +++ b/infra/README.md @@ -67,12 +67,31 @@ root. Everything has a working default; override via env var only when needed. -| Var | Default | Used by | -|---|---|---| -| `SCW_ZONE` | `fr-par-2` | `rent_baremetal.sh` | -| `SCW_TYPE` | `EM-I320E-NVME` | `rent_baremetal.sh` | -| `SCW_OS_ID` | Debian 12 UUID | `rent_baremetal.sh` | -| `SCW_PROJECT_ID` | team project UUID | `rent_baremetal.sh` | -| `READY_TIMEOUT` | `1800` (s) | `rent_baremetal.sh` | -| `PROVISION_FILE` | `infra/provision.sh` | both wrappers | -| `SSH_USER` | `root` | `provision_server.sh` | +| Var | Default | Used by | Notes | +|---|---|---|---| +| `SCW_TYPE` | `EM-I320E-NVME` | `rent_baremetal.sh` | Scaleway commercial type. Must have an `hourly` offer in `$SCW_ZONE` or the script refuses. | +| `SCW_ZONE` | `fr-par-2` | `rent_baremetal.sh` | One of `fr-par-1`, `fr-par-2`, `nl-ams-1`, `nl-ams-2`, `pl-waw-2`, `pl-waw-3`. | +| `SCW_OS_ID` | `83640d93-...` (Debian 12) | `rent_baremetal.sh` | Must have `cloud_init_supported: true`. | +| `SCW_PROJECT_ID` | `946cfb34-...` (lambda_vm) | `rent_baremetal.sh` | Determines which scw IAM SSH keys get installed. | +| `READY_TIMEOUT` | `1800` (s) | `rent_baremetal.sh` | How long to wait for `status=ready && install.status=completed`. | +| `PROVISION_FILE` | `/provision.sh` | both wrappers | Path to the remote provisioning script. | +| `SSH_USER` | `root` | `provision_server.sh` | Switch to `admin` for re-runs after sshd hardening. | + +### `SCW_TYPE` options + +| Type | CPU | RAM | Disk | Price (€/h) | +|---|---|---|---|---| +| `EM-I220E-NVME` | AMD EPYC 8124P (16c/32t @ 2.5 GHz) | 128 GB | 2× 960 GB NVMe | 0.548 | +| `EM-I320E-NVME` | AMD EPYC 8224P (24c/48t @ 2.5 GHz) | 192 GB | 2× 1.92 TB NVMe | 0.822 (default) | +| `EM-I420E-NVME` | AMD EPYC 8324P (32c/64t @ 2.6 GHz) | 256 GB | 2× 1.92 TB NVMe | 1.096 | + +### `SCW_ZONE` options + +| Zone | Location | +|---|---| +| `fr-par-1` | Paris, France | +| `fr-par-2` | Paris, France (default) | +| `nl-ams-1` | Amsterdam, Netherlands | +| `nl-ams-2` | Amsterdam, Netherlands | +| `pl-waw-2` | Warsaw, Poland | +| `pl-waw-3` | Warsaw, Poland | diff --git a/infra/provision.sh b/infra/provision.sh index 082e9617..405fd38d 100755 --- a/infra/provision.sh +++ b/infra/provision.sh @@ -26,7 +26,7 @@ apt-get upgrade "${APT_OPTS[@]}" # --- 2. apt packages --------------------------------------------------------- log "apt install base packages + clang/lld/llvm + xz-utils" apt-get install "${APT_OPTS[@]}" \ - ca-certificates curl wget gnupg vim git zip unzip openssl libssl-dev \ + ca-certificates curl wget gnupg vim git zip unzip openssl libssl-dev jq \ build-essential rsyslog htop rsync pkg-config locales ufw \ clang lld llvm xz-utils