feat: Created post-install script
This commit is contained in:
+22
-2
@@ -2,6 +2,20 @@
|
|||||||
# (one per server in docs/architecture.md). Each host module owns its own
|
# (one per server in docs/architecture.md). Each host module owns its own
|
||||||
# firewall (and volume, where applicable) - see modules/<host>/main.tf.
|
# firewall (and volume, where applicable) - see modules/<host>/main.tf.
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# SSH to dev/prod is only permitted from the vpn server's public IP: admins must
|
||||||
|
# tunnel through the VPN first, whose egress traffic is then sourced from this IP.
|
||||||
|
# vpn itself can't require tunneling through itself, so it keeps
|
||||||
|
# var.allowed_ssh_source_ips for direct SSH access.
|
||||||
|
vpn_ssh_source_ips = ["${module.vpn.ipv4}/32"]
|
||||||
|
|
||||||
|
# Rendered once and uploaded+run by every host module immediately after its
|
||||||
|
# server is created - see scripts/bootstrap.sh.tftpl.
|
||||||
|
bootstrap_script = templatefile("${path.module}/scripts/bootstrap.sh.tftpl", {
|
||||||
|
deploy_user = var.deploy_user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module "network" {
|
module "network" {
|
||||||
source = "./modules/network"
|
source = "./modules/network"
|
||||||
|
|
||||||
@@ -20,8 +34,10 @@ module "dev" {
|
|||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.dev_private_ip
|
private_ip = var.dev_private_ip
|
||||||
volume_size = var.dev_volume_size
|
volume_size = var.dev_volume_size
|
||||||
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
allowed_ssh_source_ips = local.vpn_ssh_source_ips
|
||||||
network_ip_range = var.network_ip_range
|
network_ip_range = var.network_ip_range
|
||||||
|
bootstrap_script = local.bootstrap_script
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
# module.network.id alone doesn't guarantee the subnet exists yet, and a server
|
# module.network.id alone doesn't guarantee the subnet exists yet, and a server
|
||||||
# can't join a network before it has a subnet.
|
# can't join a network before it has a subnet.
|
||||||
@@ -38,8 +54,10 @@ module "prod" {
|
|||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.prod_private_ip
|
private_ip = var.prod_private_ip
|
||||||
volume_size = var.prod_volume_size
|
volume_size = var.prod_volume_size
|
||||||
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
allowed_ssh_source_ips = local.vpn_ssh_source_ips
|
||||||
network_ip_range = var.network_ip_range
|
network_ip_range = var.network_ip_range
|
||||||
|
bootstrap_script = local.bootstrap_script
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
depends_on = [module.network]
|
depends_on = [module.network]
|
||||||
}
|
}
|
||||||
@@ -52,4 +70,6 @@ module "vpn" {
|
|||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_id = data.hcloud_ssh_key.main.id
|
ssh_key_id = data.hcloud_ssh_key.main.id
|
||||||
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
||||||
|
bootstrap_script = local.bootstrap_script
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
resource "hcloud_firewall" "this" {
|
resource "hcloud_firewall" "this" {
|
||||||
name = "dev-firewall"
|
name = "dev-firewall"
|
||||||
|
|
||||||
rule { # server ssh
|
rule { # server ssh - wired to the vpn server's public IP only, see infra/main.tf
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
@@ -61,6 +61,25 @@ resource "hcloud_server" "this" {
|
|||||||
network_id = var.network_id
|
network_id = var.network_id
|
||||||
ip = var.private_ip
|
ip = var.private_ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = self.ipv4_address
|
||||||
|
user = "root"
|
||||||
|
private_key = file(var.ssh_private_key_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "file" {
|
||||||
|
content = var.bootstrap_script
|
||||||
|
destination = "/root/bootstrap.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"chmod +x /root/bootstrap.sh",
|
||||||
|
"/root/bootstrap.sh",
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "hcloud_volume" "storage" {
|
resource "hcloud_volume" "storage" {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ variable "volume_size" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_ssh_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on dev."
|
description = "CIDRs allowed to reach port 22 on dev. Set by the root module to the vpn server's public IP."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,3 +42,13 @@ variable "network_ip_range" {
|
|||||||
description = "CIDR of the private network, allowed through the firewall for traffic from prod."
|
description = "CIDR of the private network, allowed through the firewall for traffic from prod."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "bootstrap_script" {
|
||||||
|
description = "Rendered post-install script, uploaded and executed on the server immediately after creation."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
resource "hcloud_firewall" "this" {
|
resource "hcloud_firewall" "this" {
|
||||||
name = "prod-firewall"
|
name = "prod-firewall"
|
||||||
|
|
||||||
rule { # server ssh
|
rule { # server ssh - wired to the vpn server's public IP only, see infra/main.tf
|
||||||
direction = "in"
|
direction = "in"
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
port = "22"
|
port = "22"
|
||||||
@@ -88,6 +88,25 @@ resource "hcloud_server" "this" {
|
|||||||
network_id = var.network_id
|
network_id = var.network_id
|
||||||
ip = var.private_ip
|
ip = var.private_ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = self.ipv4_address
|
||||||
|
user = "root"
|
||||||
|
private_key = file(var.ssh_private_key_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "file" {
|
||||||
|
content = var.bootstrap_script
|
||||||
|
destination = "/root/bootstrap.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"chmod +x /root/bootstrap.sh",
|
||||||
|
"/root/bootstrap.sh",
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "hcloud_volume" "storage" {
|
resource "hcloud_volume" "storage" {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ variable "volume_size" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_ssh_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on prod."
|
description = "CIDRs allowed to reach port 22 on prod. Set by the root module to the vpn server's public IP."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,3 +42,13 @@ variable "network_ip_range" {
|
|||||||
description = "CIDR of the private network, allowed through the firewall for traffic from dev."
|
description = "CIDR of the private network, allowed through the firewall for traffic from dev."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "bootstrap_script" {
|
||||||
|
description = "Rendered post-install script, uploaded and executed on the server immediately after creation."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,4 +38,23 @@ resource "hcloud_server" "this" {
|
|||||||
location = var.location
|
location = var.location
|
||||||
ssh_keys = [var.ssh_key_id]
|
ssh_keys = [var.ssh_key_id]
|
||||||
firewall_ids = [hcloud_firewall.this.id]
|
firewall_ids = [hcloud_firewall.this.id]
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = self.ipv4_address
|
||||||
|
user = "root"
|
||||||
|
private_key = file(var.ssh_private_key_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "file" {
|
||||||
|
content = var.bootstrap_script
|
||||||
|
destination = "/root/bootstrap.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"chmod +x /root/bootstrap.sh",
|
||||||
|
"/root/bootstrap.sh",
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,3 +22,13 @@ variable "allowed_ssh_source_ips" {
|
|||||||
description = "CIDRs allowed to reach port 22 on vpn."
|
description = "CIDRs allowed to reach port 22 on vpn."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "bootstrap_script" {
|
||||||
|
description = "Rendered post-install script, uploaded and executed on the server immediately after creation."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Post-install bootstrap - uploaded and executed once by OpenTofu immediately
|
||||||
|
# after server creation (see modules/<host>/main.tf). Idempotent-ish: safe to
|
||||||
|
# re-run by hand, but only ever runs automatically on first create.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
DEPLOY_USER="${deploy_user}"
|
||||||
|
|
||||||
|
apt-get update -y
|
||||||
|
apt-get upgrade -y
|
||||||
|
|
||||||
|
# --- Docker Engine + Compose plugin ---
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
apt-get install -y ca-certificates curl gnupg
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
. /etc/os-release
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Non-root sudo user, given the same key root was provisioned with ---
|
||||||
|
if ! id -u "$DEPLOY_USER" >/dev/null 2>&1; then
|
||||||
|
useradd --create-home --shell /bin/bash "$DEPLOY_USER"
|
||||||
|
usermod -aG sudo,docker "$DEPLOY_USER"
|
||||||
|
mkdir -p "/home/$DEPLOY_USER/.ssh"
|
||||||
|
cp /root/.ssh/authorized_keys "/home/$DEPLOY_USER/.ssh/authorized_keys"
|
||||||
|
chown -R "$DEPLOY_USER:$DEPLOY_USER" "/home/$DEPLOY_USER/.ssh"
|
||||||
|
chmod 700 "/home/$DEPLOY_USER/.ssh"
|
||||||
|
chmod 600 "/home/$DEPLOY_USER/.ssh/authorized_keys"
|
||||||
|
echo "$DEPLOY_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$DEPLOY_USER"
|
||||||
|
chmod 440 "/etc/sudoers.d/90-$DEPLOY_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH hardening: key-only auth everywhere. Root keeps key-only login as a
|
||||||
|
# fallback rather than being fully locked out, in case $DEPLOY_USER setup above
|
||||||
|
# ever fails silently on a future image. ---
|
||||||
|
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
|
||||||
|
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
||||||
|
systemctl reload ssh 2>/dev/null || systemctl reload sshd
|
||||||
|
|
||||||
|
# --- Unattended security upgrades ---
|
||||||
|
apt-get install -y unattended-upgrades
|
||||||
|
dpkg-reconfigure -f noninteractive unattended-upgrades
|
||||||
|
systemctl enable --now unattended-upgrades
|
||||||
@@ -8,11 +8,17 @@
|
|||||||
# (Console > Security > SSH Keys). Required.
|
# (Console > Security > SSH Keys). Required.
|
||||||
ssh_key_name = "your-key-name"
|
ssh_key_name = "your-key-name"
|
||||||
|
|
||||||
|
# Local path to the private key matching ssh_key_name above. OpenTofu uses this
|
||||||
|
# once per server to upload and run the post-install bootstrap script
|
||||||
|
# (infra/scripts/bootstrap.sh.tftpl) immediately after creation. Required.
|
||||||
|
ssh_private_key_path = "~/.ssh/id_ed25519"
|
||||||
|
|
||||||
# Optional overrides - defaults live in variables.tf
|
# Optional overrides - defaults live in variables.tf
|
||||||
# location = "nbg1"
|
# location = "nbg1"
|
||||||
# dev_server_type = "cx22"
|
# dev_server_type = "cx22"
|
||||||
# prod_server_type = "cx32"
|
# prod_server_type = "cx22"
|
||||||
# vpn_server_type = "cx22"
|
# vpn_server_type = "cx22"
|
||||||
# dev_volume_size = 50
|
# dev_volume_size = 20
|
||||||
# prod_volume_size = 50
|
# prod_volume_size = 20
|
||||||
# allowed_ssh_source_ips = ["203.0.113.4/32"]
|
# allowed_ssh_source_ips = ["203.0.113.4/32"]
|
||||||
|
# deploy_user = "deploy"
|
||||||
|
|||||||
+12
-1
@@ -76,7 +76,18 @@ variable "ssh_key_name" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_ssh_source_ips" {
|
||||||
description = "CIDRs allowed to reach port 22 on every server. Narrow this to your own IP(s) once known."
|
description = "CIDRs allowed to reach port 22 on the vpn server. Narrow this to your own IP(s) once known. dev and prod don't use this - their SSH is restricted to the vpn server's own public IP instead (see infra/main.tf)."
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = ["0.0.0.0/0", "::/0"]
|
default = ["0.0.0.0/0", "::/0"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Local path to the private key matching var.ssh_key_name. Used once per server by OpenTofu to upload and run the post-install bootstrap script over SSH immediately after creation (see infra/scripts/bootstrap.sh.tftpl)."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "deploy_user" {
|
||||||
|
description = "Non-root sudo user created on every server by the bootstrap script, with the same SSH key as root."
|
||||||
|
type = string
|
||||||
|
default = "deploy"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user