Compare commits
No commits in common. "master" and "legacy" have entirely different histories.
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,12 +1,15 @@
|
|||||||
# Config files
|
# Config files
|
||||||
config.yaml
|
build-config.yaml
|
||||||
|
deploy-config.yaml
|
||||||
|
config.ign
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
# Images
|
|
||||||
*.iso
|
# Downloaded images
|
||||||
|
fedora-coreos-*-hetzner.x86_64.raw.xz
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
83
README.md
83
README.md
@ -4,10 +4,7 @@
|
|||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 for (Hetzner) Dedicated Servers.
|
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 with focus on Hetzner Infra.
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This project is still WIP, having some issues with networking of the installeer / installed instance.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -17,7 +14,7 @@ Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 for (Hetzn
|
|||||||
- TPM-based boot verification
|
- TPM-based boot verification
|
||||||
- BTRFS RAID1 for data redundancy
|
- BTRFS RAID1 for data redundancy
|
||||||
- Dedicated database subvolume with `nodatacow` and `noatime`
|
- 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).
|
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
|
### TPM Updates
|
||||||
After firmware updates (UEFI/BIOS), the TPM bindings need to be updated:
|
After firmware updates (UEFI/BIOS), the TPM bindings need to be updated:
|
||||||
(otherwise the system will not be able to boot without recovery phrase)
|
(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:
|
2. The script will:
|
||||||
- Show current PCR values
|
- Show current PCR values
|
||||||
- Update TPM bindings to match new measurements
|
- Update TPM bindings to match new measurements
|
||||||
- Verify all bindings are correct
|
- Verify all bindings are correct
|
||||||
3. Manual passphrase is available in `/root/luks-passphrase.txt` if needed
|
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
|
## Setup
|
||||||
|
|
||||||
1. **Configure Installer**
|
1. **Configure Build Settings**
|
||||||
```bash
|
```bash
|
||||||
# Edit the variables at the top of install.sh:
|
cp build-config.yaml.example build-config.yaml
|
||||||
vim install.sh
|
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**
|
2. **Build Base Image** (one-time setup)
|
||||||
- Log into Hetzner Robot
|
|
||||||
- Select your server
|
|
||||||
- Go to "Rescue" tab
|
|
||||||
- Choose "Linux" and "64 bit"
|
|
||||||
- Activate Rescue System
|
|
||||||
- Upload the installer:
|
|
||||||
```bash
|
```bash
|
||||||
scp install.sh root@your-server:/root/
|
python3 build.py # Creates and uploads Fedora Server image to Hetzner
|
||||||
```
|
```
|
||||||
- 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**
|
3. **Configure Deployment Settings**
|
||||||
```bash
|
```bash
|
||||||
ssh null@your-server
|
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
|
systemctl status clevis-luks-askpass
|
||||||
lsblk
|
lsblk
|
||||||
btrfs filesystem show # Check RAID1 status
|
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
43
build-config.yaml.example
Normal 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
547
build.py
Normal 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()
|
13
deploy-config.yaml.example
Normal file
13
deploy-config.yaml.example
Normal 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
169
deploy.py
Normal 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()
|
413
install.sh
413
install.sh
@ -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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pyyaml>=6.0.1
|
Loading…
Reference in New Issue
Block a user