Initial commit
This commit is contained in:
commit
dbb7983283
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal 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
96
MASTER_README.md
Normal 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
82
README.md
Normal 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
25
build-config.yaml.example
Normal 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
240
build.py
Normal 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()
|
10
deploy-config.yaml.example
Normal file
10
deploy-config.yaml.example
Normal 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
104
deploy.py
Normal 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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
pyyaml>=6.0.1
|
Loading…
Reference in New Issue
Block a user