Idempotent Ubuntu 24.04 bootstrapping via cURL | Bash: stdin /dev/tty, ephemeral credentials, and modular SSH
Back to blog

Idempotent Ubuntu 24.04 bootstrapping via cURL | Bash: stdin /dev/tty, ephemeral credentials, and modular SSH

9/21/2026 · 2 min · DevOps

Idempotent Ubuntu 24.04 bootstrapping via cURL | Bash: stdin /dev/tty, ephemeral credentials, and modular SSH#

Automating initial cloud instance configuration requires more than chaining shell commands together: it demands understanding how interpreters handle file descriptors when executed via pipes (curl | bash), avoiding entropy pool stalls, and managing Ubuntu 24.04 LTS (Noble Numbat) modular SSH configurations.

Here is the design and implementation of a resilient remote provisioning script, detailing standard I/O edge cases and safe credential workflows.


1. Remote provisioning requirements#

The goal was hosting a remote script executable directly on fresh instances via:

curl -fsSL https://domain.com.br/bootstrap.sh | bash

Key requirements:

  1. Non-interactive system upgrades (DEBIAN_FRONTEND=noninteractive).
  2. Generating a strong, randomized root password at runtime without hardcoded secrets.
  3. Installing base toolchains and custom infrastructure monitoring agents.
  4. Temporarily enabling SSH password authentication for root to allow public key transfer (ssh-copy-id).
  5. Pausing for operator validation, offering continue, abort, and retry branches.
  6. Reverting SSH to a hardened state (disabling password authentication and restoring modular includes).
  7. Injecting top-of-chain firewall rules via iptables with automated persistence.

2. Technical obstacles and solutions#

A. The stdin conflict in curl | bash#

When executing scripts through pipes (curl ... | bash), the shell standard input stdin (file descriptor 0) connects to the output stream of curl. A standard read command attempts to consume subsequent script lines from the pipe instead of waiting for keyboard input.

The Solution: Direct the read builtin explicitly to the controlling terminal device (/dev/tty):

read -p "Option: " opt < /dev/tty

This guarantees execution pauses for operator input regardless of how the script was piped into bash.

B. Non-blocking random passwords (/dev/urandom)#

On freshly provisioned VMs, the kernel entropy pool has not yet accumulated sufficient hardware noise. Reading from /dev/random can block execution indefinitely until adequate I/O interrupts occur.

The reliable, non-blocking approach is reading from /dev/urandom and filtering with base64 and tr:

NEW_ROOT_PASSWORD=$(head -c 12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 16)

C. Modular SSH architecture in Ubuntu 24.04#

Ubuntu 24.04 utilizes drop-in includes at /etc/ssh/sshd_config.d/*.conf (such as 50-cloud-init.conf), which override global settings in /etc/ssh/sshd_config.

To ensure temporary password authentication works during bootstrapping:

  1. Comment out the Include directive and enable PermitRootLogin yes.
  2. Restart the ssh service.
  3. After confirming authorized keys exist in /root/.ssh/authorized_keys, restore the original configuration and re-enable Include.

3. The complete bootstrapping script#

#!/bin/bash
set -eo pipefail

# --- 1. Ephemeral Credentials and Constants ---
NEW_ROOT_PASSWORD=$(head -c 12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 16)
AGENT_URL="https://domain.com.br/downloads/agent-service_1.0-1_all.deb"
AGENT_DEB="agent-service_1.0-1_all.deb"

# Privilege validation
if [[ $EUID -ne 0 ]]; then
   echo "ERROR: This script must be run as root." 
   exit 1
fi

echo "[1/8] Setting ephemeral root password and updating packages..."
echo "root:$NEW_ROOT_PASSWORD" | chpasswd

export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get upgrade -y
apt-get install -y vim wget iptables-persistent

echo "[2/8] Installing infrastructure agent..."
if wget -q -O "$AGENT_DEB" "$AGENT_URL"; then
    apt-get install -y ./"$AGENT_DEB" || dpkg -i "$AGENT_DEB"
    rm -f "$AGENT_DEB"
fi

echo "[3/8] Detecting public IP address..."
PUBLIC_IP=$(curl -s --connect-timeout 5 ifconfig.me || hostname -I | awk '{print $1}')

echo "[4/8] Temporarily adjusting SSH for key deployment..."
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sed -i 's/^Include \/etc\/ssh\/sshd_config.d\/\*.conf/#&/' /etc/ssh/sshd_config
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart ssh

echo "[5/8] Awaiting authorized key deployment..."
while true; do
    echo "========================================================"
    echo "PUBLIC IP:          $PUBLIC_IP"
    echo "EPHEMERAL PASSWORD: $NEW_ROOT_PASSWORD"
    echo "Deploy your key:    ssh-copy-id root@$PUBLIC_IP"
    echo "Options: [c]ontinue | [a]bort | [r]eset"
    echo "========================================================"
    
    # Read directly from controlling terminal
    read -p "Select action: " opt < /dev/tty

    case $opt in
        [Cc]* )
            if [ -s /root/.ssh/authorized_keys ]; then
                echo "✓ SSH key successfully detected in authorized_keys."
                break
            else
                echo "WARNING: /root/.ssh/authorized_keys is empty or missing."
                echo "Deploy your public key before continuing."
            fi
            ;;
        [Aa]* )
            echo "Aborting. Restoring SSH security defaults..."
            mv /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
            systemctl restart ssh
            exit 1
            ;;
        [Rr]* )
            echo "Resetting .ssh directory..."
            rm -rf /root/.ssh && mkdir -p /root/.ssh && chmod 700 /root/.ssh
            ;;
        * ) 
            echo "Invalid option. Type 'c', 'a', or 'r'."
            ;;
    esac
done

echo "[6/8] Hardening SSH: disabling password authentication..."
mv /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
systemctl restart ssh

echo "[7/8] Applying firewall rules..."
# Insert at top of chain to take precedence over DROP rules
iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT
netfilter-persistent save

echo "[8/8] Provisioning completed successfully."

Practical automation takeaways#

When designing remote shell provisioning pipelines:

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments