commit dbb7983283dddea6203d1470fff54e02df5744d6 Author: Dominik Roth Date: Tue May 13 14:12:54 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43bb5f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Config files +build-config.yaml +deploy-config.yaml +config.ign + +# Python +__pycache__/ +*.py[cod] +*$py.class + +# Downloaded images +fedora-coreos-*-hetzner.x86_64.raw.xz + +# Environment +.env \ No newline at end of file diff --git a/MASTER_README.md b/MASTER_README.md new file mode 100644 index 0000000..fec8ca1 --- /dev/null +++ b/MASTER_README.md @@ -0,0 +1,96 @@ +# Tang Server Setup + +Tang server for remote LUKS unlock. Runs on-premise with logging for future approval system integration. + +## Quick Setup + +```bash +# Install Tang +# Fedora/CentOS: +sudo dnf install tang +# Ubuntu: +sudo apt install tang + +# Enable and start Tang service +sudo systemctl enable tangd.socket +sudo systemctl start tangd.socket + +# Generate keys +sudo mkdir -p /var/db/tang +sudo tangd-keygen /var/db/tang + +# Get thumbprint for Ignition config +sudo tang-show-keys /var/db/tang +``` + +## Security + +### Connection Security +- Tang uses HTTPS for all connections +- Each connection is encrypted end-to-end +- Tang verifies client identity through challenge-response +- Client verifies Tang's identity through signed advertisements + +### Request Logging +To log all unlock requests (for future approval system): + +1. Create a wrapper script: +```bash +#!/bin/bash +# /usr/local/bin/tangd-wrapper + +# Get client info +CLIENT_IP="$SOCAT_PEERADDR" +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + +echo "$TIMESTAMP: Unlock request from $CLIENT_IP" >> /var/log/tang-requests.log +wall "Tang unlock request from $CLIENT_IP at $TIMESTAMP" # Notify all TTYs +exec /usr/libexec/tangd "$@" +echo "$TIMESTAMP: Request auto-approved" >> /var/log/tang-requests.log +``` + +2. Make it executable: +```bash +sudo chmod +x /usr/local/bin/tangd-wrapper +``` + +3. Configure systemd to use the wrapper: +```bash +# Create override directory +sudo mkdir -p /etc/systemd/system/tangd.socket.d/ + +# Create override file +sudo tee /etc/systemd/system/tangd.socket.d/override.conf << EOF +[Socket] +ExecStart= +ExecStart=/usr/local/bin/tangd-wrapper +EOF + +# Reload and restart +sudo systemctl daemon-reload +sudo systemctl restart tangd.socket +``` + +Now when a server requests an unlock: +1. A message appears on all TTYs (including SSH sessions) +2. The request is logged to `/var/log/tang-requests.log` +3. The request is automatically approved +4. All actions are logged with timestamps + +Future integration points: +- Add webhook support to notify Slack/Discord +- Add approval via web interface +- Add rate limiting +- Add client whitelisting + +## Backup +```bash +# Backup keys +sudo tar -czf tang-keys-$(date +%Y%m%d).tar.gz /var/db/tang/ +``` + +## Recovery +If keys are lost: +1. Generate new keys +2. Update all client configurations +3. Re-encrypt all client systems \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..47e3fce --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Project Nullpoint + +Secure Fedora CoreOS server setup with LUKS encryption, TPM, and BTRFS RAID1. + +## Features + +- Fedora CoreOS base +- Full disk encryption with LUKS +- Remote unlock via Tang server +- TPM-based boot verification +- BTRFS RAID1 storage +- Secure Boot with custom kernel signing + +## 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 + +### TPM Integration +- TPM2 chip verifies boot integrity +- PCR measurements ensure system hasn't been tampered with +- Combined with Tang for defense in depth + +## Repository Structure +``` +. +├── deploy.sh # Deployment script for Hetzner +├── generate_ignition.py # Python-based Ignition config generator +├── MASTER_README.md # Tang server setup documentation +├── README.md # Main project documentation +├── requirements.txt # Python dependencies +└── settings.yaml # Configuration settings +``` + +## 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 +dnf install jq coreos-installer python3-pyyaml + +# Configure Hetzner +export HCLOUD_TOKEN="your-token-here" +hcloud ssh-key create --name "fedora-coreos-hetzner" --public-key "$(cat ~/.ssh/id_ed25519.pub)" +``` + +## Setup + +1. **Configure Build Settings** + ```bash + cp build-config.yaml.example build-config.yaml + vim build-config.yaml # Edit LUKS, storage, and image settings + ``` + +2. **Build Base Image** (one-time setup) + ```bash + python3 build.py # Creates and uploads FCOS 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 core@your-server + systemctl status clevis-luks-askpass + lsblk + clevis-luks-list -d /dev/sda2 + ``` \ No newline at end of file diff --git a/build-config.yaml.example b/build-config.yaml.example new file mode 100644 index 0000000..908885d --- /dev/null +++ b/build-config.yaml.example @@ -0,0 +1,25 @@ +# Build Configuration +image: + name: fedora-coreos-nullpoint + stream: stable + arch: x86_64 + hetzner_arch: x86 + +# System Configuration +system: + # LUKS Configuration + luks: + tang_url: https://tang.example.com + tang_thumbprint: your-tang-thumbprint + + # Storage Configuration + storage: + boot_size_mib: 512 + compression: zstd + subvolumes: + - name: "@" + path: "/" + - name: "@home" + path: "/home" + - name: "@var" + path: "/var" \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..dccf09d --- /dev/null +++ b/build.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 + +import os +import sys +import yaml +import subprocess +import json +from pathlib import Path + +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', 'coreos-installer'] + 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_ignition_config(config): + """Generate Ignition configuration.""" + system = config['system'] + luks = system['luks'] + storage = system['storage'] + + # 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" + } + ] + } + } + + # 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.""" + image = config['image'] + cmd = [ + 'coreos-installer', 'download', + '-s', image['stream'], + '-p', 'hetzner', + '-a', image['arch'], + '-f', 'raw.xz' + ] + 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): + """Create new snapshot from image.""" + image = config['image'] + cmd = [ + 'hcloud-upload-image', 'upload', + '--architecture', image['hetzner_arch'], + '--compression', 'xz', + '--image-path', str(image_file), + '--name', image['name'], + '--labels', f'os=fedora-coreos,channel={image["stream"]}', + '--description', f'Fedora CoreOS ({image["stream"]}, {image["arch"]}) 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() + + # 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 FCOS image + print("Downloading FCOS image...") + download_fcos_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) + + print("Image build complete!") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/deploy-config.yaml.example b/deploy-config.yaml.example new file mode 100644 index 0000000..b9a534d --- /dev/null +++ b/deploy-config.yaml.example @@ -0,0 +1,10 @@ +# Deployment Configuration +hetzner: + datacenter: nbg1 + server_type: cx31 + ssh_key_name: fedora-coreos-hetzner + +# Hostname Configuration +hostname: + prefix: nullpoint + format: "{prefix}-{date}-{random}" # date format: YYMMDD, random: 000-999 \ No newline at end of file diff --git a/deploy.py b/deploy.py new file mode 100644 index 0000000..a556cad --- /dev/null +++ b/deploy.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +import os +import sys +import yaml +import subprocess +import json +import shutil +from datetime import datetime +import random + +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(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 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 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) + +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(config) + + # 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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1321e71 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyyaml>=6.0.1 \ No newline at end of file