Initial commit

This commit is contained in:
Dominik Moritz Roth 2025-05-13 14:12:54 +02:00
commit dbb7983283
8 changed files with 573 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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

96
MASTER_README.md Normal file
View File

@ -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

82
README.md Normal file
View File

@ -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
```

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

@ -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"

240
build.py Normal file
View File

@ -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()

View File

@ -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

104
deploy.py Normal file
View File

@ -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()

1
requirements.txt Normal file
View File

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