swithcing from CoreOS to Fedora Server
This commit is contained in:
parent
1180a3d8ac
commit
4d5880df27
42
README.md
42
README.md
@ -4,36 +4,48 @@
|
||||
<br>
|
||||
</div>
|
||||
|
||||
Secure Fedora CoreOS server setup with LUKS encryption, TPM, and BTRFS RAID1 with focus on Hetzner Infra.
|
||||
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 with focus on Hetzner Infra.
|
||||
|
||||
## Features
|
||||
|
||||
- Fedora CoreOS base
|
||||
- Fedora Server base
|
||||
- Full disk encryption with LUKS
|
||||
- Remote unlock via Tang server
|
||||
- TPM-based boot verification
|
||||
- BTRFS RAID1 storage
|
||||
- BTRFS RAID1 storage with optimized subvolumes
|
||||
- Automated deployment to Hetzner
|
||||
- Cloud-init based configuration
|
||||
|
||||
## Security Model
|
||||
|
||||
### Tang Server Operation
|
||||
The Tang server provides secure remote unlocking of LUKS volumes:
|
||||
1. First connection: Client verifies Tang's public key advertisement
|
||||
2. Boot time: Client sends encrypted challenge to Tang
|
||||
3. Tang proves identity by decrypting challenge
|
||||
4. Client receives key to unlock LUKS volume
|
||||
### Unlock Methods
|
||||
The system uses multiple methods to unlock the LUKS volumes:
|
||||
1. **Primary Method**: TPM2 + Tang server
|
||||
- TPM2 verifies boot integrity
|
||||
- Tang server provides remote unlock capability
|
||||
- Both must succeed for automatic unlock
|
||||
2. **Fallback Method**: Manual passphrase
|
||||
- Available via SSH if primary method fails
|
||||
- Can be used for recovery or maintenance
|
||||
|
||||
### TPM Integration
|
||||
- TPM2 chip verifies boot integrity
|
||||
- PCR measurements ensure system hasn't been tampered with
|
||||
- Combined with Tang for defense in depth
|
||||
- Monitors all critical boot components
|
||||
|
||||
### Storage Security
|
||||
- BTRFS RAID1 for data redundancy
|
||||
- Dedicated database subvolume with `nodatacow` and `noatime`
|
||||
- LUKS2 encryption with multiple unlock methods
|
||||
- Secure boot enabled by default
|
||||
- Redundant boot partition using BTRFS RAID1
|
||||
|
||||
## Repository Structure
|
||||
```
|
||||
.
|
||||
├── build.sh # Build and upload image from build-config
|
||||
├── deploy.sh # Deployment script for Hetzner from deploy-config
|
||||
├── build.py # Build and upload image from build-config
|
||||
├── deploy.py # Deployment script for Hetzner from deploy-config
|
||||
├── MASTER_README.md # Tang server setup documentation
|
||||
├── README.md # Main project documentation
|
||||
└── requirements.txt # Python dependencies
|
||||
@ -45,11 +57,11 @@ The Tang server provides secure remote unlocking of LUKS volumes:
|
||||
# Install tools
|
||||
curl -fsSL https://raw.githubusercontent.com/hetznercloud/cli/master/install.sh | bash
|
||||
go install github.com/hetznercloud/hcloud-upload-image@latest
|
||||
dnf install jq coreos-installer python3-pyyaml
|
||||
sudo dnf install -y jq python3-pyyaml libguestfs-tools cloud-image-utils curl
|
||||
|
||||
# Configure Hetzner
|
||||
export HCLOUD_TOKEN="your-token-here"
|
||||
hcloud ssh-key create --name "fedora-coreos-hetzner" --public-key "$(cat ~/.ssh/id_ed25519.pub)"
|
||||
hcloud ssh-key create --name "fedora-server-hetzner" --public-key "$(cat ~/.ssh/id_ed25519.pub)"
|
||||
```
|
||||
|
||||
## Setup
|
||||
@ -62,7 +74,7 @@ hcloud ssh-key create --name "fedora-coreos-hetzner" --public-key "$(cat ~/.ssh/
|
||||
|
||||
2. **Build Base Image** (one-time setup)
|
||||
```bash
|
||||
python3 build.py # Creates and uploads FCOS image to Hetzner
|
||||
python3 build.py # Creates and uploads Fedora Server image to Hetzner
|
||||
```
|
||||
|
||||
3. **Configure Deployment Settings**
|
||||
@ -78,7 +90,7 @@ hcloud ssh-key create --name "fedora-coreos-hetzner" --public-key "$(cat ~/.ssh/
|
||||
|
||||
5. **Verify**
|
||||
```bash
|
||||
ssh core@your-server
|
||||
ssh admin@your-server
|
||||
systemctl status clevis-luks-askpass
|
||||
lsblk
|
||||
clevis-luks-list -d /dev/sda2
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Build Configuration
|
||||
image:
|
||||
name: fedora-coreos-nullpoint
|
||||
stream: stable
|
||||
name: nullpoint
|
||||
version: 39
|
||||
arch: x86_64
|
||||
hetzner_arch: x86
|
||||
|
||||
@ -11,15 +11,46 @@ system:
|
||||
luks:
|
||||
tang_url: https://tang.example.com
|
||||
tang_thumbprint: your-tang-thumbprint
|
||||
|
||||
# TPM Configuration
|
||||
tpm:
|
||||
pcr_bank: sha256 # PCR bank to use (sha1 or sha256)
|
||||
pcr_ids: [0,4,7,8,9] # PCRs to measure
|
||||
# PCR descriptions:
|
||||
# 0: Core System Firmware executable code (BIOS/UEFI) (RECOMMENDED)
|
||||
# 1: Core System Firmware data (BIOS/UEFI settings)
|
||||
# 2: Extended or pluggable executable code
|
||||
# 3: Extended or pluggable firmware data
|
||||
# 4: Boot Manager Code (bootloader) (RECOMMENDED)
|
||||
# 5: Boot Manager Configuration and Data
|
||||
# 6: Platform-specific code
|
||||
# 7: Platform-specific configuration (RECOMMENDED)
|
||||
# 8: UEFI driver and application code (RECOMMENDED)
|
||||
# 9: UEFI driver and application configuration (RECOMMENDED)
|
||||
# 10: UEFI Handoff Tables
|
||||
# 11: UEFI Boot Services Code
|
||||
# 12: UEFI Boot Services Data
|
||||
# 13: UEFI Runtime Services Code
|
||||
# 14: UEFI Runtime Services Data
|
||||
# 15: UEFI Secure Boot State
|
||||
|
||||
# Storage Configuration
|
||||
storage:
|
||||
boot_size_mib: 512
|
||||
compression: zstd
|
||||
subvolumes:
|
||||
- name: "@"
|
||||
path: "/"
|
||||
- name: "@home"
|
||||
path: "/home"
|
||||
- name: "@var"
|
||||
path: "/var"
|
||||
# Cloud-init Configuration
|
||||
cloud_init:
|
||||
timezone: UTC
|
||||
users:
|
||||
- name: admin
|
||||
groups: wheel
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
ssh_authorized_keys:
|
||||
- "your-ssh-key-here"
|
||||
packages:
|
||||
- btrfs-progs
|
||||
- clevis
|
||||
- clevis-luks
|
||||
- clevis-tang
|
||||
- clevis-tpm2
|
||||
- tpm2-tools
|
||||
- tpm2-tss
|
||||
- cryptsetup
|
||||
- systemd
|
||||
- curl
|
277
build.py
277
build.py
@ -5,7 +5,9 @@ import sys
|
||||
import yaml
|
||||
import subprocess
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
def load_config(config_file):
|
||||
"""Load and parse YAML config file."""
|
||||
@ -14,7 +16,7 @@ def load_config(config_file):
|
||||
|
||||
def check_prerequisites():
|
||||
"""Check if required tools are installed."""
|
||||
required_tools = ['hcloud', 'hcloud-upload-image', 'jq', 'coreos-installer']
|
||||
required_tools = ['hcloud', 'hcloud-upload-image', 'jq', 'virt-customize', 'cloud-localds']
|
||||
for tool in required_tools:
|
||||
if not shutil.which(tool):
|
||||
print(f"Error: {tool} not found. Please install it first.")
|
||||
@ -26,177 +28,113 @@ def check_hetzner_token():
|
||||
print("Error: HCLOUD_TOKEN environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
def generate_ignition_config(config):
|
||||
"""Generate Ignition configuration."""
|
||||
def generate_cloud_init(config):
|
||||
"""Generate cloud-init configuration."""
|
||||
system = config['system']
|
||||
luks = system['luks']
|
||||
storage = system['storage']
|
||||
cloud_init = system['cloud_init']
|
||||
tpm_config = system['luks']['tpm']
|
||||
|
||||
# Generate the Ignition config
|
||||
ignition_config = {
|
||||
"ignition": {
|
||||
"version": "3.4.0",
|
||||
"config": {
|
||||
"merge": []
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"disks": [
|
||||
{
|
||||
"device": "/dev/sda",
|
||||
"partitions": [
|
||||
{
|
||||
"label": "boot",
|
||||
"number": 1,
|
||||
"sizeMiB": storage['boot_size_mib'],
|
||||
"typeGuid": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
|
||||
},
|
||||
{
|
||||
"label": "root",
|
||||
"number": 2,
|
||||
"sizeMiB": 0,
|
||||
"typeGuid": "21686148-6449-6E6F-744E-656564454649"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"device": "/dev/sdb",
|
||||
"partitions": [
|
||||
{
|
||||
"label": "boot2",
|
||||
"number": 1,
|
||||
"sizeMiB": storage['boot_size_mib'],
|
||||
"typeGuid": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
|
||||
},
|
||||
{
|
||||
"label": "root2",
|
||||
"number": 2,
|
||||
"sizeMiB": 0,
|
||||
"typeGuid": "21686148-6449-6E6F-744E-656564454649"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"luks": [
|
||||
{
|
||||
"name": "root",
|
||||
"device": "/dev/disk/by-partlabel/root",
|
||||
"clevis": {
|
||||
"tpm2": True,
|
||||
"tang": [
|
||||
{
|
||||
"url": luks['tang_url'],
|
||||
"thumbprint": luks['tang_thumbprint']
|
||||
}
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "root2",
|
||||
"device": "/dev/disk/by-partlabel/root2",
|
||||
"clevis": {
|
||||
"tpm2": True,
|
||||
"tang": [
|
||||
{
|
||||
"url": luks['tang_url'],
|
||||
"thumbprint": luks['tang_thumbprint']
|
||||
}
|
||||
],
|
||||
"threshold": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"filesystems": [
|
||||
{
|
||||
"path": "/boot",
|
||||
"device": "/dev/disk/by-partlabel/boot",
|
||||
"format": "vfat",
|
||||
"label": "boot",
|
||||
"wipeFilesystem": True
|
||||
},
|
||||
{
|
||||
"path": "/boot2",
|
||||
"device": "/dev/disk/by-partlabel/boot2",
|
||||
"format": "vfat",
|
||||
"label": "boot2",
|
||||
"wipeFilesystem": True
|
||||
}
|
||||
]
|
||||
},
|
||||
"systemd": {
|
||||
"units": [
|
||||
{
|
||||
"name": "kernel-sign.service",
|
||||
"enabled": True,
|
||||
"contents": "[Unit]\nDescription=Sign new kernel images\nAfter=kernel-install.service\n\n[Service]\nType=oneshot\nExecStart=/usr/local/bin/sign-kernel.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target"
|
||||
}
|
||||
]
|
||||
}
|
||||
# Generate TPM config
|
||||
tpm_json = {
|
||||
'pcr_bank': tpm_config['pcr_bank'],
|
||||
'pcr_ids': ','.join(map(str, tpm_config['pcr_ids']))
|
||||
}
|
||||
|
||||
# Base cloud-init config
|
||||
cloud_config = {
|
||||
'timezone': cloud_init['timezone'],
|
||||
'users': cloud_init['users'],
|
||||
'package_update': True,
|
||||
'package_upgrade': True,
|
||||
'packages': cloud_init['packages'],
|
||||
'write_files': [
|
||||
{
|
||||
'path': '/etc/clevis/tang.conf',
|
||||
'content': f"URL={system['luks']['tang_url']}\nThumbprint={system['luks']['tang_thumbprint']}\n",
|
||||
'permissions': '0644'
|
||||
},
|
||||
{
|
||||
'path': '/etc/clevis/tpm2.conf',
|
||||
'content': json.dumps(tpm_json),
|
||||
'permissions': '0644'
|
||||
}
|
||||
],
|
||||
'runcmd': [
|
||||
# Verify Tang server is accessible
|
||||
f'curl -s -f {system["luks"]["tang_url"]}/adv > /dev/null || {{ echo "Tang server not accessible"; exit 1; }}',
|
||||
# Verify TPM is available
|
||||
'tpm2_getcap properties-fixed > /dev/null || { echo "TPM not available"; exit 1; }',
|
||||
# Create filesystem
|
||||
'mkfs.btrfs -f -d raid1 -m raid1 /dev/sda2 /dev/sdb2',
|
||||
'mount /dev/sda2 /mnt',
|
||||
# Create subvolumes
|
||||
'btrfs subvolume create /mnt/@boot',
|
||||
'btrfs subvolume create /mnt/@home',
|
||||
'btrfs subvolume create /mnt/@db',
|
||||
'chattr +C /mnt/@db',
|
||||
# Setup LUKS with escaped passphrase
|
||||
'echo -n "${LUKS_PASSPHRASE}" | tr -d "\n" | cryptsetup luksFormat /dev/sda2 --type luks2 --key-file -',
|
||||
'echo -n "${LUKS_PASSPHRASE}" | tr -d "\n" | cryptsetup luksFormat /dev/sdb2 --type luks2 --key-file -',
|
||||
# Setup Clevis with error handling
|
||||
'clevis luks bind -d /dev/sda2 tpm2 -c /etc/clevis/tpm2.conf || { echo "TPM bind failed"; exit 1; }',
|
||||
'clevis luks bind -d /dev/sda2 tang -c /etc/clevis/tang.conf || { echo "Tang bind failed"; exit 1; }',
|
||||
'clevis luks bind -d /dev/sdb2 tpm2 -c /etc/clevis/tpm2.conf || { echo "TPM bind failed"; exit 1; }',
|
||||
'clevis luks bind -d /dev/sdb2 tang -c /etc/clevis/tang.conf || { echo "Tang bind failed"; exit 1; }',
|
||||
# Setup fstab
|
||||
'echo "/dev/mapper/root / btrfs compress=zstd 0 0" >> /etc/fstab',
|
||||
'echo "/dev/mapper/root /boot btrfs subvol=@boot,compress=zstd 0 0" >> /etc/fstab',
|
||||
'echo "/dev/mapper/root /home btrfs subvol=@home,compress=zstd 0 0" >> /etc/fstab',
|
||||
'echo "/dev/mapper/root /db btrfs subvol=@db,nodatacow,noatime,compress=zstd 0 0" >> /etc/fstab',
|
||||
# Enable services
|
||||
'systemctl enable clevis-luks-askpass.service',
|
||||
'systemctl enable clevis-luks-askpass.path'
|
||||
]
|
||||
}
|
||||
|
||||
return yaml.dump(cloud_config)
|
||||
|
||||
# Add BTRFS filesystems
|
||||
for subvol in storage['subvolumes']:
|
||||
ignition_config['storage']['filesystems'].append({
|
||||
"path": subvol['path'],
|
||||
"device": "/dev/disk/by-id/dm-name-root",
|
||||
"format": "btrfs",
|
||||
"label": "root",
|
||||
"wipeFilesystem": subvol['name'] == "@",
|
||||
"options": [
|
||||
f"subvol={subvol['name']}",
|
||||
f"compress={storage['compression']}"
|
||||
]
|
||||
})
|
||||
|
||||
return json.dumps(ignition_config, indent=2)
|
||||
|
||||
def download_fcos_image(config):
|
||||
"""Download FCOS image."""
|
||||
def download_fedora_image(config):
|
||||
"""Download Fedora Server cloud image."""
|
||||
image = config['image']
|
||||
url = f"https://download.fedoraproject.org/pub/fedora/linux/releases/{image['version']}/Cloud/x86_64/images/Fedora-Cloud-Base-{image['version']}-1.6.x86_64.qcow2"
|
||||
|
||||
cmd = ['curl', '-L', '-o', 'fedora-server.qcow2', url]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
def customize_image(config):
|
||||
"""Customize the Fedora Server image."""
|
||||
# Generate cloud-init config
|
||||
cloud_init = generate_cloud_init(config)
|
||||
|
||||
# Create cloud-init ISO
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml') as f:
|
||||
f.write(cloud_init)
|
||||
f.flush()
|
||||
subprocess.run(['cloud-localds', 'cloud-init.iso', f.name], check=True)
|
||||
|
||||
# Customize image
|
||||
cmd = [
|
||||
'coreos-installer', 'download',
|
||||
'-s', image['stream'],
|
||||
'-p', 'hetzner',
|
||||
'-a', image['arch'],
|
||||
'-f', 'raw.xz'
|
||||
'virt-customize',
|
||||
'-a', 'fedora-server.qcow2',
|
||||
'--selinux-relabel',
|
||||
'--run-command', 'dnf clean all',
|
||||
'--run-command', 'dnf -y update',
|
||||
'--run-command', 'dnf -y install cloud-init',
|
||||
'--copy-in', 'cloud-init.iso:/var/lib/cloud/seed/nocloud/'
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
def get_image_file():
|
||||
"""Get the downloaded image file name."""
|
||||
files = sorted(Path('.').glob('fedora-coreos-*-hetzner.x86_64.raw.xz'))
|
||||
if not files:
|
||||
print("Error: No FCOS image found")
|
||||
sys.exit(1)
|
||||
return files[-1]
|
||||
|
||||
def delete_existing_image(image_name):
|
||||
"""Delete existing image if it exists."""
|
||||
cmd = [
|
||||
'hcloud', 'image', 'list',
|
||||
'--type=snapshot',
|
||||
f'--selector=name={image_name}',
|
||||
'--output', 'json'
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
images = json.loads(result.stdout)
|
||||
|
||||
if images:
|
||||
print("Deleting existing image...")
|
||||
subprocess.run(['hcloud', 'image', 'delete', str(images[0]['id'])], check=True)
|
||||
|
||||
def create_snapshot(config, image_file):
|
||||
def create_snapshot(config):
|
||||
"""Create new snapshot from image."""
|
||||
image = config['image']
|
||||
cmd = [
|
||||
'hcloud-upload-image', 'upload',
|
||||
'--architecture', image['hetzner_arch'],
|
||||
'--compression', 'xz',
|
||||
'--image-path', str(image_file),
|
||||
'--image-path', 'fedora-server.qcow2',
|
||||
'--name', image['name'],
|
||||
'--labels', f'os=fedora-coreos,channel={image["stream"]}',
|
||||
'--description', f'Fedora CoreOS ({image["stream"]}, {image["arch"]}) for Nullpoint'
|
||||
'--labels', f'os=fedora-server,version={image["version"]}',
|
||||
'--description', f'Fedora Server {image["version"]} for Nullpoint'
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
@ -213,26 +151,17 @@ def main():
|
||||
check_prerequisites()
|
||||
check_hetzner_token()
|
||||
|
||||
# Generate Ignition config
|
||||
print("Generating Ignition config...")
|
||||
ignition_config = generate_ignition_config(config)
|
||||
with open('config.ign', 'w') as f:
|
||||
f.write(ignition_config)
|
||||
# Download Fedora Server image
|
||||
print("Downloading Fedora Server image...")
|
||||
download_fedora_image(config)
|
||||
|
||||
# Download FCOS image
|
||||
print("Downloading FCOS image...")
|
||||
download_fcos_image(config)
|
||||
# Customize image
|
||||
print("Customizing image...")
|
||||
customize_image(config)
|
||||
|
||||
# Get image file
|
||||
image_file = get_image_file()
|
||||
|
||||
# Delete existing image if it exists
|
||||
print("Checking for existing image...")
|
||||
delete_existing_image(config['image']['name'])
|
||||
|
||||
# Create new snapshot
|
||||
print("Creating snapshot from image...")
|
||||
create_snapshot(config, image_file)
|
||||
# Create snapshot
|
||||
print("Creating snapshot...")
|
||||
create_snapshot(config)
|
||||
|
||||
print("Image build complete!")
|
||||
|
||||
|
106
deploy.py
106
deploy.py
@ -8,6 +8,39 @@ import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
import random
|
||||
import tempfile
|
||||
import secrets
|
||||
import string
|
||||
|
||||
# List of adjectives for hostname generation
|
||||
ADJECTIVES = [
|
||||
# Scientific/Sci-fi
|
||||
'quantum', 'atomic', 'plasma', 'fusion', 'ionic', 'magnetic', 'cosmic',
|
||||
'stellar', 'nebular', 'pulsar', 'quasar', 'warp', 'phaser', 'hyper',
|
||||
'temporal', 'spatial', 'dimensional', 'subspace', 'transwarp',
|
||||
# Cool/Interesting
|
||||
'abysmal', 'adamant', 'aerial', 'arcane', 'astral', 'azure', 'celestial',
|
||||
'crimson', 'cryptic', 'crystalline', 'dormant', 'eerie', 'eldritch',
|
||||
'ethereal', 'fractal', 'frozen', 'ghostly', 'gilded', 'singular',
|
||||
'hollow', 'infernal', 'lunar', 'mystic', 'nebulous', 'obsidian',
|
||||
'occult', 'prismatic', 'radiant', 'shadow', 'solar', 'spectral',
|
||||
'stellar', 'sublime', 'titanic', 'twilight', 'void', 'volcanic'
|
||||
]
|
||||
|
||||
NOUNS = [
|
||||
# Star Trek
|
||||
'enterprise', 'voyager', 'galaxy', 'intrepid', 'nova', 'warbird', 'falcon',
|
||||
'aegis', 'nemesis', 'equinox', 'stargazer', 'challenger', 'discovery',
|
||||
'prometheus', 'odyssey', 'daedalus', 'apollo', 'korolev', 'phoenix', 'orion',
|
||||
# Space
|
||||
'nebula', 'pulsar', 'quasar', 'nova', 'supernova', 'blackhole',
|
||||
'wormhole', 'singularity', 'galaxy', 'void', 'rift', 'nexus',
|
||||
# Cool Concepts
|
||||
'abyss', 'aether', 'anomaly', 'artifact', 'beacon', 'cipher', 'crystal',
|
||||
'echo', 'enigma', 'essence', 'fractal', 'horizon', 'infinity',
|
||||
'paradox', 'phoenix', 'prism', 'spectrum', 'tesseract',
|
||||
'vector', 'vertex', 'vortex', 'zenith'
|
||||
]
|
||||
|
||||
def load_config(config_file):
|
||||
"""Load and parse YAML config file."""
|
||||
@ -28,12 +61,12 @@ def check_hetzner_token():
|
||||
print("Error: HCLOUD_TOKEN environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
def generate_hostname(config):
|
||||
"""Generate a unique hostname."""
|
||||
prefix = config['hostname']['prefix']
|
||||
timestamp = datetime.now().strftime('%y%m%d')
|
||||
random_num = random.randint(0, 999)
|
||||
return f"{prefix}-{timestamp}-{random_num:03d}"
|
||||
def generate_hostname():
|
||||
"""Generate a unique hostname in adjective-noun-date-time format."""
|
||||
adjective = random.choice(ADJECTIVES)
|
||||
noun = random.choice(NOUNS)
|
||||
timestamp = datetime.now().strftime('%y%m%d-%H%M')
|
||||
return f"{adjective}-{noun}-{timestamp}"
|
||||
|
||||
def get_image_id():
|
||||
"""Get the base image ID."""
|
||||
@ -52,19 +85,58 @@ def get_image_id():
|
||||
|
||||
return images[0]['id']
|
||||
|
||||
def generate_secure_passphrase(length=32):
|
||||
"""Generate a secure random passphrase."""
|
||||
alphabet = string.ascii_letters + string.digits + string.punctuation
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
def create_server(config, hostname, image_id):
|
||||
"""Create a new server."""
|
||||
hetzner = config['hetzner']
|
||||
cmd = [
|
||||
'hcloud', 'server', 'create',
|
||||
'--name', hostname,
|
||||
'--type', hetzner['server_type'],
|
||||
'--datacenter', hetzner['datacenter'],
|
||||
'--image', str(image_id),
|
||||
'--ssh-key', hetzner['ssh_key_name'],
|
||||
'--user-data-from-file', 'config.ign'
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# Generate secure passphrase
|
||||
passphrase = generate_secure_passphrase()
|
||||
|
||||
# Generate cloud-init config for this specific server
|
||||
cloud_init = {
|
||||
'hostname': hostname,
|
||||
'timezone': 'UTC',
|
||||
'users': config.get('users', []),
|
||||
'package_update': True,
|
||||
'package_upgrade': True,
|
||||
'write_files': [
|
||||
{
|
||||
'path': '/root/luks-passphrase.txt',
|
||||
'content': passphrase,
|
||||
'permissions': '0600'
|
||||
}
|
||||
],
|
||||
'runcmd': [
|
||||
f'export LUKS_PASSPHRASE="{passphrase}"'
|
||||
]
|
||||
}
|
||||
|
||||
# Create temporary cloud-init config
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml') as f:
|
||||
yaml.dump(cloud_init, f)
|
||||
f.flush()
|
||||
|
||||
cmd = [
|
||||
'hcloud', 'server', 'create',
|
||||
'--name', hostname,
|
||||
'--type', hetzner['server_type'],
|
||||
'--datacenter', hetzner['datacenter'],
|
||||
'--image', str(image_id),
|
||||
'--ssh-key', hetzner['ssh_key_name'],
|
||||
'--user-data-from-file', f.name
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
# Print passphrase for user to save
|
||||
print(f"\nIMPORTANT: Save this LUKS passphrase for {hostname}:")
|
||||
print(f"{passphrase}\n")
|
||||
print("This passphrase will be needed if TPM+Tang unlock fails.")
|
||||
print("It is also saved in /root/luks-passphrase.txt on the server.")
|
||||
|
||||
def get_server_ip(hostname):
|
||||
"""Get the server's IP address."""
|
||||
@ -86,7 +158,7 @@ def main():
|
||||
check_hetzner_token()
|
||||
|
||||
# Generate hostname
|
||||
hostname = generate_hostname(config)
|
||||
hostname = generate_hostname()
|
||||
|
||||
# Get image ID
|
||||
image_id = get_image_id()
|
||||
|
Loading…
Reference in New Issue
Block a user