Compare commits

..

No commits in common. "master" and "legacy" have entirely different histories.

8 changed files with 817 additions and 467 deletions

9
.gitignore vendored
View File

@ -1,12 +1,15 @@
# Config files
config.yaml
build-config.yaml
deploy-config.yaml
config.ign
# Python
__pycache__/
*.py[cod]
*$py.class
# Images
*.iso
# Downloaded images
fedora-coreos-*-hetzner.x86_64.raw.xz
# Environment
.env

View File

@ -4,10 +4,7 @@
<br>
</div>
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 for (Hetzner) Dedicated Servers.
> [!NOTE]
> This project is still WIP, having some issues with networking of the installeer / installed instance.
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 with focus on Hetzner Infra.
## Features
@ -17,7 +14,7 @@ Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 for (Hetzn
- TPM-based boot verification
- BTRFS RAID1 for data redundancy
- Dedicated database subvolume with `nodatacow` and `noatime`
- SSH key-only access with early boot SSH via dropbear
- Automated deployment to Hetzner
If you need a dead man's switch to go along with it check out [raven](https://git.dominik-roth.eu/dodox/raven).
@ -37,65 +34,55 @@ The system uses multiple methods to unlock the LUKS volumes:
### TPM Updates
After firmware updates (UEFI/BIOS), the TPM bindings need to be updated:
(otherwise the system will not be able to boot without recovery phrase)
1. Use the provided script: `sudo /root/update-tpm-bindings.py`
1. Use the provided script: `sudo /root/update-tpm-bindings.sh`
2. The script will:
- Show current PCR values
- Update TPM bindings to match new measurements
- Verify all bindings are correct
3. Manual passphrase is available in `/root/luks-passphrase.txt` if needed
## Prerequisites
```bash
# Install tools
curl -fsSL https://raw.githubusercontent.com/hetznercloud/cli/master/install.sh | bash
go install github.com/hetznercloud/hcloud-upload-image@latest
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-server-hetzner" --public-key "$(cat ~/.ssh/id_ed25519.pub)"
```
## Setup
1. **Configure Installer**
1. **Configure Build Settings**
```bash
# Edit the variables at the top of install.sh:
vim install.sh
cp build-config.yaml.example build-config.yaml
vim build-config.yaml # Edit LUKS, storage, and image settings
```
Set your:
- Tang server URLs and thumbprints
- TPM PCR settings
- Fedora version
- SSH public key for the default user
2. **Install on Hetzner Server**
- Log into Hetzner Robot
- Select your server
- Go to "Rescue" tab
- Choose "Linux" and "64 bit"
- Activate Rescue System
- Upload the installer:
```bash
scp install.sh root@your-server:/root/
```
- SSH into Rescue System:
```bash
ssh root@your-server
```
- Make it executable and run:
```bash
chmod +x install.sh
./install.sh
```
- If the script tells you that no TPM is available, you'll need to make a support ticket to get KVM access and enable TPM in the BIOS.
- The script will:
- Generate and display a LUKS passphrase (save this!)
- Download and prepare the Fedora installer
- Configure networking for Hetzner's unusual setup
- Start the Fedora installer
- You can monitor the installation via SSH on port 2222:
```bash
ssh -p 2222 root@your-server
```
- During the Fedora installation:
- Disk encryption and RAID will be configured
- TPM and Tang bindings will be set up
- Network configuration will be applied
3. **Verify Installation**
2. **Build Base Image** (one-time setup)
```bash
ssh null@your-server
python3 build.py # Creates and uploads Fedora Server image to Hetzner
```
3. **Configure Deployment Settings**
```bash
cp deploy-config.yaml.example deploy-config.yaml
vim deploy-config.yaml # Edit server type, location, and hostname settings
```
4. **Deploy Server**
```bash
python3 deploy.py # Creates new server from base image
```
5. **Verify**
```bash
ssh admin@your-server
systemctl status clevis-luks-askpass
lsblk
btrfs filesystem show # Check RAID1 status
clevis-luks-list -d /dev/sda3 # Note: sda3 is the LUKS partition
clevis-luks-list -d /dev/sda2
```

43
build-config.yaml.example Normal file
View File

@ -0,0 +1,43 @@
# Build Configuration
image:
name: nullpoint
version: 39
arch: x86_64
hetzner_arch: x86
# System Configuration
system:
# LUKS Configuration
luks:
tang_servers:
- url: https://tang1.example.com
thumbprint: your-tang1-thumbprint
- url: https://tang2.example.com
thumbprint: your-tang2-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
# System Settings
timezone: UTC
keyboard: us
language: en_US.UTF-8

547
build.py Normal file
View File

@ -0,0 +1,547 @@
#!/usr/bin/env python3
import os
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."""
with open(config_file, 'r') as f:
return yaml.safe_load(f)
def check_prerequisites():
"""Check if required tools are installed."""
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.")
sys.exit(1)
def check_hetzner_token():
"""Check if HCLOUD_TOKEN is set."""
if not os.environ.get('HCLOUD_TOKEN'):
print("Error: HCLOUD_TOKEN environment variable not set")
sys.exit(1)
def generate_cloud_init(config):
"""Generate cloud-init configuration."""
system = config['system']
cloud_init = system['cloud_init']
tpm_config = system['luks']['tpm']
# 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'] + ['mdadm'], # Add mdadm package
'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'
},
{
'path': '/root/update-tpm-bindings.sh',
'content': '''#!/bin/bash
# Script to update TPM bindings after firmware updates
# Usage: sudo ./update-tpm-bindings.sh
set -e
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Check if TPM is available
if ! tpm2_getcap properties-fixed > /dev/null; then
echo "Error: TPM not available"
exit 1
fi
# Check if at least one Tang server is accessible
TANG_AVAILABLE=0
for tang_url in $(grep -h "url" /etc/clevis/sss.conf | grep -o '"url": "[^"]*"' | cut -d'"' -f4); do
if curl -s -f "$tang_url/adv" > /dev/null; then
echo "Tang server $tang_url is accessible"
TANG_AVAILABLE=1
break
else
echo "Tang server $tang_url is not accessible"
fi
done
if [ $TANG_AVAILABLE -eq 0 ]; then
echo "Error: No Tang servers are accessible"
exit 1
fi
# Get current PCR values
echo "Current PCR values:"
PCR_IDS=$(grep -h "pcr_ids" /etc/clevis/sss.conf | grep -o '"pcr_ids": "[^"]*"' | cut -d'"' -f4)
tpm2_pcrread sha256:$(echo $PCR_IDS | tr ',' ' ')
# Ask for confirmation
read -p "Have you updated firmware? Continue with TPM binding update? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted"
exit 1
fi
# Get LUKS passphrase
if [ -f /root/luks-passphrase.txt ]; then
LUKS_PASSPHRASE=$(cat /root/luks-passphrase.txt)
else
echo "LUKS passphrase file not found. Please enter your LUKS passphrase:"
read -s LUKS_PASSPHRASE
echo
fi
# Update TPM bindings
echo "Updating TPM bindings..."
for dev in /dev/sda2 /dev/sdb2; do
echo "Processing $dev..."
# Unbind old SSS binding
SLOT=$(clevis luks list -d "$dev" | grep -n "sss" | cut -d: -f1)
if [ -n "$SLOT" ]; then
echo "Removing old SSS binding from slot $SLOT"
clevis luks unbind -d "$dev" -s "$SLOT" || true
fi
# Create new binding with SSS using the same config
echo "Adding new SSS binding"
echo -n "$LUKS_PASSPHRASE" | clevis luks bind -d "$dev" sss -c /etc/clevis/sss.conf -k-
# Verify binding was successful
if ! clevis luks list -d "$dev" | grep -q "sss"; then
echo "Error: Failed to create SSS binding for $dev"
exit 1
fi
done
echo "TPM bindings updated successfully!"
echo "Please reboot to verify the changes."
''',
'permissions': '0700'
}
],
# Use cloud-init's native disk setup for partitioning only
'disk_setup': {
'/dev/sda': {
'table_type': 'gpt',
'layout': [
[1024, 'boot'], # 1GB for boot
['auto', 'data'] # Rest for LUKS1
],
'overwrite': True
},
'/dev/sdb': {
'table_type': 'gpt',
'layout': [
[1024, 'boot'], # 1GB for boot mirror
['auto', 'data'] # Rest for LUKS2
],
'overwrite': True
}
},
'runcmd': [
# Check if passphrase is set
'[ -n "$LUKS_PASSPHRASE" ] || { echo "Error: LUKS passphrase not found"; exit 1; }',
# 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 boot RAID1
'mdadm --create --verbose /dev/md0 --level=1 --raid-devices=2 /dev/sda1 /dev/sdb1 --metadata=1.2',
# Create filesystem on RAID array
'mkfs.ext4 -L boot /dev/md0',
# Mount boot RAID
'mkdir -p /mnt/boot',
'mount /dev/md0 /mnt/boot',
# Setup LUKS on data partitions
'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; }',
# Open LUKS volumes
'echo -n "${LUKS_PASSPHRASE}" | tr -d "\n" | cryptsetup luksOpen /dev/sda2 root_a --key-file -',
'echo -n "${LUKS_PASSPHRASE}" | tr -d "\n" | cryptsetup luksOpen /dev/sdb2 root_b --key-file -',
# Create BTRFS on decrypted devices
'mkfs.btrfs -f -d raid1 -m raid1 /dev/mapper/root_a /dev/mapper/root_b',
'mount /dev/mapper/root_a /mnt',
# Create subvolumes
'btrfs subvolume create /mnt/@home',
'btrfs subvolume create /mnt/@db',
'chattr +C /mnt/@db',
# Setup fstab
'echo "/dev/md0 /boot ext4 defaults 0 0" >> /etc/fstab',
'echo "/dev/mapper/root_a / btrfs compress=zstd 0 0" >> /etc/fstab',
'echo "/dev/mapper/root_a /home btrfs subvol=@home,compress=zstd 0 0" >> /etc/fstab',
'echo "/dev/mapper/root_a /db btrfs subvol=@db,nodatacow,noatime,compress=zstd 0 0" >> /etc/fstab',
# Save RAID configuration
'mdadm --detail --scan > /etc/mdadm.conf',
# Enable services
'systemctl enable clevis-luks-askpass.service',
'systemctl enable clevis-luks-askpass.path'
]
}
return yaml.dump(cloud_config)
def download_fedora_image(config):
"""Download Fedora Server netinstall image."""
image = config['image']
url = f"https://download.fedoraproject.org/pub/fedora/linux/releases/{image['version']}/Server/x86_64/iso/Fedora-Server-netinst-x86_64-{image['version']}-1.6.iso"
cmd = ['curl', '-L', '-o', 'fedora-server.iso', url]
subprocess.run(cmd, check=True)
def generate_kickstart(config):
"""Generate kickstart configuration for automated installation."""
system = config['system']
# TPM update script content
tpm_update_script = '''#!/bin/bash
# Script to update TPM bindings after firmware updates
# Usage: sudo ./update-tpm-bindings.sh
set -e
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Check if TPM is available
if ! tpm2_getcap properties-fixed > /dev/null; then
echo "Error: TPM not available"
exit 1
fi
# Check if at least one Tang server is accessible
TANG_AVAILABLE=0
for tang_url in $(grep -h "url" /etc/clevis/sss.conf | grep -o '"url": "[^"]*"' | cut -d'"' -f4); do
if curl -s -f "$tang_url/adv" > /dev/null; then
echo "Tang server $tang_url is accessible"
TANG_AVAILABLE=1
break
else
echo "Tang server $tang_url is not accessible"
fi
done
if [ $TANG_AVAILABLE -eq 0 ]; then
echo "Error: No Tang servers are accessible"
exit 1
fi
# Get current PCR values
echo "Current PCR values:"
PCR_IDS=$(grep -h "pcr_ids" /etc/clevis/sss.conf | grep -o '"pcr_ids": "[^"]*"' | cut -d'"' -f4)
tpm2_pcrread sha256:$(echo $PCR_IDS | tr ',' ' ')
# Ask for confirmation
read -p "Have you updated firmware? Continue with TPM binding update? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted"
exit 1
fi
# Get LUKS passphrase
if [ -f /root/luks-passphrase.txt ]; then
LUKS_PASSPHRASE=$(cat /root/luks-passphrase.txt)
else
echo "LUKS passphrase file not found. Please enter your LUKS passphrase:"
read -s LUKS_PASSPHRASE
echo
fi
# Update TPM bindings
echo "Updating TPM bindings..."
for dev in /dev/sda2 /dev/sdb2; do
echo "Processing $dev..."
# Unbind old SSS binding
SLOT=$(clevis luks list -d "$dev" | grep -n "sss" | cut -d: -f1)
if [ -n "$SLOT" ]; then
echo "Removing old SSS binding from slot $SLOT"
clevis luks unbind -d "$dev" -s "$SLOT" || true
fi
# Create new binding with SSS using the same config
echo "Adding new SSS binding"
echo -n "$LUKS_PASSPHRASE" | clevis luks bind -d "$dev" sss -c /etc/clevis/sss.conf -k-
# Verify binding was successful
if ! clevis luks list -d "$dev" | grep -q "sss"; then
echo "Error: Failed to create SSS binding for $dev"
exit 1
fi
done
echo "TPM bindings updated successfully!"
echo "Please reboot to verify the changes."
'''
# Create SSS config for TPM + any Tang server
tang_servers = []
for server in system['luks']['tang_servers']:
tang_servers.append({"url": server['url']})
# Convert Tang servers to JSON for SSS config
tang_servers_json = json.dumps(tang_servers)
# Create SSS policy: Require TPM AND at least one Tang server
sss_config = {
"t": 2, # Threshold: Both pins must succeed
"pins": {
"tpm2": {
"pcr_bank": system['luks']['tpm']['pcr_bank'],
"pcr_ids": ','.join(map(str, system['luks']['tpm']['pcr_ids']))
},
"tang": {"t": 1, "tang": tang_servers} # Only one Tang server needed from the list
}
}
# Convert config to JSON string
sss_config_json = json.dumps(sss_config)
kickstart = f"""# Kickstart configuration for Fedora Server
# Generated for Nullpoint
# System language
lang en_US.UTF-8
# Keyboard layouts
keyboard --vckeymap=us --xlayouts='us'
# Network information
network --bootproto=dhcp --device=link --activate
# Root password
rootpw --lock
# System timezone
timezone {system['timezone']} --utc
# Installation type
text
# Wipe all disk
zerombr
clearpart --all --initlabel
# Disk partitioning information
# Boot partitions (5GB each)
part /boot --fstype=btrfs --size=5120 --ondisk=sda
part /boot --fstype=btrfs --size=5120 --ondisk=sdb
# Main data partitions with LUKS
part / --fstype=btrfs --encrypted --cipher=aes-xts-plain64 --luks-version=luks2 --grow --ondisk=sda
part / --fstype=btrfs --encrypted --cipher=aes-xts-plain64 --luks-version=luks2 --grow --ondisk=sdb
# Package source
url --mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=fedora-$releasever&arch=$basearch
repo --name=fedora
repo --name=updates
# Make sure initial-setup runs
firstboot --reconfig
# Package selection
%packages
@^server-product
@system-tools
btrfs-progs
clevis
clevis-luks
clevis-tang
clevis-tpm2
tpm2-tools
tpm2-tss
cryptsetup
systemd
curl
shim-x64
grub2-efi-x64
dropbear
%end
# Pre-installation script
%pre
# Create TPM and Tang config files
mkdir -p /etc/clevis
# Save SSS config for TPM + Tang servers
cat > /etc/clevis/sss.conf << EOF
{sss_config_json}
EOF
%end
# Post-installation script
%post
# https://unix.stackexchange.com/a/351755 for handling TTY in anaconda
printf "\n=== Nullpoint Installation Progress ===\r\n" > /dev/tty1
printf "Press Alt+F3 to view detailed installation logs\r\n" > /dev/tty1
printf "Press Alt+F1 to return to main installation screen\r\n" > /dev/tty1
printf "Current step: Setting up TPM and Tang...\r\n\n" > /dev/tty1
{{
# Get the LUKS passphrase that was used during installation
LUKS_PASSPHRASE=$(cat /tmp/luks-passphrase.txt)
echo "$LUKS_PASSPHRASE" > /root/luks-passphrase.txt
chmod 600 /root/luks-passphrase.txt
# Setup Clevis with SSS policy (TPM + at least one Tang server)
printf "Configuring TPM and Tang with SSS policy...\r\n" > /dev/tty1
echo "Using SSS policy: TPM verification AND (at least one Tang server)" > /dev/tty1
echo -n "$LUKS_PASSPHRASE" | clevis luks bind -d /dev/sda2 sss -c /etc/clevis/sss.conf -k-
echo -n "$LUKS_PASSPHRASE" | clevis luks bind -d /dev/sdb2 sss -c /etc/clevis/sss.conf -k-
# Get BTRFS UUID (same for all devices in the filesystem)
BTRFS_UUID=$(blkid -s UUID -o value /dev/mapper/luks-$(blkid -s UUID -o value /dev/sda2))
# Create subvolumes
printf "Creating BTRFS subvolumes...\r\n" > /dev/tty1
# Mount both devices for RAID1
mount -t btrfs -o raid1 UUID=$BTRFS_UUID /mnt
btrfs subvolume create /mnt/@root
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@db
chattr +C /mnt/@db
# Setup fstab
printf "Configuring system mount points...\r\n" > /dev/tty1
cat > /etc/fstab << EOF
UUID=$BTRFS_UUID / btrfs subvol=@root,compress=zstd,raid1 0 0
UUID=$BTRFS_UUID /home btrfs subvol=@home,compress=zstd,raid1 0 0
UUID=$BTRFS_UUID /db btrfs subvol=@db,nodatacow,noatime,compress=zstd,raid1 0 0
EOF
# Configure dropbear for early SSH access
printf "Configuring early SSH access...\r\n" > /dev/tty1
mkdir -p /etc/dropbear
echo "{config['admin_ssh_key']}" > /etc/dropbear/authorized_keys
chmod 600 /etc/dropbear/authorized_keys
# Enable dropbear for early SSH
systemctl enable dropbear
systemctl enable dropbear.socket
# Enable services
printf "Enabling system services...\r\n" > /dev/tty1
systemctl enable clevis-luks-askpass.service
systemctl enable clevis-luks-askpass.path
# Create TPM update script
printf "Creating TPM update script...\r\n" > /dev/tty1
cat > /root/update-tpm-bindings.sh << 'EOF'
{tpm_update_script}
EOF
chmod +x /root/update-tpm-bindings.sh
printf "\nInstallation complete! The system will reboot shortly.\r\n" > /dev/tty1
printf "IMPORTANT: LUKS passphrase has been saved to /root/luks-passphrase.txt\r\n" > /dev/tty1
}} 2>&1 | tee -a /root/postinstall.log > /dev/tty3
%end
"""
return kickstart
def customize_image(config):
"""Customize the Fedora Server image."""
# Generate kickstart config
kickstart = generate_kickstart(config)
# Create kickstart file
with tempfile.NamedTemporaryFile(mode='w', suffix='.ks') as f:
f.write(kickstart)
f.flush()
# Create custom ISO with kickstart
cmd = [
'mkisofs',
'-o', 'fedora-server-custom.iso',
'-b', 'isolinux/isolinux.bin',
'-c', 'isolinux/boot.cat',
'-boot-info-table',
'-no-emul-boot',
'-boot-load-size', '4',
'-R',
'-J',
'-v',
'-T',
'-V', 'Fedora-S-custom',
'-A', 'Fedora-S-custom',
'fedora-server.iso',
f.name
]
subprocess.run(cmd, check=True)
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', 'fedora-server-custom.iso',
'--name', image['name'],
'--labels', f'os=fedora-server,version={image["version"]}',
'--description', f'Fedora Server {image["version"]} for Nullpoint'
]
subprocess.run(cmd, check=True)
def main():
"""Main entry point."""
# Load config
if not os.path.exists('build-config.yaml'):
print("Error: build-config.yaml not found")
sys.exit(1)
config = load_config('build-config.yaml')
# Check prerequisites
check_prerequisites()
check_hetzner_token()
# Download Fedora Server image
print("Downloading Fedora Server image...")
download_fedora_image(config)
# Customize image
print("Customizing image...")
customize_image(config)
# Create snapshot
print("Creating snapshot...")
create_snapshot(config)
print("Image build complete!")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,13 @@
# Deployment Configuration
hetzner:
datacenter: nbg1
server_type: cx31
ssh_key_name: fedora-server-hetzner
# Admin Configuration
admin_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..." # Your SSH public key here
# Hostname Configuration
hostname:
prefix: nullpoint
format: "{prefix}-{date}-{random}" # date format: YYMMDD, random: 000-999

169
deploy.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
import os
import sys
import yaml
import subprocess
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."""
with open(config_file, 'r') as f:
return yaml.safe_load(f)
def check_prerequisites():
"""Check if required tools are installed."""
required_tools = ['hcloud', 'jq']
for tool in required_tools:
if not shutil.which(tool):
print(f"Error: {tool} not found. Please install it first.")
sys.exit(1)
def check_hetzner_token():
"""Check if HCLOUD_TOKEN is set."""
if not os.environ.get('HCLOUD_TOKEN'):
print("Error: HCLOUD_TOKEN environment variable not set")
sys.exit(1)
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."""
cmd = [
'hcloud', 'image', 'list',
'--type=snapshot',
'--selector=name=fedora-coreos-nullpoint',
'--output', 'json'
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
images = json.loads(result.stdout)
if not images:
print("Error: Base image not found. Run build.py first.")
sys.exit(1)
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']
# Generate cloud-init config for this specific server
cloud_init = {
'hostname': hostname,
'timezone': 'UTC',
'users': [
{
'name': 'admin',
'groups': ['wheel'],
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
'ssh_authorized_keys': [config['admin_ssh_key']]
}
],
'package_update': True,
'package_upgrade': True
}
# 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(f"\nServer '{hostname}' created successfully!")
print("The LUKS passphrase has been saved to /root/luks-passphrase.txt on the server.")
print("Please save this passphrase securely - it will be needed if TPM+Tang unlock fails.")
print("\nYou can connect using: ssh admin@<server-ip>")
def get_server_ip(hostname):
"""Get the server's IP address."""
cmd = ['hcloud', 'server', 'ip', hostname]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout.strip()
def main():
"""Main entry point."""
# Load config
if not os.path.exists('deploy-config.yaml'):
print("Error: deploy-config.yaml not found")
sys.exit(1)
config = load_config('deploy-config.yaml')
# Check prerequisites
check_prerequisites()
check_hetzner_token()
# Generate hostname
hostname = generate_hostname()
# Get image ID
image_id = get_image_id()
# Create server
print(f"Creating server '{hostname}'...")
create_server(config, hostname, image_id)
# Get server IP
server_ip = get_server_ip(hostname)
print(f"Server created! IP: {server_ip}")
print(f"You can connect using: ssh core@{server_ip}")
if __name__ == '__main__':
main()

View File

@ -1,413 +0,0 @@
#!/bin/bash
BANNER=$(cat << "EOF"
:^7J5GB##&&##GPY?~:
^75B&@@@@@@&&&@@@@@@@#GJ~:
5&@@@&B5?7~^^^^^~!7YP#@@@@#!
Y##P7^ :~JB#B!
:: :
7PP?: :^~!!~^: :?PP7
:B@@B: !5B&@@@@&B5! :#@@B:
:!!: ^G@@@&BPPB@@@@G^ :!!:
:B@@@5^ ^5@@@B:
:7J7: !@@@# :&@@@~ :?J7:
J@@@5 :#@@@Y: :Y@@@B: 5@@@J
!@@@&^ ~B@@@&G55G&@@@B~ ~&@@@~
5@@@G: :7P#@@@@@@#P7: :B@@@Y
:P@@@B~ :~!77!~: ~B@@@P
Y@@@&Y^ ^5@@@@J
!G@@@&P7^ ^7P&@@@G~
!P&@@@&B? :: ?B&@@@&P!
^75#&&Y :P&&5: 5&&B57^
:^^ :P&&5: ^^:
^^
[nullpoint]
EOF
)
TANG_SERVERS=(
# "https://tang1.example.com your-thumbprint-1"
# "https://tang2.example.com your-thumbprint-2"
)
TPM_PCR_BANK="sha256"
TPM_PCR_IDS="0,1,2,3,4,5,6,7,8"
FEDORA_VERSION="42"
FEDORA_USER="null"
ENABLE_MOTD=true
SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkoTn2NreAXMriOUqzyj3YoFW6jMo9B5B+3R5k8yrMi dodox@dodox-ProArt"
########################################################
# Config End
########################################################
set -euo pipefail
alias c="clear"
alias cls="clear"
clear
echo -e "\n$BANNER"
echo -e "\n[+] Starting installation..."
# Check for TPM
echo "[+] Checking for TPM..."
if [ ! -d "/sys/class/tpm/tpm0" ]; then
echo "WARNING: No TPM detected!"
echo "This system will not be able to use TPM-based boot verification."
echo "You might need to enable TPM in the BIOS. (On Hetzner make a support ticket for KVM access)"
read -p "Continue without TPM? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation aborted."
exit 1
fi
echo "Proceeding without TPM..."
TPM_ENABLED=false
else
echo "TPM detected."
TPM_ENABLED=true
fi
# Check for SSH key
if [ -z "${SSH_KEY:-}" ]; then
echo "No SSH key provided. Please enter your public SSH key:"
read -r SSH_KEY
if [ -z "$SSH_KEY" ]; then
echo "Error: SSH key is required for installation"
exit 1
fi
fi
# Generate secure LUKS passphrase
echo "[+] Generating secure LUKS passphrase..."
LUKS_PASSPHRASE=$(openssl rand -base64 30)
echo "----------------------------------------"
echo "Generated LUKS passphrase:"
echo "${LUKS_PASSPHRASE}"
echo "----------------------------------------"
echo "Please save these credentials securely. You will need them for recovery."
echo "Press Enter to continue..."
read
# Install required packages
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y genisoimage grub-efi-amd64-bin util-linux kexec-tools
# Detect disk naming scheme and set variables
echo "[+] Detecting disk configuration..."
DISKS=($(lsblk -d -n -o NAME | grep -E '^(sd[a-z]|nvme[0-9]+n[0-9]+)$' | sort))
if [ ${#DISKS[@]} -lt 1 ]; then
echo "Error: Expected at least 1 disk, found ${#DISKS[@]}"
exit 1
fi
DISK="/dev/${DISKS[0]}"
# Create a small partition for installer files
echo "[+] Creating installer partition..."
parted -s $DISK mklabel gpt
parted -s $DISK mkpart primary ext4 0% 2GB
parted -s $DISK name 1 installer
mkfs.ext4 -L installer ${DISK}1
mkdir -p /mnt/installer
mount ${DISK}1 /mnt/installer
# Download Fedora installer
echo "[+] Downloading Fedora installer..."
wget -O /mnt/installer/Fedora-Server-netinst-x86_64-${FEDORA_VERSION}-1.1.iso "https://download.fedoraproject.org/pub/fedora/linux/releases/${FEDORA_VERSION}/Server/x86_64/iso/Fedora-Server-netinst-x86_64-${FEDORA_VERSION}-1.1.iso"
# Mount Fedora ISO
echo "[+] Mounting Fedora installer..."
mkdir -p /mnt/iso
mount -o loop /mnt/installer/Fedora-Server-netinst-x86_64-${FEDORA_VERSION}-1.1.iso /mnt/iso
# Get current IP address and gateway from first non-loopback interface
echo "[+] Detecting current IP address and gateway..."
INTERFACE=$(ip -o -4 route show to default | awk '{print $5}' | head -n1)
IPV4=$(ip -4 addr show $INTERFACE | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}')
GATEWAY=$(ip route show default | awk '/default/ {print $3}')
echo "[+] Detected network configuration:"
echo "Interface: $INTERFACE"
echo "IP: $IPV4"
echo "Gateway: $GATEWAY"
# Create kickstart file
echo "[+] Creating kickstart configuration..."
cat > /mnt/installer/ks.cfg << 'KICKSTART'
# Fedora Server installation with our secure setup
text
lang en_US.UTF-8
keyboard us
timezone --utc Etc/UTC
# Security settings
selinux --enforcing
rootpw --lock
user --name=${FEDORA_USER} --groups=wheel --shell=/bin/bash --lock
# SSH setup
sshkey --username=${FEDORA_USER} "${SSH_KEY}"
# Network - let installer detect interface
network --bootproto=static --ip=${IPV4} --netmask=255.255.255.255 --gateway=${GATEWAY} --nameserver=185.12.64.1 --nameserver=185.12.64.2 --activate
# Bootloader
bootloader --timeout=1 --location=mbr --append="no_timer_check console=tty1 console=ttyS0,115200n8"
# Services
services --enabled=sshd,clevis-luks-askpass,dropbear
# Use existing partitions
part /boot/efi --fstype=vfat --onpart=${PART1}
part /boot --fstype=ext4 --onpart=/dev/md0
part / --fstype=btrfs --onpart=/dev/mapper/root_a
# Packages
%packages
@^server-product-environment
clevis
clevis-luks
clevis-tang
clevis-tpm2
tpm2-tools
tpm2-tss
cryptsetup
btrfs-progs
mdadm
dropbear
git
zsh
lsd
bat
tmux
neovim
fortune-mod
cowsay
lolcat
xclip
python3-pip
%end
# Pre-installation
%pre
#!/bin/bash
# Use the LUKS passphrase from rescue script
LUKS_PASSPHRASE="${LUKS_PASSPHRASE}"
echo "\$LUKS_PASSPHRASE" > /tmp/luks.key
chmod 600 /tmp/luks.key
# Detect disk naming scheme and set variables
DISKS=($(lsblk -d -n -o NAME | grep -E '^(sd[a-z]|nvme[0-9]+n[0-9]+)$' | sort))
if [ ${#DISKS[@]} -ne 2 ]; then
echo "Error: Expected exactly 2 disks, found ${#DISKS[@]}"
exit 1
fi
# Set disk variables
DISK1="/dev/${DISKS[0]}"
DISK2="/dev/${DISKS[1]}"
# Stop any existing RAID arrays
mdadm --stop /dev/md0 2>/dev/null || true
# Unmount any existing partitions
for disk in $DISK1 $DISK2; do
umount -f $disk* 2>/dev/null || true
done
# Stop any device mapper devices
dmsetup remove_all 2>/dev/null || true
# Disconnect NVMe devices if present
if [[ "$DISK1" =~ nvme ]] || [[ "$DISK2" =~ nvme ]]; then
nvme disconnect-all
sleep 2
fi
# Zero out partition tables
for disk in $DISK1 $DISK2; do
if [[ "$disk" =~ nvme ]]; then
blkdiscard -f $disk
else
dd if=/dev/zero of=$disk bs=1M count=2 conv=fsync
dd if=/dev/zero of=$disk bs=1M seek=$(($(blockdev --getsz $disk) / 2048 - 2)) count=2 conv=fsync
fi
sync
done
# Create partitions
for disk in $DISK1 $DISK2; do
parted -s $disk mklabel gpt
parted -s $disk mkpart primary fat32 0% 512MB
parted -s $disk mkpart primary ext4 512MB 1.5GB
parted -s $disk mkpart primary ext4 1.5GB 100%
parted -s $disk set 1 boot on
sync
done
# For NVMe disks, we need to append 'p' to partition numbers
if [[ "$DISK1" =~ nvme ]]; then
PART1="${DISK1}p1"
PART2="${DISK1}p2"
PART3="${DISK1}p3"
PART4="${DISK2}p1"
PART5="${DISK2}p2"
PART6="${DISK2}p3"
else
PART1="${DISK1}1"
PART2="${DISK1}2"
PART3="${DISK1}3"
PART4="${DISK2}1"
PART5="${DISK2}2"
PART6="${DISK2}3"
fi
# Create EFI partitions
mkfs.vfat -F 32 $PART1
mkfs.vfat -F 32 $PART4
# Create boot RAID1
mdadm --create /dev/md0 --level=1 --raid-devices=2 --metadata=0.90 --force --run $PART2 $PART5
mkfs.ext4 /dev/md0
# Create LUKS volumes
echo "$LUKS_PASSPHRASE" | cryptsetup luksFormat $PART3 --type luks2
echo "$LUKS_PASSPHRASE" | cryptsetup luksFormat $PART6 --type luks2
# Open LUKS volumes
echo "$LUKS_PASSPHRASE" | cryptsetup luksOpen $PART3 root_a
echo "$LUKS_PASSPHRASE" | cryptsetup luksOpen $PART6 root_b
# Create BTRFS RAID1
mkfs.btrfs -f -d raid1 -m raid1 /dev/mapper/root_a /dev/mapper/root_b
# Create subvolumes
mount /dev/mapper/root_a /mnt/sysimage
btrfs subvolume create /mnt/sysimage/@root
btrfs subvolume create /mnt/sysimage/@home
btrfs subvolume create /mnt/sysimage/@db
chattr +C /mnt/sysimage/@db
# Configure Clevis
if [ ${#TANG_SERVERS[@]} -gt 0 ] || [ "$TPM_ENABLED" = true ]; then
mkdir -p /mnt/sysimage/etc/clevis
# Build Tang servers JSON array if we have any
if [ ${#TANG_SERVERS[@]} -gt 0 ]; then
TANG_JSON="["
for server in "${TANG_SERVERS[@]}"; do
read -r url thumbprint <<< "$server"
TANG_JSON+="{\"url\":\"$url\",\"thumbprint\":\"$thumbprint\"},"
done
TANG_JSON="${TANG_JSON%,}]" # Remove trailing comma and close array
fi
# Calculate t value based on enabled methods
T_VALUE=1
if [ ${#TANG_SERVERS[@]} -gt 0 ] && [ "$TPM_ENABLED" = true ]; then
T_VALUE=2
fi
# Create Clevis config
cat > /mnt/sysimage/etc/clevis/clevis.conf << EOF
{
"t": $T_VALUE,
"pins": {
$([ "$TPM_ENABLED" = true ] && echo "\"tpm2\": {
\"pcr_bank\": \"$TPM_PCR_BANK\",
\"pcr_ids\": \"$TPM_PCR_IDS\"
},")
$([ ${#TANG_SERVERS[@]} -gt 0 ] && echo "\"tang\": {
\"t\": 1,
\"tang\": $TANG_JSON
}")
}
}
EOF
# Bind LUKS volumes
clevis luks bind -d $PART3 sss -c /mnt/sysimage/etc/clevis/clevis.conf
clevis luks bind -d $PART6 sss -c /mnt/sysimage/etc/clevis/clevis.conf
fi
# Cleanup
umount /mnt/sysimage
%end
# Post-installation
%post
# Configure network with static IP (Hetzner dedicated server style)
cat > /etc/sysconfig/network-scripts/ifcfg-ens3 << EOF
DEVICE=ens3
ONBOOT=yes
BOOTPROTO=static
IPADDR=$IPV4
NETMASK=255.255.255.255
SCOPE="peer $GATEWAY"
EOF
# Create route file
cat > /etc/sysconfig/network-scripts/route-ens3 << EOF
ADDRESS0=0.0.0.0
NETMASK0=0.0.0.0
GATEWAY0=$GATEWAY
EOF
# Reload network configuration
nmcli con reload || true
nmcli con up ens3 || true
# Update fstab
cat > /etc/fstab << "FSTAB"
${PART1} /boot/efi vfat defaults 1 2
/dev/md0 /boot ext4 defaults 1 2
/dev/mapper/root_a / btrfs subvol=@root,defaults,noatime 0 0
/dev/mapper/root_a /home btrfs subvol=@home,defaults,noatime 0 0
/dev/mapper/root_a /db btrfs subvol=@db,defaults,noatime,nodatacow 0 0
FSTAB
# Configure dropbear for early SSH access
mkdir -p /etc/dracut.conf.d
cat > /etc/dracut.conf.d/dropbear.conf << "DROPBEAR"
add_drivers+=" dropbear "
install_optional_items=yes
DROPBEAR
# Add SSH key to dropbear
mkdir -p /etc/dropbear
echo "$SSH_KEY" > /etc/dropbear/authorized_keys
chmod 600 /etc/dropbear/authorized_keys
# Regenerate initramfs with dropbear
dracut -f
# Set up MOTD
if [ "$ENABLE_MOTD" = true ]; then
cat > /etc/motd << "MOTD"
$BANNER
MOTD
fi
# Enable required services
systemctl enable clevis-luks-askpass
systemctl enable dropbear
# Force SELinux relabel on next boot
touch /.autorelabel
%end
KICKSTART
# Get actual ISO label
ISO_LABEL=$(isoinfo -d -i /mnt/installer/Fedora-Server-netinst-x86_64-${FEDORA_VERSION}-1.1.iso | grep "Volume id:" | cut -d: -f2 | tr -d ' ')
# Set IP-related kernel boot params for installer
KERNEL_NET_PARAMS="ip=$IPV4::$GATEWAY:255.255.255.255::ens3:none nameserver=185.12.64.1 nameserver=185.12.64.2"
echo "[+] Configuration complete. Starting installation with kexec..."
echo "----------------------------------------"
cat /mnt/installer/ks.cfg
echo "----------------------------------------"
read -p "Press Enter to continue..."
kexec -l /mnt/iso/images/pxeboot/vmlinuz --initrd=/mnt/iso/images/pxeboot/initrd.img --append="inst.ks=file:///dev/disk/by-label/installer/ks.cfg inst.stage2=hd:LABEL=${ISO_LABEL} ${KERNEL_NET_PARAMS} inst.sshd inst.ssh.port=2222 inst.ssh.key=${SSH_KEY}"
kexec -e

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pyyaml>=6.0.1