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:
- Non-interactive system upgrades (
DEBIAN_FRONTEND=noninteractive). - Generating a strong, randomized root password at runtime without hardcoded secrets.
- Installing base toolchains and custom infrastructure monitoring agents.
- Temporarily enabling SSH password authentication for root to allow public key transfer (
ssh-copy-id). - Pausing for operator validation, offering continue, abort, and retry branches.
- Reverting SSH to a hardened state (disabling password authentication and restoring modular includes).
- Injecting top-of-chain firewall rules via
iptableswith 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:
- Comment out the
Includedirective and enablePermitRootLogin yes. - Restart the
sshservice. - After confirming authorized keys exist in
/root/.ssh/authorized_keys, restore the original configuration and re-enableInclude.
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:
- Control the terminal descriptor: In
curl | bashpipelines, standardstdinreads from the network stream. Always use< /dev/ttywhen interactive confirmation is required. - Avoid hardcoded passwords:
/dev/urandomprovides immediate entropy without risk of deadlock during early VM initialization. - Respect Netfilter evaluation precedence: Insert rules at position 1 (
-I INPUT 1) to ensure accept directives precede broad DROP or REJECT rules. - Clean up orphan processes: If background agents fail to exit cleanly post-setup, review our guide on how to terminate complex process trees using pkill.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments