Compare commits

..

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

17 changed files with 842 additions and 3689 deletions

9
.gitignore vendored
View File

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

114
README.md
View File

@ -4,73 +4,85 @@
<br>
</div>
Secure AlmaLinux (RHEL) Server setup with LUKS encryption, Tang, TPM and RAID1 for Hetzner Dedicated Servers.
Secure Fedora Server setup with LUKS encryption, TPM, and BTRFS RAID1 with focus on Hetzner Infra.
## Features
- AlmaLinux Server base
- Fedora Server base
- Full disk encryption with LUKS
- Remote unlock via Tang server
- TPM-based boot verification
- mdadm RAID1 + XFS (RHEL standard)
- SSH key-only access with early boot SSH via dropbear
- Best-in-class terminal: zsh + powerlevel10k + evil tmux
- BTRFS RAID1 for data redundancy
- Dedicated database subvolume with `nodatacow` and `noatime`
- Automated deployment to Hetzner
## Unlock Strategy
If you need a dead man's switch to go along with it check out [raven](https://git.dominik-roth.eu/dodox/raven).
1. **Automatic unlock via Tang/TPM** (default):
- Configure TPM2 and/or Tang servers in post-install.sh
- System unlocks automatically if conditions are met
- No manual intervention required
## Security Model
2. **Manual unlock via SSH** (fallback):
- SSH to server on port 22 (dropbear in early boot)
- Enter LUKS passphrase when prompted (twice, once per disk)
- Used when automatic unlock fails or is not configured
### Unlock Methods
The system uses multiple methods to unlock the LUKS volumes:
1. **Primary Method**: TPM2 + Tang server
- TPM2 verifies boot integrity
- Tang server provides remote unlock capability
- Both must succeed for automatic unlock
2. **Fallback Method**: Manual passphrase
- Available via SSH before LUKS unlock
- Uses dropbear for early SSH access
- Can be used for recovery or maintenance
## Install
### TPM Updates
After firmware updates (UEFI/BIOS), the TPM bindings need to be updated:
(otherwise the system will not be able to boot without recovery phrase)
1. Use the provided script: `sudo /root/update-tpm-bindings.sh`
2. The script will:
- Show current PCR values
- Update TPM bindings to match new measurements
- Verify all bindings are correct
3. Manual passphrase is available in `/root/luks-passphrase.txt` if needed
Boot your Hetzner server into rescue mode and run:
## Prerequisites
```bash
wget -qO- https://git.dominik-roth.eu/dodox/nullpoint/raw/branch/master/get.sh | 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)"
```
The installer will:
- Detect your SSH key from the current session
- Ask for hostname and username
- Generate a secure LUKS passphrase (SAVE IT!)
- Download and configure everything
- Run Hetzner's installimage automatically
---
<div align="center">
<img src='./icon_cluster.svg' width="150px">
<h2>nullpoint cluster</h2>
<br>
</div>
Encrypted network and storage pool using [Nebula](https://github.com/slackhq/nebula) mesh VPN and [GlusterFS](https://www.gluster.org/) distributed filesystem.
## Features
- **Encrypted mesh network** - All traffic encrypted via Nebula overlay (192.168.100.0/24)
- **Distributed storage** - Data replicated across all storage nodes
- **Simple joining** - Single preshared secret + lighthouse endpoint
- **Flexible nodes** - Full nodes (replicate data) or remote nodes (no storage)
## Setup
```bash
wget -qO- https://git.dominik-roth.eu/dodox/nullpoint/raw/branch/master/cluster-setup.sh | sudo bash
```
1. **Configure Build Settings**
```bash
cp build-config.yaml.example build-config.yaml
vim build-config.yaml # Edit LUKS, storage, and image settings
```
Choose your node type:
- **Full node** - Runs GlusterFS server, contributes storage, acts as lighthouse
- Use for servers in same datacenter/region with low latency
- **Remote node** - GlusterFS client only, no storage contribution
- Use for edge locations, different regions, or high-latency connections
- Avoids replication delays since writes don't wait for this node
2. **Build Base Image** (one-time setup)
```bash
python3 build.py # Creates and uploads Fedora Server image to Hetzner
```
Storage mounted at `/data/storage/` on all nodes.
3. **Configure Deployment Settings**
```bash
cp deploy-config.yaml.example deploy-config.yaml
vim deploy-config.yaml # Edit server type, location, and hostname settings
```
4. **Deploy Server**
```bash
python3 deploy.py # Creates new server from base image
```
5. **Verify**
```bash
ssh admin@your-server
systemctl status clevis-luks-askpass
lsblk
btrfs filesystem show # Check RAID1 status
clevis-luks-list -d /dev/sda2
```

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

@ -0,0 +1,43 @@
# Build Configuration
image:
name: nullpoint
version: 39
arch: x86_64
hetzner_arch: x86
# System Configuration
system:
# LUKS Configuration
luks:
tang_servers:
- url: https://tang1.example.com
thumbprint: your-tang1-thumbprint
- url: https://tang2.example.com
thumbprint: your-tang2-thumbprint
# TPM Configuration
tpm:
pcr_bank: sha256 # PCR bank to use (sha1 or sha256)
pcr_ids: [0,4,7,8,9] # PCRs to measure
# PCR descriptions:
# 0: Core System Firmware executable code (BIOS/UEFI) (RECOMMENDED)
# 1: Core System Firmware data (BIOS/UEFI settings)
# 2: Extended or pluggable executable code
# 3: Extended or pluggable firmware data
# 4: Boot Manager Code (bootloader) (RECOMMENDED)
# 5: Boot Manager Configuration and Data
# 6: Platform-specific code
# 7: Platform-specific configuration (RECOMMENDED)
# 8: UEFI driver and application code (RECOMMENDED)
# 9: UEFI driver and application configuration (RECOMMENDED)
# 10: UEFI Handoff Tables
# 11: UEFI Boot Services Code
# 12: UEFI Boot Services Data
# 13: UEFI Runtime Services Code
# 14: UEFI Runtime Services Data
# 15: UEFI Secure Boot State
# System Settings
timezone: UTC
keyboard: us
language: en_US.UTF-8

547
build.py Normal file
View File

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

View File

@ -1,564 +0,0 @@
#!/bin/bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
NEBULA_NETWORK="192.168.100.0/24"
NEBULA_PORT=4242
NEBULA_CONFIG="/etc/nebula"
GLUSTER_BRICK_PATH="/gluster/cluster"
GLUSTER_MOUNT_PATH="/data/storage"
GLUSTER_VOLUME="cluster-volume"
NEBULA_VERSION="v1.9.6"
CLUSTER_LOGO=$(cat << "EOF"
==
.@@@@@
=@@@@@%
+@@@@@
:@@@@@
*@@@@: -*#+:
*@@@- %@@@@ .%@@@@@@@@@@@@*
@@@@@ %@@@@ *@@@@@@@@@@@@@@@@
+@@@@* #@@@@. %@@@@@@@.
@@@@@@ -@@@@%:@@@@@@:
*@@@@@@. #@@@@@@@@@:
.@@@@@@@@%=.#@@@@@@@@@@@@@#.
*@@@@@@@@@@@@@@@@@@@@@@@@@@+
=@@@@@@@@@@@@@@%=#@@@@@@@@%
:@@@@@@@@@+ -@@@@@@+
*@@@@@-%@@@@: .@@@@@#
=@@@@@@@ .@@@@# #@@@@+
%@@@@@@@@@@@@@@@@. @@@@@ @@@@@
#@@@@@@@@@@@@@= @@@@@ =@@@%
:#@@@@- .@@@@#
#@@@@-
#@@@@%
+@@@@@*
@@@@@-
.#%.
[nullpoint cluster]
EOF
)
echo -e "${GREEN}${CLUSTER_LOGO}${NC}\n"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root${NC}"
exit 1
fi
# Install base packages
echo -e "${YELLOW}[+] Installing base packages...${NC}"
dnf install -y curl tar || exit 1
# Download and install Nebula
echo -e "${YELLOW}[+] Downloading Nebula ${NEBULA_VERSION}...${NC}"
cd /tmp
curl -LO "https://github.com/slackhq/nebula/releases/download/${NEBULA_VERSION}/nebula-linux-amd64.tar.gz"
tar -zxf nebula-linux-amd64.tar.gz
mv nebula nebula-cert /usr/local/bin/
chmod +x /usr/local/bin/nebula /usr/local/bin/nebula-cert
# Create directories
echo -e "${YELLOW}[+] Creating directories...${NC}"
mkdir -p "$GLUSTER_MOUNT_PATH"
mkdir -p /data
mkdir -p "$NEBULA_CONFIG"
# Function to generate Nebula CA and certificates
generate_nebula_ca() {
echo -e "${YELLOW}[+] Generating Nebula CA...${NC}"
cd "$NEBULA_CONFIG"
/usr/local/bin/nebula-cert ca -name "Nullpoint Cluster CA"
chmod 600 ca.key
}
# Function to create host certificate
create_host_cert() {
local hostname="$1"
local ip="$2"
local groups="$3"
cd "$NEBULA_CONFIG"
/usr/local/bin/nebula-cert sign -name "$hostname" -ip "$ip" -groups "$groups"
chmod 600 "${hostname}.key"
}
# Function to get next available IP
get_next_ip() {
local base_ip="192.168.100"
local next_num=10
if [ -f "$NEBULA_CONFIG/cluster-registry.txt" ]; then
# Find highest IP in use
existing_ips=$(grep -oE "192\.168\.100\.[0-9]+" "$NEBULA_CONFIG/cluster-registry.txt" | cut -d. -f4 | sort -n | tail -1)
if [ ! -z "$existing_ips" ]; then
next_num=$((existing_ips + 1))
fi
fi
echo "${base_ip}.${next_num}"
}
# Function to setup firewall rules
setup_firewall() {
echo -e "${YELLOW}[+] Configuring firewall...${NC}"
# Nebula
firewall-cmd --permanent --add-port=${NEBULA_PORT}/udp
# GlusterFS ports
firewall-cmd --permanent --add-service=glusterfs
firewall-cmd --permanent --add-port=24007-24008/tcp # GlusterFS Daemon
firewall-cmd --permanent --add-port=49152-49200/tcp # Brick ports
# Allow traffic from Nebula network
firewall-cmd --permanent --zone=trusted --add-source=${NEBULA_NETWORK}
firewall-cmd --reload
}
# Create Nebula systemd service
create_nebula_service() {
cat > /etc/systemd/system/nebula.service <<EOF
[Unit]
Description=Nebula overlay networking tool
After=network.target
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/bin/nebula -config ${NEBULA_CONFIG}/config.yaml
ExecReload=/bin/kill -HUP \$MAINPID
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
# Create new cluster
create_cluster() {
echo -e "${GREEN}[*] Creating new cluster...${NC}\n"
local hostname=$(hostname)
local node_ip="192.168.100.1"
# First cluster node must be full node
echo -e "${YELLOW}First cluster node must be a full node (storage provider)${NC}"
# Install GlusterFS server packages
echo -e "${YELLOW}[+] Installing GlusterFS server packages...${NC}"
dnf install -y glusterfs-server || exit 1
systemctl enable glusterd
systemctl start glusterd
# Create brick directory
mkdir -p "$GLUSTER_BRICK_PATH"
# Ask for lighthouse endpoints
echo -e "${YELLOW}Enter lighthouse endpoints (DNS names or IPs).${NC}"
echo -e "${YELLOW}Recommended: Use a DNS name with redundant backing for HA.${NC}"
echo -e "${YELLOW}You can enter multiple endpoints separated by commas.${NC}"
read -p "Lighthouse endpoints (e.g., cluster.example.com or 1.2.3.4,5.6.7.8): " lighthouse_endpoints
if [ -z "$lighthouse_endpoints" ]; then
echo -e "${RED}At least one lighthouse endpoint required!${NC}"
exit 1
fi
am_lighthouse="true"
# Generate Nebula CA
generate_nebula_ca
# Create certificate for this node
create_host_cert "$hostname" "${node_ip}/24" "cluster"
# Create Nebula config
cat > "${NEBULA_CONFIG}/config.yaml" <<EOF
pki:
ca: ${NEBULA_CONFIG}/ca.crt
cert: ${NEBULA_CONFIG}/${hostname}.crt
key: ${NEBULA_CONFIG}/${hostname}.key
lighthouse:
am_lighthouse: ${am_lighthouse}
serve_dns: false
interval: 60
hosts:$(echo "$lighthouse_endpoints" | tr ',' '\n' | while read endpoint; do echo "
- \"${endpoint}:${NEBULA_PORT}\""; done)
listen:
host: 0.0.0.0
port: ${NEBULA_PORT}
punchy:
punch: true
respond: true
tun:
disabled: false
dev: nebula1
drop_local_broadcast: false
drop_multicast: false
tx_queue: 500
mtu: 1300
logging:
level: info
format: text
firewall:
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
max_connections: 100000
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: any
- port: any
proto: any
host: any
EOF
# Start Nebula as systemd service
create_nebula_service
systemctl enable nebula
systemctl start nebula
# Setup firewall
setup_firewall
# Create cluster registry
echo "${node_ip} ${hostname} full $(date)" > "${NEBULA_CONFIG}/cluster-registry.txt"
# Create GlusterFS volume
echo -e "${YELLOW}[+] Creating GlusterFS volume...${NC}"
mkdir -p "${GLUSTER_BRICK_PATH}/brick1"
gluster volume create ${GLUSTER_VOLUME} $(hostname):${GLUSTER_BRICK_PATH}/brick1 force 2>/dev/null || true
gluster volume start ${GLUSTER_VOLUME} 2>/dev/null || true
# Mount volume
mount -t glusterfs localhost:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH}
grep -q "${GLUSTER_VOLUME}" /etc/fstab || echo "localhost:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} glusterfs defaults,_netdev 0 0" >> /etc/fstab
# Package CA certificate for sharing
ca_cert_b64=$(base64 -w0 "${NEBULA_CONFIG}/ca.crt")
echo -e "\n${GREEN}════════════════════════════════════════${NC}"
echo -e "${GREEN}Cluster created successfully!${NC}"
echo -e "${GREEN}════════════════════════════════════════${NC}\n"
echo -e "Share this cluster secret with joining nodes:\n"
echo -e "${GREEN}${lighthouse_endpoints}:${NEBULA_PORT}:${ca_cert_b64}${NC}\n"
echo -e "${YELLOW}Status:${NC}"
echo " - Nebula IP: ${node_ip}"
echo " - Lighthouse endpoints: ${lighthouse_endpoints}:${NEBULA_PORT}"
echo " - This node is lighthouse: ${am_lighthouse}"
echo " - GlusterFS volume: ${GLUSTER_VOLUME}"
echo " - Mount point: ${GLUSTER_MOUNT_PATH}"
}
# Join existing cluster
join_cluster() {
echo -e "${GREEN}[*] Joining existing cluster...${NC}\n"
local hostname=$(hostname)
local my_ip=$(get_next_ip)
# Get cluster details
read -p "Enter cluster secret (lighthouse_endpoints:port:ca_cert_base64): " cluster_secret
if [ -z "$cluster_secret" ]; then
echo -e "${RED}Cluster secret required!${NC}"
exit 1
fi
# Parse secret
lighthouse_endpoints=$(echo "$cluster_secret" | cut -d: -f1)
nebula_port=$(echo "$cluster_secret" | cut -d: -f2)
ca_cert_b64=$(echo "$cluster_secret" | cut -d: -f3-)
# Ask about node type
echo -e "${YELLOW}Select node type:${NC}"
echo " 1) Full node (contributes storage, lighthouse, read/write)"
echo " 2) Remote node (client only, no storage contribution)"
echo -e "${YELLOW}Note: Use remote nodes for locations with high latency to the cluster${NC}"
read -p "Enter choice [1-2]: " node_type
if [ "$node_type" = "2" ]; then
is_remote="true"
am_lighthouse="false"
echo -e "${YELLOW}Configuring as remote node (GlusterFS client only)${NC}"
# Install only GlusterFS client packages
echo -e "${YELLOW}[+] Installing GlusterFS client packages...${NC}"
dnf install -y glusterfs glusterfs-fuse || exit 1
else
is_remote="false"
am_lighthouse="true"
echo -e "${YELLOW}Configuring as full node (GlusterFS server)${NC}"
# Install GlusterFS server packages
echo -e "${YELLOW}[+] Installing GlusterFS server packages...${NC}"
dnf install -y glusterfs-server || exit 1
systemctl enable glusterd
systemctl start glusterd
# Create brick directory for full nodes
mkdir -p "$GLUSTER_BRICK_PATH"
fi
echo -e "${YELLOW}[+] Configuring Nebula (IP: ${my_ip})...${NC}"
# Decode and save CA certificate
echo "$ca_cert_b64" | base64 -d > "${NEBULA_CONFIG}/ca.crt"
echo -e "${RED}WARNING: Certificate signing not implemented in this simplified version.${NC}"
echo -e "${YELLOW}On the lighthouse node, run this command to create a certificate for this node:${NC}"
echo -e "${GREEN}cd ${NEBULA_CONFIG} && /usr/local/bin/nebula-cert sign -name \"${hostname}\" -ip \"${my_ip}/24\" -groups \"cluster\"${NC}"
echo -e "${YELLOW}Then copy ${hostname}.crt and ${hostname}.key to ${NEBULA_CONFIG}/ on this node.${NC}"
read -p "Press enter once you've created and copied the certificate files..."
# Create Nebula config
cat > "${NEBULA_CONFIG}/config.yaml" <<EOF
pki:
ca: ${NEBULA_CONFIG}/ca.crt
cert: ${NEBULA_CONFIG}/${hostname}.crt
key: ${NEBULA_CONFIG}/${hostname}.key
lighthouse:
am_lighthouse: ${am_lighthouse}
interval: 60
hosts:$(echo "$lighthouse_endpoints" | tr ',' '\n' | while read endpoint; do echo "
- \"${endpoint}:${nebula_port}\""; done)
listen:
host: 0.0.0.0
port: ${NEBULA_PORT}
punchy:
punch: true
respond: true
tun:
disabled: false
dev: nebula1
drop_local_broadcast: false
drop_multicast: false
tx_queue: 500
mtu: 1300
logging:
level: info
format: text
firewall:
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
max_connections: 100000
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: any
- port: any
proto: any
host: any
EOF
# Start Nebula
create_nebula_service
systemctl enable nebula
systemctl start nebula
# Setup firewall
setup_firewall
# Wait for Nebula connection
echo -e "${YELLOW}[+] Waiting for Nebula connection...${NC}"
sleep 5
# Test connection - try pinging the first node
echo -e "${YELLOW}[+] Testing Nebula connection...${NC}"
if ping -c 1 -W 3 192.168.100.1 > /dev/null 2>&1; then
echo -e "${GREEN}[✓] Connected to node at 192.168.100.1${NC}"
else
echo -e "${YELLOW}[!] Could not reach 192.168.100.1 - this may be normal if it's the first node${NC}"
fi
# Register with cluster
node_type_str=$([ "$is_remote" = "true" ] && echo "remote" || echo "full")
echo "${my_ip} ${hostname} ${node_type_str} $(date)" >> "${NEBULA_CONFIG}/cluster-registry.txt"
# Handle GlusterFS setup based on node type
if [ "$is_remote" = "true" ]; then
# Remote node - GlusterFS client only
echo -e "${YELLOW}[+] Mounting GlusterFS as client...${NC}"
# Find a full node to connect to (try first few IPs)
mount_successful=false
for ip in 192.168.100.1 192.168.100.2 192.168.100.3; do
if ping -c 1 -W 2 $ip > /dev/null 2>&1; then
echo -e "${YELLOW}Attempting to mount from $ip...${NC}"
if mount -t glusterfs ${ip}:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} 2>/dev/null; then
mount_successful=true
mount_ip=$ip
break
fi
fi
done
if [ "$mount_successful" = "true" ]; then
# Add to fstab
grep -q "${GLUSTER_VOLUME}" /etc/fstab || echo "${mount_ip}:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} glusterfs defaults,_netdev,backup-volfile-servers=192.168.100.1:192.168.100.2:192.168.100.3 0 0" >> /etc/fstab
echo -e "${GREEN}Remote node configured - mounted cluster storage as client${NC}"
else
echo -e "${RED}Failed to mount GlusterFS volume!${NC}"
echo "Make sure at least one full node is running."
fi
else
# Full node - GlusterFS server
echo -e "${YELLOW}[+] Joining GlusterFS cluster as server...${NC}"
# Try to probe existing nodes
echo -e "${YELLOW}[+] Looking for existing GlusterFS peers...${NC}"
peer_found=false
for ip in 192.168.100.1 192.168.100.2 192.168.100.3; do
if [ "$ip" != "${my_ip}" ] && ping -c 1 -W 2 $ip > /dev/null 2>&1; then
if gluster peer probe $ip 2>/dev/null; then
echo "Connected to peer at $ip"
peer_found=true
break
fi
fi
done
if [ "$peer_found" = "false" ]; then
echo -e "${YELLOW}No existing peers found - this might be normal for early nodes${NC}"
fi
# Wait for peer connection
sleep 3
# Create brick directory
mkdir -p "${GLUSTER_BRICK_PATH}/brick1"
if [ "$peer_found" = "true" ]; then
# Add brick to existing volume
echo -e "${YELLOW}[+] Adding brick to GlusterFS volume...${NC}"
# Get current replica count
replica_count=$(gluster volume info ${GLUSTER_VOLUME} 2>/dev/null | grep "Number of Bricks" | grep -oE "[0-9]+" | head -1)
if [ ! -z "$replica_count" ]; then
new_replica_count=$((replica_count + 1))
gluster volume add-brick ${GLUSTER_VOLUME} replica ${new_replica_count} $(hostname):${GLUSTER_BRICK_PATH}/brick1 force
fi
fi
# Mount the volume locally
mount -t glusterfs localhost:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} 2>/dev/null ||
mount -t glusterfs 192.168.100.1:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} 2>/dev/null
# Add to fstab
grep -q "${GLUSTER_VOLUME}" /etc/fstab || echo "localhost:/${GLUSTER_VOLUME} ${GLUSTER_MOUNT_PATH} glusterfs defaults,_netdev 0 0" >> /etc/fstab
echo -e "${GREEN}Full node configured - contributing storage to cluster${NC}"
fi
echo -e "\n${GREEN}════════════════════════════════════════${NC}"
echo -e "${GREEN}Successfully joined cluster!${NC}"
echo -e "${GREEN}════════════════════════════════════════${NC}\n"
echo -e "${YELLOW}Node details:${NC}"
echo " - Nebula IP: ${my_ip}"
echo " - Hostname: ${hostname}"
echo " - Node type: $([ "$is_remote" = "true" ] && echo "Remote (no storage)" || echo "Full (contributes storage)")"
echo " - GlusterFS mounted at: ${GLUSTER_MOUNT_PATH}"
}
# Show cluster status
show_status() {
echo -e "\n${YELLOW}=== Cluster Status ===${NC}\n"
if [ -f "${NEBULA_CONFIG}/config.yaml" ]; then
echo -e "${GREEN}Nebula Status:${NC}"
systemctl is-active nebula && echo "Service: Active" || echo "Service: Inactive"
if ip addr show nebula1 >/dev/null 2>&1; then
echo "Interface: nebula1 $(ip addr show nebula1 | grep 'inet ' | awk '{print $2}')"
else
echo "Interface: Not found"
fi
echo ""
if [ -f "${NEBULA_CONFIG}/cluster-registry.txt" ]; then
echo -e "${GREEN}Cluster Nodes:${NC}"
cat "${NEBULA_CONFIG}/cluster-registry.txt"
echo ""
fi
else
echo -e "${RED}Nebula not configured${NC}\n"
fi
echo -e "${GREEN}GlusterFS Status:${NC}"
gluster peer status 2>/dev/null || echo "Not connected to cluster"
echo ""
gluster volume status ${GLUSTER_VOLUME} 2>/dev/null || echo "Volume ${GLUSTER_VOLUME} not found"
echo ""
echo -e "${GREEN}Mounted at:${NC} ${GLUSTER_MOUNT_PATH}"
df -h ${GLUSTER_MOUNT_PATH} 2>/dev/null || echo "Not mounted"
}
# Main menu
echo "What would you like to do?"
echo " 1) Create new cluster"
echo " 2) Join existing cluster"
echo " 3) Show cluster status"
echo " 4) Exit"
echo ""
read -p "Enter choice [1-4]: " choice
case $choice in
1)
create_cluster
;;
2)
join_cluster
;;
3)
show_status
;;
4)
echo "Exiting..."
exit 0
;;
*)
echo -e "${RED}Invalid choice${NC}"
exit 1
;;
esac

View File

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

169
deploy.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
import os
import sys
import yaml
import subprocess
import json
import shutil
from datetime import datetime
import random
import tempfile
import secrets
import string
# List of adjectives for hostname generation
ADJECTIVES = [
# Scientific/Sci-fi
'quantum', 'atomic', 'plasma', 'fusion', 'ionic', 'magnetic', 'cosmic',
'stellar', 'nebular', 'pulsar', 'quasar', 'warp', 'phaser', 'hyper',
'temporal', 'spatial', 'dimensional', 'subspace', 'transwarp',
# Cool/Interesting
'abysmal', 'adamant', 'aerial', 'arcane', 'astral', 'azure', 'celestial',
'crimson', 'cryptic', 'crystalline', 'dormant', 'eerie', 'eldritch',
'ethereal', 'fractal', 'frozen', 'ghostly', 'gilded', 'singular',
'hollow', 'infernal', 'lunar', 'mystic', 'nebulous', 'obsidian',
'occult', 'prismatic', 'radiant', 'shadow', 'solar', 'spectral',
'stellar', 'sublime', 'titanic', 'twilight', 'void', 'volcanic'
]
NOUNS = [
# Star Trek
'enterprise', 'voyager', 'galaxy', 'intrepid', 'nova', 'warbird', 'falcon',
'aegis', 'nemesis', 'equinox', 'stargazer', 'challenger', 'discovery',
'prometheus', 'odyssey', 'daedalus', 'apollo', 'korolev', 'phoenix', 'orion',
# Space
'nebula', 'pulsar', 'quasar', 'nova', 'supernova', 'blackhole',
'wormhole', 'singularity', 'galaxy', 'void', 'rift', 'nexus',
# Cool Concepts
'abyss', 'aether', 'anomaly', 'artifact', 'beacon', 'cipher', 'crystal',
'echo', 'enigma', 'essence', 'fractal', 'horizon', 'infinity',
'paradox', 'phoenix', 'prism', 'spectrum', 'tesseract',
'vector', 'vertex', 'vortex', 'zenith'
]
def load_config(config_file):
"""Load and parse YAML config file."""
with open(config_file, 'r') as f:
return yaml.safe_load(f)
def check_prerequisites():
"""Check if required tools are installed."""
required_tools = ['hcloud', 'jq']
for tool in required_tools:
if not shutil.which(tool):
print(f"Error: {tool} not found. Please install it first.")
sys.exit(1)
def check_hetzner_token():
"""Check if HCLOUD_TOKEN is set."""
if not os.environ.get('HCLOUD_TOKEN'):
print("Error: HCLOUD_TOKEN environment variable not set")
sys.exit(1)
def generate_hostname():
"""Generate a unique hostname in adjective-noun-date-time format."""
adjective = random.choice(ADJECTIVES)
noun = random.choice(NOUNS)
timestamp = datetime.now().strftime('%y%m%d-%H%M')
return f"{adjective}-{noun}-{timestamp}"
def get_image_id():
"""Get the base image ID."""
cmd = [
'hcloud', 'image', 'list',
'--type=snapshot',
'--selector=name=fedora-coreos-nullpoint',
'--output', 'json'
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
images = json.loads(result.stdout)
if not images:
print("Error: Base image not found. Run build.py first.")
sys.exit(1)
return images[0]['id']
def generate_secure_passphrase(length=32):
"""Generate a secure random passphrase."""
alphabet = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(alphabet) for _ in range(length))
def create_server(config, hostname, image_id):
"""Create a new server."""
hetzner = config['hetzner']
# Generate cloud-init config for this specific server
cloud_init = {
'hostname': hostname,
'timezone': 'UTC',
'users': [
{
'name': 'admin',
'groups': ['wheel'],
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
'ssh_authorized_keys': [config['admin_ssh_key']]
}
],
'package_update': True,
'package_upgrade': True
}
# Create temporary cloud-init config
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml') as f:
yaml.dump(cloud_init, f)
f.flush()
cmd = [
'hcloud', 'server', 'create',
'--name', hostname,
'--type', hetzner['server_type'],
'--datacenter', hetzner['datacenter'],
'--image', str(image_id),
'--ssh-key', hetzner['ssh_key_name'],
'--user-data-from-file', f.name
]
subprocess.run(cmd, check=True)
print(f"\nServer '{hostname}' created successfully!")
print("The LUKS passphrase has been saved to /root/luks-passphrase.txt on the server.")
print("Please save this passphrase securely - it will be needed if TPM+Tang unlock fails.")
print("\nYou can connect using: ssh admin@<server-ip>")
def get_server_ip(hostname):
"""Get the server's IP address."""
cmd = ['hcloud', 'server', 'ip', hostname]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout.strip()
def main():
"""Main entry point."""
# Load config
if not os.path.exists('deploy-config.yaml'):
print("Error: deploy-config.yaml not found")
sys.exit(1)
config = load_config('deploy-config.yaml')
# Check prerequisites
check_prerequisites()
check_hetzner_token()
# Generate hostname
hostname = generate_hostname()
# Get image ID
image_id = get_image_id()
# Create server
print(f"Creating server '{hostname}'...")
create_server(config, hostname, image_id)
# Get server IP
server_ip = get_server_ip(hostname)
print(f"Server created! IP: {server_ip}")
print(f"You can connect using: ssh core@{server_ip}")
if __name__ == '__main__':
main()

View File

@ -1,271 +0,0 @@
'builtin' 'local' '-a' 'p10k_config_opts'
[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases')
[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob')
[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand')
'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand'
() {
emulate -L zsh
setopt no_unset extended_glob
unset -m 'POWERLEVEL9K_*'
autoload -Uz is-at-least && is-at-least 5.1 || return
zmodload zsh/langinfo
if [[ ${langinfo[CODESET]:-} != (utf|UTF)(-|)8 ]]; then
local LC_ALL=${${(@M)$(locale -a):#*.(utf|UTF)(-|)8}[1]:-en_US.UTF-8}
fi
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
context
dir
vcs
)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
background_jobs
direnv
virtualenv
pyenv
disk_usage
)
typeset -g POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION='${P9K_VISUAL_IDENTIFIER// }'
typeset -g POWERLEVEL9K_MODE=nerdfont-complete
typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT=
typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=false
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─'
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─'
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─'
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮'
typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤'
typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯'
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' '
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND=
if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then
typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242
typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}'
typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}'
fi
typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\uE0B1'
typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\uE0B3'
typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0'
typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2'
typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=''
typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL=''
typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=232
typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=7
typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='%B${P9K_CONTENT// }'
typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND=
typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76
typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION=' '
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION=' '
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION=' '
typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION=' '
typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL=
typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE=
typeset -g POWERLEVEL9K_DIR_BACKGROUND=234
typeset -g POWERLEVEL9K_DIR_FOREGROUND=39
typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique
typeset -g POWERLEVEL9K_SHORTEN_DELIMITER=
typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=103
typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=39
typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=false
local anchor_files=(
.bzr
.citc
.git
.hg
.node-version
.python-version
.ruby-version
.shorten_folder_marker
.svn
.terraform
CVS
Cargo.toml
composer.json
go.mod
package.json
)
typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})"
typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1
typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80
typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40
typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50
typeset -g POWERLEVEL9K_DIR_HYPERLINK=false
typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=true
typeset -g POWERLEVEL9K_DIR_CLASSES=()
typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=10
typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=235
typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=10
typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=235
typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=10
typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=235
typeset -g POWERLEVEL9K_VCS_CONFLICTED_FOREGROUND=9
typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=235
typeset -g POWERLEVEL9K_VCS_LOADING_FOREGROUND=15
typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=255
typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 '
POWERLEVEL9K_VCS_BRANCH_ICON=${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}
typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?'
POWERLEVEL9K_VCS_UNTRACKED_ICON=${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}
function my_git_formatter() {
emulate -L zsh
if [[ -n $P9K_CONTENT ]]; then
typeset -g my_git_format=$P9K_CONTENT
return
fi
local meta=''
local clean=''
local modified=''
local untracked=''
local conflicted=''
local res
local where
if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then
res+="${clean}${POWERLEVEL9K_VCS_BRANCH_ICON}"
where=${(V)VCS_STATUS_LOCAL_BRANCH}
elif [[ -n $VCS_STATUS_TAG ]]; then
res+="${meta}#"
where=${(V)VCS_STATUS_TAG}
fi
(( $#where > 32 )) && where[13,-13]="…"
res+="${clean}${where//\%/%%}"
[[ -z $where ]] && res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}"
if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then
res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}"
fi
(( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}${VCS_STATUS_COMMITS_BEHIND}"
(( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" "
(( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}${VCS_STATUS_COMMITS_AHEAD}"
(( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}"
[[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}"
(( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}"
(( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}"
(( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}"
(( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}"
typeset -g my_git_format=$res
}
functions -M my_git_formatter 2>/dev/null
typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true
typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter()))+${my_git_format}}'
typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1
typeset -g POWERLEVEL9K_VCS_BACKENDS=(git)
typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true
typeset -g POWERLEVEL9K_STATUS_OK=true
typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔'
typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true
typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔'
typeset -g POWERLEVEL9K_STATUS_ERROR=true
typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true
typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false
typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true
typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘'
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=0
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=3
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0
typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s'
typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false
typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION=
typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION=
typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=95
typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=98
typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=true
typeset -g POWERLEVEL9K_VI_MODE_FOREGROUND=0
typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL
typeset -g POWERLEVEL9K_VI_MODE_NORMAL_BACKGROUND=2
typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL
typeset -g POWERLEVEL9K_VI_MODE_VISUAL_BACKGROUND=4
typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE
typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_BACKGROUND=3
typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING=
typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=8
typeset -g POWERLEVEL9K_LOAD_WHICH=5
typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true
typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false
typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}'
typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=1
typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=3
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=3
typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=0
typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m'
typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n'
typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false
typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER=
typeset -g POWERLEVEL9K_ANACONDA_SHOW_PYTHON_VERSION=false
typeset -g POWERLEVEL9K_ANACONDA_{LEFT,RIGHT}_DELIMITER=
typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false
typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER=
typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true
typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false
typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false
typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global)
typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false
typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=(
'*' DEFAULT)
typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_FOREGROUND=4
typeset -g POWERLEVEL9K_TERRAFORM_DEFAULT_BACKGROUND=0
typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc'
typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=(
'*' DEFAULT)
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=7
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_BACKGROUND=5
typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION=
POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}'
POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}'
typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|terraform|pulumi'
typeset -g POWERLEVEL9K_AWS_CLASSES=(
'*' DEFAULT)
typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|pulumi'
typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs'
typeset -g POWERLEVEL9K_GCLOUD_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT//\%/%%}'
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|pulumi'
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=(
'*' DEFAULT)
typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}'
typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=0
typeset -g POWERLEVEL9K_VPN_IP_BACKGROUND=6
typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION=
typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(wg|(.*tun))[0-9]*'
typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20
typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=1
typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=2
typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=3
typeset -g POWERLEVEL9K_BATTERY_STAGES=$'\uf58d\uf579\uf57a\uf57b\uf57c\uf57d\uf57e\uf57f\uf580\uf581\uf578'
typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false
typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}'
typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false
function prompt_example() {
p10k segment -i "POWERLEVEL9K_LINUX_ICON" -r -f blue -t " "
}
function instant_prompt_example() {
prompt_example
}
typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=off
typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose
typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true
(( ! $+functions[p10k] )) || p10k reload
}
(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]}
'builtin' 'unset' 'p10k_config_opts'

File diff suppressed because it is too large Load Diff

View File

@ -1,404 +0,0 @@
# : << EOF
# https://github.com/gpakosz/.tmux
# (‑●‑●)> dual licensed under the WTFPL v2 license and the MIT license,
# without any warranty.
# Copyright 2012— Gregory Pakosz (@gpakosz).
# -- navigation ----------------------------------------------------------------
# if you're running tmux within iTerm2
# - and tmux is 1.9 or 1.9a
# - and iTerm2 is configured to let option key act as +Esc
# - and iTerm2 is configured to send [1;9A -> [1;9D for option + arrow keys
# then uncomment the following line to make Meta + arrow keys mapping work
#set -ga terminal-overrides "*:kUP3=\e[1;9A,*:kDN3=\e[1;9B,*:kRIT3=\e[1;9C,*:kLFT3=\e[1;9D"
# -- windows & pane creation ---------------------------------------------------
# new window retains current path, possible values are:
# - true
# - false (default)
tmux_conf_new_window_retain_current_path=false
# new pane retains current path, possible values are:
# - true (default)
# - false
tmux_conf_new_pane_retain_current_path=true
# new pane tries to reconnect ssh sessions (experimental), possible values are:
# - true
# - false (default)
tmux_conf_new_pane_reconnect_ssh=true
# prompt for session name when creating a new session, possible values are:
# - true
# - false (default)
tmux_conf_new_session_prompt=false
# -- display -------------------------------------------------------------------
# RGB 24-bit colour support (tmux >= 2.2), possible values are:
# - true
# - false (default)
tmux_conf_24b_colour=true
# default theme
tmux_conf_theme_colour_1="#080808" # dark gray
tmux_conf_theme_colour_2="#303030" # gray
tmux_conf_theme_colour_3="#8a8a8a" # light gray
tmux_conf_theme_colour_4="#00afff" # light blue
tmux_conf_theme_colour_5="#ffff00" # yellow
tmux_conf_theme_colour_6="#080808" # dark gray
tmux_conf_theme_colour_7="#e4e4e4" # white
tmux_conf_theme_colour_8="#080808" # dark gray
tmux_conf_theme_colour_9="#ffff00" # yellow
tmux_conf_theme_colour_10="#ff00af" # pink
tmux_conf_theme_colour_11="#5fff00" # green
tmux_conf_theme_colour_12="#8a8a8a" # light gray
tmux_conf_theme_colour_13="#e4e4e4" # white
tmux_conf_theme_colour_14="#080808" # dark gray
tmux_conf_theme_colour_15="#080808" # dark gray
tmux_conf_theme_colour_16="#d70000" # red
tmux_conf_theme_colour_17="#e4e4e4" # white
# default theme (ansi)
#tmux_conf_theme_colour_1="colour0"
#tmux_conf_theme_colour_2="colour8"
#tmux_conf_theme_colour_3="colour8"
#tmux_conf_theme_colour_4="colour14"
#tmux_conf_theme_colour_5="colour11"
#tmux_conf_theme_colour_6="colour0"
#tmux_conf_theme_colour_7="colour15"
#tmux_conf_theme_colour_8="colour0"
#tmux_conf_theme_colour_9="colour11"
#tmux_conf_theme_colour_10="colour13"
#tmux_conf_theme_colour_11="colour10"
#tmux_conf_theme_colour_12="colour8"
#tmux_conf_theme_colour_13="colour15"
#tmux_conf_theme_colour_14="colour0"
#tmux_conf_theme_colour_15="colour0"
#tmux_conf_theme_colour_16="colour1"
#tmux_conf_theme_colour_17="colour15"
# window style
tmux_conf_theme_window_fg="default"
tmux_conf_theme_window_bg="default"
# highlight focused pane (tmux >= 2.1), possible values are:
# - true
# - false (default)
tmux_conf_theme_highlight_focused_pane=false
# focused pane colours:
tmux_conf_theme_focused_pane_bg="$tmux_conf_theme_colour_2"
# pane border style, possible values are:
# - thin (default)
# - fat
tmux_conf_theme_pane_border_style=thin
# pane borders colours:
tmux_conf_theme_pane_border="$tmux_conf_theme_colour_2"
tmux_conf_theme_pane_active_border="$tmux_conf_theme_colour_4"
# pane indicator colours (when you hit <prefix> + q)
tmux_conf_theme_pane_indicator="$tmux_conf_theme_colour_4"
tmux_conf_theme_pane_active_indicator="$tmux_conf_theme_colour_4"
# status line style
tmux_conf_theme_message_fg="$tmux_conf_theme_colour_1"
tmux_conf_theme_message_bg="$tmux_conf_theme_colour_5"
tmux_conf_theme_message_attr="bold"
# status line command style (<prefix> : Escape)
tmux_conf_theme_message_command_fg="$tmux_conf_theme_colour_5"
tmux_conf_theme_message_command_bg="$tmux_conf_theme_colour_1"
tmux_conf_theme_message_command_attr="bold"
# window modes style
tmux_conf_theme_mode_fg="$tmux_conf_theme_colour_1"
tmux_conf_theme_mode_bg="$tmux_conf_theme_colour_5"
tmux_conf_theme_mode_attr="bold"
# status line style
tmux_conf_theme_status_fg="$tmux_conf_theme_colour_3"
tmux_conf_theme_status_bg="$tmux_conf_theme_colour_1"
tmux_conf_theme_status_attr="none"
# terminal title
# - built-in variables are:
# - #{circled_window_index}
# - #{circled_session_name}
# - #{hostname}
# - #{hostname_ssh}
# - #{username}
# - #{username_ssh}
#tmux_conf_theme_terminal_title="#{username}@#{hostname}: #S - #I - #W"
tmux_conf_theme_terminal_title=" "
# window status style
# - built-in variables are:
# - #{circled_window_index}
# - #{circled_session_name}
# - #{hostname}
# - #{hostname_ssh}
# - #{username}
# - #{username_ssh}
tmux_conf_theme_window_status_fg="$tmux_conf_theme_colour_3"
tmux_conf_theme_window_status_bg="$tmux_conf_theme_colour_1"
tmux_conf_theme_window_status_attr="none"
tmux_conf_theme_window_status_format="#I #W"
#tmux_conf_theme_window_status_format="#{circled_window_index} #W"
#tmux_conf_theme_window_status_format="#I #W#{?window_bell_flag,🔔,}#{?window_zoomed_flag,🔍,}"
# window current status style
# - built-in variables are:
# - #{circled_window_index}
# - #{circled_session_name}
# - #{hostname}
# - #{hostname_ssh}
# - #{username}
# - #{username_ssh}
tmux_conf_theme_window_status_current_fg="$tmux_conf_theme_colour_1"
tmux_conf_theme_window_status_current_bg="$tmux_conf_theme_colour_4"
tmux_conf_theme_window_status_current_attr="bold"
tmux_conf_theme_window_status_current_format="#I #W"
#tmux_conf_theme_window_status_current_format="#{circled_window_index} #W"
#tmux_conf_theme_window_status_current_format="#I #W#{?window_zoomed_flag,🔍,}"
# window activity status style
tmux_conf_theme_window_status_activity_fg="default"
tmux_conf_theme_window_status_activity_bg="default"
tmux_conf_theme_window_status_activity_attr="underscore"
# window bell status style
tmux_conf_theme_window_status_bell_fg="$tmux_conf_theme_colour_5"
tmux_conf_theme_window_status_bell_bg="default"
tmux_conf_theme_window_status_bell_attr="blink,bold"
# window last status style
tmux_conf_theme_window_status_last_fg="$tmux_conf_theme_colour_4"
tmux_conf_theme_window_status_last_bg="$tmux_conf_theme_colour_2"
tmux_conf_theme_window_status_last_attr="none"
# status left/right sections separators
#tmux_conf_theme_left_separator_main=""
#tmux_conf_theme_left_separator_sub="|"
#tmux_conf_theme_right_separator_main=""
#tmux_conf_theme_right_separator_sub="|"
tmux_conf_theme_left_separator_main="\uE0B0" # /!\ you don't need to install Powerline
tmux_conf_theme_left_separator_sub="\uE0B1" # you only need fonts patched with
tmux_conf_theme_right_separator_main="\uE0B2" # Powerline symbols or the standalone
tmux_conf_theme_right_separator_sub="\uE0B3" # PowerlineSymbols.otf font, see README.md
# status left/right content:
# - separate main sections with "|"
# - separate subsections with ","
# - built-in variables are:
# - #{battery_bar}
# - #{battery_hbar}
# - #{battery_percentage}
# - #{battery_status}
# - #{battery_vbar}
# - #{circled_session_name}
# - #{hostname_ssh}
# - #{hostname}
# - #{loadavg}
# - #{pairing}
# - #{prefix}
# - #{root}
# - #{synchronized}
# - #{uptime_y}
# - #{uptime_d} (modulo 365 when #{uptime_y} is used)
# - #{uptime_h}
# - #{uptime_m}
# - #{uptime_s}
# - #{username}
# - #{username_ssh}
#tmux_conf_theme_status_left=" ❐ #S | ↑#{?uptime_y, #{uptime_y}y,}#{?uptime_d, #{uptime_d}d,}#{?uptime_h, #{uptime_h}h,}#{?uptime_m, #{uptime_m}m,} "
tmux_conf_theme_status_left=" [#S] | #{username}@#{hostname} ,"
#tmux_conf_theme_status_left=" [#S] "
#tmux_conf_theme_status_right=" #{prefix}#{pairing}#{synchronized}#{?battery_status,#{battery_status},}#{?battery_bar, #{battery_bar},}#{?battery_percentage, #{battery_percentage},} , %R , %d %b | #{username}#{root} | #{hostname} "
tmux_conf_theme_status_right=", %R , %d %b | #{battery_percentage} "
# status left style
tmux_conf_theme_status_left_fg="$tmux_conf_theme_colour_1,$tmux_conf_theme_colour_12"
tmux_conf_theme_status_left_bg="$tmux_conf_theme_colour_3,$tmux_conf_theme_colour_1"
tmux_conf_theme_status_left_attr="bold,none,none"
# status right style
tmux_conf_theme_status_right_fg="$tmux_conf_theme_colour_12,$tmux_conf_theme_colour_1"
tmux_conf_theme_status_right_bg="$tmux_conf_theme_colour_1,$tmux_conf_theme_colour_3"
tmux_conf_theme_status_right_attr="none,none,none"
# pairing indicator
tmux_conf_theme_pairing="⚇" # U+2687
tmux_conf_theme_pairing_fg="none"
tmux_conf_theme_pairing_bg="none"
tmux_conf_theme_pairing_attr="none"
# prefix indicator
tmux_conf_theme_prefix="⌨" # U+2328
tmux_conf_theme_prefix_fg="none"
tmux_conf_theme_prefix_bg="none"
tmux_conf_theme_prefix_attr="none"
# root indicator
tmux_conf_theme_root="!"
tmux_conf_theme_root_fg="none"
tmux_conf_theme_root_bg="none"
tmux_conf_theme_root_attr="bold,blink"
# synchronized indicator
tmux_conf_theme_synchronized="⚏" # U+268F
tmux_conf_theme_synchronized_fg="none"
tmux_conf_theme_synchronized_bg="none"
tmux_conf_theme_synchronized_attr="none"
# battery bar symbols
tmux_conf_battery_bar_symbol_full="◼"
tmux_conf_battery_bar_symbol_empty="◻"
#tmux_conf_battery_bar_symbol_full="♥"
#tmux_conf_battery_bar_symbol_empty="·"
# battery bar length (in number of symbols), possible values are:
# - auto
# - a number, e.g. 5
tmux_conf_battery_bar_length="auto"
# battery bar palette, possible values are:
# - gradient (default)
# - heat
# - "colour_full_fg,colour_empty_fg,colour_bg"
tmux_conf_battery_bar_palette="gradient"
#tmux_conf_battery_bar_palette="#d70000,#e4e4e4,#000000" # red, white, black
# battery hbar palette, possible values are:
# - gradient (default)
# - heat
# - "colour_low,colour_half,colour_full"
tmux_conf_battery_hbar_palette="gradient"
#tmux_conf_battery_hbar_palette="#d70000,#ff5f00,#5fff00" # red, orange, green
# battery vbar palette, possible values are:
# - gradient (default)
# - heat
# - "colour_low,colour_half,colour_full"
tmux_conf_battery_vbar_palette="gradient"
#tmux_conf_battery_vbar_palette="#d70000,#ff5f00,#5fff00" # red, orange, green
# symbols used to indicate whether battery is charging or discharging
tmux_conf_battery_status_charging="↑" # U+2191
tmux_conf_battery_status_discharging="↓" # U+2193
#tmux_conf_battery_status_charging="🔌" # U+1F50C
#tmux_conf_battery_status_discharging="🔋" # U+1F50B
# clock style (when you hit <prefix> + t)
# you may want to use %I:%M %p in place of %R in tmux_conf_theme_status_right
tmux_conf_theme_clock_colour="$tmux_conf_theme_colour_4"
tmux_conf_theme_clock_style="24"
# -- clipboard -----------------------------------------------------------------
# in copy mode, copying selection also copies to the OS clipboard
# - true
# - false (default)
# on macOS, this requires installing reattach-to-user-namespace, see README.md
# on Linux, this requires xsel or xclip
tmux_conf_copy_to_os_clipboard=true
# -- user customizations -------------------------------------------------------
# this is the place to override or undo settings
# increase history size
set -g history-limit 2048
# start with mouse mode enabled
set -g mouse on
# force Vi mode
# really you should export VISUAL or EDITOR environment variable, see manual
#set -g status-keys vi
#set -g mode-keys vi
# replace C-b by C-a instead of using both prefixes
#set -gu prefix2
#unbind C-a
#unbind C-b
#set -g prefix C-a
#bind C-a send-prefix
# move status line to top
#set -g status-position top
####### FULL CUSTOM ########
#bind '+' split-window -h
#bind '#' split-window -v
bind -r '+' split-window -p 50 -h -c "#{pane_current_path}"# \; set -u pane-active-border-style
bind -r '#' split-window -p 50 -v -c "#{pane_current_path}"# \; set -u pane-active-border-style
bind -n C-Left select-pane -L
bind -n C-Right select-pane -R
bind -n C-Up select-pane -U
bind -n C-Down select-pane -D
bind -r Tab next-window
#bind c new-window -c '#{pane_current_path}'
bind Z resize-pane -Z
set -g mouse on
setw -g monitor-activity on
set -g visual-activity on
set -g history-limit 2048
set -s escape-time 50
#set -g default-terminal "screen-256color"
set -g status-bg black
set -g status-fg white
set -g window-status-current-style bg=white,fg=black,bold
#set -g status-justify centre
set -g status-interval 60
set -g status-left-length 30
set -g status-left '#[fg=green][#S] #(whoami)@#(hostname) \uE0B0 #[bg=black] '
set -g status-right '#[fg=yellow]#(cut -d " " -f 1-3 /proc/loadavg)#[default] #[fg=white]%H:%M#[default]'
#set-option -ga terminal-overrides ",xterm-256color:Tc"
###bind y run-shell "tmux show-buffer | xclip -sel clip -i" \; display-message "Copied tmux buffer to system clipboard"
#bind -T copy-mode y send-keys -X copy-pipe-and-cancel "xsel -i -p && xsel -o -p | xsel -i -b"
#bind C-y run "xsel -o | tmux load-buffer - ; tmux paste-buffer"
# -- custom variables ----------------------------------------------------------
# to define a custom #{foo} variable, define a POSIX shell function between the
# '# EOF' and the '# "$@"' lines. Please note that the opening brace { character
# must be on the same line as the function name otherwise the parse won't detect
# it.
#
# then, use #{foo} in e.g. the 'tmux_conf_theme_status_left' or the
# 'tmux_conf_theme_status_right' variables.
# # /!\ do not remove the following line
# EOF
#
# weather() {
# curl -m 1 wttr.in?format=3 2>/dev/null
# sleep 900 # sleep for 15 minutes, throttle network requests whatever the value of status-interval
# }
#
# online() {
# ping -t 1 -c 1 1.1.1.1 >/dev/null 2>&1 && printf '✔' || printf '✘'
# }
#
# "$@"
# # /!\ do not remove the previous line

View File

@ -1,142 +0,0 @@
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="powerlevel10k/powerlevel10k"
HIST_STAMPS="dd.mm.yyyy"
plugins=(git)
source $ZSH/oh-my-zsh.sh
autoload -U +X bashcompinit && bashcompinit
[ -f /usr/share/bash-completion/completions/dnf ] && source /usr/share/bash-completion/completions/dnf
######################
# User configuration #
######################
export EDITOR='nvim'
# nullpoint custom fastfetch
alias fastfetch='fastfetch --file-raw /etc/nullpoint-logo'
if [ $TILIX_ID ] || [ $VTE_VERSION ]; then
[ -f /etc/profile.d/vte.sh ] && source /etc/profile.d/vte.sh
fi
#export LS_COLORS='rs=0:no=00:mi=00:mh=00:ln=01;36:or=01;31:di=01;34:ow=04;01;34:st=34:tw=04;34:pi=01;33:so=01;33:do=01;33:bd=01;33:cd=01;33:su=01;35:sg=01;35:ca=01;35:ex=01;32:'
##########
# lang
alias python='python3.13'
alias pip='python -m pip'
alias ptpython="python -m ptpython"
alias ptpy="python -m ptpython"
alias py13='python3.13'
alias py9='python3.9'
alias bpytop="python -m bpytop"
# python venv
alias pvcreate="python -m venv .venv"
alias pvactivate="source .venv/bin/activate"
alias pvinstall="echo 'Creating Python-venv' && pvcreate && echo 'Entering venv' && pvactivate && echo 'Upgrading pip' && pip install pip --upgrade && echo 'Installing Dependencies' && pip install -r requirements.txt && echo 'Done.'"
# qol
alias cls="clear"
alias root="sudo su --shell /bin/zsh"
alias open="xdg-open"
alias filenum="du -a | cut -d/ -f2 | sort | uniq -c | sort -nr"
alias pg="progress"
alias hist="history"
alias untar="tar -zxvf"
alias neofetch=fastfetch
# grep
alias antigrep="grep -v"
alias grep2="grep -A 2 -B 2"
alias grep4="grep -A 4 -B 4"
alias grep8="grep -A 8 -B 8"
alias grep16="grep -A 16 -B 16"
alias grep32="grep -A 32 -B 32"
alias ag="alias | grep"
# packages
alias get="sudo dnf install"
alias upd="sudo dnf check-upgrade"
alias upg="sudo dnf upgrade"
# chars
alias c="clear"
alias t="bpytop"
alias p="ptpy"
alias h="history | grep"
alias v="nvim"
alias n="nano"
alias o="open"
alias e="\d"
alias ":q"="exit"
# git
alias gs="git status"
alias gt="git log --all --graph --decorate --oneline --abbrev-commit"
alias gnoc='git shortlog -sn | cat' # Number Of Commit by user
alias gloc='git ls-files | while read f; do git blame -w -M -C -C --line-porcelain "$f" | grep -I "^author "; done | sort -f | uniq -ic | sort -n --reverse' # Lines Of Code by user (in final version)
alias gpa='gaa && gc -m "." && gp -4'
# dirs
alias tmp="cd ~/.tmp"
# extensions
alias sudo="sudo "
alias watch="watch "
alias watch1="watch -n 1 "
# better ls via lsd
alias ls="lsd"
alias l="ls -1"
alias la="ls -A"
alias laa="ls -a"
alias lsa="ls -lA"
alias lla="ls -lA"
alias ltree="ls --tree"
alias d0="ls --tree"
alias d="ls --depth 1 --tree"
alias d1="ls --depth 1 --tree"
alias d2="ls --depth 2 --tree"
alias d3="ls --depth 3 --tree"
alias d4="ls --depth 4 --tree"
alias d5="ls --depth 5 --tree"
alias d6="ls --depth 6 --tree"
# more ls
alias lo="\ls --color=tty"
alias dtree="tree --du -h"
alias gtree="tree --du -h -F -C | grep 'G] ' --color=never"
# better cat via batcat
alias bat="bat --pager ''"
alias pat="bat --pager '' --plain"
alias lat="bat"
# more fancy batcat stuff
alias man="batman"
# tmux
alias s="tmux"
alias sa="tmux attach"
alias sn="tmux new -s"
alias san="tmux attach -t"
alias sl="tmux ls"
alias sk="tmux kill-session -t"
alias ska="tmux kill-session -a"
#####
export PATH="$HOME/.local/bin:$PATH"
# Enable bash completion compatibility in zsh
autoload -U +X bashcompinit && bashcompinit
autoload -U +X compinit && compinit
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh

14
get.sh
View File

@ -1,14 +0,0 @@
#!/bin/bash
# Simple installer for nullpoint - just downloads and runs
# Usage: wget -qO- https://git.dominik-roth.eu/dodox/nullpoint/raw/branch/master/get.sh | bash
set -euo pipefail
# Clean up any existing directory
[ -d "/tmp/nullpoint_installer" ] && rm -rf /tmp/nullpoint_installer
# Clone and run
echo "[+] Downloading nullpoint installer..."
git clone https://git.dominik-roth.eu/dodox/nullpoint.git /tmp/nullpoint_installer
cd /tmp/nullpoint_installer
exec bash install.sh

View File

@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
height="200px"
width="200px"
version="1.1"
id="svg3"
sodipodi:docname="icon_cluster.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<sodipodi:namedview
id="namedview3"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="5.59"
inkscape:cx="100"
inkscape:cy="100"
inkscape:window-width="3440"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg3" />
<path
d="M12 3c-1.333 1 -2 2.5 -2 4.5c0 3 2 4.5 2 4.5s2 1.5 2 4.5c0 2 -.667 3.5 -2 4.5"
id="path1"
style="stroke:#ffffff;stroke-opacity:1;fill:none;fill-opacity:1" />
<path
d="M19.794 16.5c-.2 -1.655 -1.165 -2.982 -2.897 -3.982c-2.597 -1.5 -4.897 -.518 -4.897 -.518s-2.299 .982 -4.897 -.518c-1.732 -1 -2.698 -2.327 -2.897 -3.982"
id="path2"
style="stroke:#ffffff;stroke-opacity:1" />
<path
d="M19.794 7.5c-1.532 -.655 -3.165 -.482 -4.897 .518c-2.597 1.5 -2.897 3.982 -2.897 3.982s-.299 2.482 -2.897 3.982c-1.732 1 -3.365 1.173 -4.897 .518"
id="path3"
style="stroke:#ffffff;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,13 +0,0 @@
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme1n1
HOSTNAME nullpoint
BOOTLOADER grub
SWRAID 1
SWRAIDLEVEL 1
FILESYSTEM xfs
IMAGE /root/images/Alma-9-latest-amd64-base.tar.gz
CRYPTPASSWORD changeme
PART /boot/efi esp 256M
PART /boot ext4 2G
PART / xfs all crypt

View File

@ -1,252 +0,0 @@
#!/bin/bash
# nullpoint installer - run this from Hetzner rescue mode
# Use get.sh for wget piping: wget -qO- https://git.dominik-roth.eu/dodox/nullpoint/raw/branch/master/get.sh | bash
set -euo pipefail
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
)
clear
echo -e "\n$BANNER"
echo -e "\n[+] nullpoint installer starting..."
# Check if we're in Hetzner rescue mode
if [ ! -f /etc/hetzner-build ]; then
echo "ERROR: This script must be run from Hetzner rescue mode!"
exit 1
fi
# Get SSH key from current session
echo "[+] Detecting SSH key from current session..."
SSH_KEY=$(grep "^ssh-" ~/.ssh/authorized_keys | head -1)
if [ -z "$SSH_KEY" ]; then
echo "ERROR: No SSH key found in authorized_keys!"
echo "Please enter your SSH public key:"
read -r SSH_KEY
if [ -z "$SSH_KEY" ]; then
echo "SSH key is required!"
exit 1
fi
fi
echo "Found SSH key: ${SSH_KEY:0:50}..."
# Ask for hostname
echo -e "\n[+] Server configuration"
read -r -p "Enter hostname [nullpoint]: " HOSTNAME < /dev/tty
HOSTNAME=${HOSTNAME:-nullpoint}
# Ask for username
read -r -p "Enter username for admin account [null]: " USERNAME < /dev/tty
USERNAME=${USERNAME:-null}
# Generate secure LUKS passphrase
echo -e "\n[+] Generating secure LUKS passphrase..."
LUKS_PASS=$(openssl rand -base64 30)
echo -e "\n================================================"
echo "LUKS PASSPHRASE (SAVE THIS!):"
echo "$LUKS_PASS"
echo "================================================"
echo -e "\nPress Enter when you've saved the passphrase..."
read -r < /dev/tty
# Check if we're already in the nullpoint directory or need to use it
if [ -f "install.conf" ] && [ -f "post-install.sh" ]; then
echo "[+] Using local nullpoint files..."
else
# Clean up any existing directory
if [ -d "/tmp/nullpoint" ]; then
rm -rf /tmp/nullpoint
fi
echo "[+] Downloading nullpoint configuration..."
git clone https://git.dominik-roth.eu/dodox/nullpoint.git /tmp/nullpoint
cd /tmp/nullpoint
fi
echo "[+] Configuring installation..."
# Auto-detect drives
echo "[+] Detecting drives..."
DRIVES=($(lsblk -dno NAME,TYPE | grep disk | awk '{print "/dev/"$1}' | sort))
if [ ${#DRIVES[@]} -lt 2 ]; then
echo "ERROR: Need at least 2 drives for RAID1, found ${#DRIVES[@]}"
exit 1
fi
DRIVE1="${DRIVES[0]}"
DRIVE2="${DRIVES[1]}"
echo " Found drives: $DRIVE1 and $DRIVE2"
# Update install.conf with detected drives
if ! sed -i "s|^DRIVE1 .*|DRIVE1 $DRIVE1|" install.conf; then
echo "ERROR: Failed to update DRIVE1 in install.conf"
exit 1
fi
if ! sed -i "s|^DRIVE2 .*|DRIVE2 $DRIVE2|" install.conf; then
echo "ERROR: Failed to update DRIVE2 in install.conf"
exit 1
fi
# Update hostname
if ! sed -i "s/^HOSTNAME .*/HOSTNAME $HOSTNAME/" install.conf; then
echo "ERROR: Failed to update HOSTNAME in install.conf"
exit 1
fi
# Use awk for CRYPTPASSWORD to handle special characters
if ! awk -v pass="$LUKS_PASS" '
/^CRYPTPASSWORD / { print "CRYPTPASSWORD " pass; next }
{ print }
' install.conf > install.conf.tmp; then
echo "ERROR: Failed to update CRYPTPASSWORD in install.conf"
exit 1
fi
mv install.conf.tmp install.conf
# Update post-install.sh
if ! sed -i "s/^ALMA_USER=.*/ALMA_USER=\"$USERNAME\"/" post-install.sh; then
echo "ERROR: Failed to update ALMA_USER in post-install.sh"
exit 1
fi
# Use awk to replace SSH_KEY line to avoid sed issues with special characters
if ! awk -v key="$SSH_KEY" '
/^SSH_KEY=/ { print "SSH_KEY=\"" key "\""; next }
{ print }
' post-install.sh > post-install.sh.tmp; then
echo "ERROR: Failed to update SSH_KEY in post-install.sh"
exit 1
fi
mv post-install.sh.tmp post-install.sh
# Copy to root directory where installimage expects them
if ! cp install.conf /root/; then
echo "ERROR: Failed to copy install.conf to /root/"
exit 1
fi
if ! cp post-install.sh /root/; then
echo "ERROR: Failed to copy post-install.sh to /root/"
exit 1
fi
chmod +x /root/post-install.sh
# Ask for optional features
echo -e "\n[+] Optional features:"
read -r -p "Do you have a TPM and want to use it? [y/N]: " USE_TPM < /dev/tty
if [[ "$USE_TPM" =~ ^[Yy]$ ]]; then
echo "TPM will be configured if available."
else
sed -i 's/^TPM_ENABLED=.*/TPM_ENABLED=false/' /root/post-install.sh
fi
read -r -p "Do you want to configure remote unlock Tang servers? [y/N]: " USE_TANG < /dev/tty
if [[ "$USE_TANG" =~ ^[Yy]$ ]]; then
echo "Configuring Tang servers..."
TANG_CONFIG=""
while true; do
read -r -p "Enter Tang server URL (or press Enter to finish): " TANG_URL < /dev/tty
if [ -z "$TANG_URL" ]; then
break
fi
read -r -p "Enter thumbprint for $TANG_URL: " TANG_THUMBPRINT < /dev/tty
if [ -n "$TANG_THUMBPRINT" ]; then
TANG_CONFIG+=" \"$TANG_URL $TANG_THUMBPRINT\"\n"
echo "Added Tang server: $TANG_URL"
else
echo "Skipping server (no thumbprint provided)"
fi
done
if [ -n "$TANG_CONFIG" ]; then
# Update the TANG_SERVERS array in post-install.sh
sed -i '/^TANG_SERVERS=(/,/^)/ {
/^TANG_SERVERS=(/ {
r /dev/stdin
d
}
/^)/ !d
}' /root/post-install.sh << EOF
TANG_SERVERS=(
$TANG_CONFIG)
EOF
echo "Configured Tang servers in post-install script."
else
echo "No Tang servers configured."
fi
fi
# Final confirmation
echo -e "\n[+] Ready to install with these settings:"
echo " Hostname: $HOSTNAME"
echo " Username: $USERNAME"
echo " SSH Key: ${SSH_KEY:0:50}..."
echo " LUKS Passphrase: $LUKS_PASS"
echo ""
read -r -p "Proceed with installation? [Y/n]: " CONFIRM < /dev/tty
if [[ "$CONFIRM" =~ ^[Nn]$ ]]; then
echo "Installation cancelled."
exit 1
fi
# Check if installimage exists
if ! command -v installimage &> /dev/null; then
echo "ERROR: installimage command not found!"
echo "This might not be a Hetzner rescue system, or installimage is not in PATH."
echo "On Hetzner rescue, installimage is usually at /root/.oldroot/nfs/install/installimage"
# Try common locations
if [ -x "/root/.oldroot/nfs/install/installimage" ]; then
echo "Found installimage at /root/.oldroot/nfs/install/installimage"
INSTALLIMAGE_CMD="/root/.oldroot/nfs/install/installimage"
else
echo "Could not find installimage. Please check your Hetzner rescue environment."
exit 1
fi
else
INSTALLIMAGE_CMD="installimage"
fi
# Run the installer
echo -e "\n[+] Starting Hetzner installimage..."
echo "The installer will now run. Follow any prompts if needed."
echo ""
if ! $INSTALLIMAGE_CMD -a -c /root/install.conf -x /root/post-install.sh; then
echo -e "\nERROR: Installation failed!"
exit 1
fi
echo -e "\n[+] Installation complete!"
echo ""
echo "IMPORTANT REMINDERS:"
echo "1. Save your LUKS passphrase securely!"
echo "2. After reboot, you'll need to enter it twice (once per disk)"
echo "3. SSH to the server as user '$USERNAME'"
echo ""
echo "The system is ready for use!"

View File

@ -1,554 +0,0 @@
#!/bin/bash
set -euo pipefail
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_ENABLED=true
TPM_PCR_BANK="sha256"
TPM_PCR_IDS="0,1,2,3,4,5,6,7,8"
ALMA_USER="null"
ENABLE_MOTD=true
# REQUIRED: Set your SSH public key here - installation will fail without it!
SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkoTn2NreAXMriOUqzyj3YoFW6jMo9B5B+3R5k8yrMi dodox@dodox-ProArt"
########################################################
# Config End
########################################################
set -euo pipefail
echo -e "\n$BANNER"
echo -e "\n[+] Starting post-installation configuration..."
# Check for SSH key
if [ -z "${SSH_KEY:-}" ]; then
echo "ERROR: No SSH key configured!"
echo "You must set SSH_KEY variable at the top of this script."
exit 1
fi
# Check for TPM
echo "[+] Checking for TPM..."
if [ ! -d "/sys/class/tpm/tpm0" ]; then
echo "WARNING: No TPM detected!"
TPM_ENABLED=false
else
echo "TPM detected."
TPM_ENABLED=true
fi
# Upgrade system packages first
echo "[+] Upgrading system packages..."
dnf upgrade -y || echo "WARNING: System upgrade failed"
# Install basic packages
echo "[+] Installing basic packages..."
dnf install -y epel-release || exit 1
dnf config-manager --set-enabled crb || exit 1
dnf install -y zsh git wget curl || exit 1
# Create user and add SSH key
echo "[+] Creating user ${ALMA_USER}..."
useradd -m -G wheel -s /bin/zsh ${ALMA_USER} || exit 1
mkdir -p /home/${ALMA_USER}/.ssh
echo "${SSH_KEY}" > /home/${ALMA_USER}/.ssh/authorized_keys
chmod 700 /home/${ALMA_USER}/.ssh
chmod 600 /home/${ALMA_USER}/.ssh/authorized_keys
chown -R ${ALMA_USER}:${ALMA_USER} /home/${ALMA_USER}/.ssh
# Configure passwordless sudo
echo "[+] Configuring passwordless sudo for ${ALMA_USER}..."
echo "${ALMA_USER} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/99-${ALMA_USER}
chmod 440 /etc/sudoers.d/99-${ALMA_USER}
# Setup terminal environment for both user and root
echo "[+] Setting up terminal environment for ${ALMA_USER} and root..."
for user_account in "${ALMA_USER}" "root"; do
echo " - Setting up $user_account..."
if [ "$user_account" = "root" ]; then
home_dir="/root"
user_prefix=""
else
home_dir="/home/${user_account}"
user_prefix="su - ${user_account} -c"
fi
# Install oh-my-zsh
if [ "$user_account" = "root" ]; then
export RUNZSH=no CHSH=no KEEP_ZSHRC=yes && bash -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 2>/dev/null || echo "WARNING: $user_account oh-my-zsh installation failed"
else
su - ${user_account} -c 'export RUNZSH=no CHSH=no KEEP_ZSHRC=yes && bash -c "$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"' 2>/dev/null || echo "WARNING: $user_account oh-my-zsh installation failed"
fi
# Install powerlevel10k
if [ "$user_account" = "root" ]; then
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${home_dir}/.oh-my-zsh/custom/themes/powerlevel10k 2>/dev/null || echo "WARNING: $user_account powerlevel10k installation failed"
else
su - ${user_account} -c 'git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/.oh-my-zsh/custom/themes/powerlevel10k' 2>/dev/null || echo "WARNING: $user_account powerlevel10k installation failed"
fi
# Install dotfiles
if [ "$user_account" = "root" ]; then
cd ${home_dir} &&
git clone https://git.dominik-roth.eu/dodox/nullpoint.git /tmp/nullpoint-dotfiles-${user_account} &&
cd /tmp/nullpoint-dotfiles-${user_account}/dotfiles &&
for file in .*; do
if [ -f "$file" ] && [ "$file" != "." ] && [ "$file" != ".." ]; then
cp "$file" ${home_dir}/ 2>/dev/null || true
fi
done &&
cd ${home_dir} && rm -rf /tmp/nullpoint-dotfiles-${user_account} || echo "WARNING: $user_account dotfiles installation failed"
else
su - ${user_account} -c "
cd &&
git clone https://git.dominik-roth.eu/dodox/nullpoint.git /tmp/nullpoint-dotfiles-${user_account} &&
cd /tmp/nullpoint-dotfiles-${user_account}/dotfiles &&
for file in .*; do
if [ -f \"\$file\" ] && [ \"\$file\" != \".\" ] && [ \"\$file\" != \"..\" ]; then
cp \"\$file\" ~/ 2>/dev/null || true
fi
done &&
cd && rm -rf /tmp/nullpoint-dotfiles-${user_account}
" || echo "WARNING: $user_account dotfiles installation failed"
fi
# Set zsh as default shell
if [ "$user_account" = "root" ]; then
sed -i 's|^root:.*:/bin/bash$|root:x:0:0:root:/root:/bin/zsh|' /etc/passwd
else
sed -i "s|^${user_account}:.*:/bin/bash$|${user_account}:x:$(id -u ${user_account}):$(id -g ${user_account})::/home/${user_account}:/bin/zsh|" /etc/passwd
fi
done
# Set up MOTD
if [ "$ENABLE_MOTD" = true ]; then
echo "[+] Setting up MOTD..."
cat > /etc/motd << MOTD
$BANNER
MOTD
fi
# Modify /etc/os-release to show nullpoint branding
echo "[+] Updating /etc/os-release with nullpoint branding..."
if [ -f /etc/os-release ]; then
# Backup original
cp /etc/os-release /etc/os-release.bak
# Get original PRETTY_NAME value
ORIG_PRETTY_NAME=$(grep '^PRETTY_NAME=' /etc/os-release | cut -d'"' -f2)
# Update PRETTY_NAME to show nullpoint with base OS info
sed -i "s/^PRETTY_NAME=.*/PRETTY_NAME=\"nullpoint (base: ${ORIG_PRETTY_NAME})\"/" /etc/os-release
echo " - Updated PRETTY_NAME to 'nullpoint (base: ${ORIG_PRETTY_NAME})'"
fi
# Install additional packages
echo "[+] Installing additional packages..."
dnf install -y \
clevis clevis-luks tpm2-tools tpm2-tss \
tmux neovim python3-pip python3.13 python3.13-pip \
tree gcc make autoconf automake tar bzip2 \
bash-completion || exit 1
# Install dropbear for early boot SSH
echo "[+] Installing dropbear for early boot SSH..."
dnf install -y dropbear dracut-network || exit 1
# Install modern CLI tools
echo "[+] Installing lsd, bat, and fastfetch..."
# Try to install fastfetch from repos first
dnf install -y fastfetch || echo "fastfetch not available in repos, skipping"
# Create nullpoint logo file for fastfetch
echo "[+] Creating nullpoint logo file..."
cat > /etc/nullpoint-logo << '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: ^^:
^^
EOF
# Install user-specific tools for both user and root
echo "[+] Installing user-specific tools..."
# Install for user
echo " - Installing for ${ALMA_USER}..."
su - ${ALMA_USER} -c 'curl -fsSL https://claude.ai/install.sh | bash' || echo "WARNING: ${ALMA_USER} Claude Code installation failed"
su - ${ALMA_USER} -c 'python3.13 -m pip install --user bpytop' || echo "WARNING: ${ALMA_USER} bpytop installation failed"
# Install for root
echo " - Installing for root..."
curl -fsSL https://claude.ai/install.sh | bash || echo "WARNING: root Claude Code installation failed"
python3.13 -m pip install bpytop || echo "WARNING: root bpytop installation failed"
# Install Docker from official repository
echo "[+] Installing Docker..."
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo || echo "WARNING: Docker repo setup failed"
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || echo "WARNING: Docker installation failed"
systemctl enable docker || echo "WARNING: Docker enable failed"
usermod -aG docker ${ALMA_USER} || echo "WARNING: Adding user to docker group failed"
# Install using fixed versions that should work
LSD_VERSION="1.0.0"
BAT_VERSION="0.24.0"
# Download and install lsd
if curl -sL "https://github.com/lsd-rs/lsd/releases/download/v${LSD_VERSION}/lsd-v${LSD_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar xz -C /tmp; then
mv /tmp/lsd-*/lsd /usr/local/bin/ 2>/dev/null || true
chmod +x /usr/local/bin/lsd 2>/dev/null || true
fi
# Download and install bat
if curl -sL "https://github.com/sharkdp/bat/releases/download/v${BAT_VERSION}/bat-v${BAT_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar xz -C /tmp; then
mv /tmp/bat-*/bat /usr/local/bin/ 2>/dev/null || true
chmod +x /usr/local/bin/bat 2>/dev/null || true
fi
# Create batman script for fancy man pages
cat > /usr/local/bin/batman << 'BATMAN'
#!/bin/bash
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
export MANROFFOPT="-c"
man "$@"
BATMAN
chmod +x /usr/local/bin/batman
# Create .tmp directory for user and root
mkdir -p /home/${ALMA_USER}/.tmp
chown ${ALMA_USER}:${ALMA_USER} /home/${ALMA_USER}/.tmp
mkdir -p /root/.tmp
# Configure Clevis for automatic unlock
if [ ${#TANG_SERVERS[@]} -gt 0 ] || [ "$TPM_ENABLED" = true ]; then
echo "[+] Configuring Clevis for automatic unlock..."
# Find LUKS devices
LUKS_DEVICES=$(lsblk -o NAME,FSTYPE -nr | grep crypto_LUKS | cut -d' ' -f1)
for device in $LUKS_DEVICES; do
DEVICE_PATH="/dev/${device}"
echo "Configuring Clevis for ${DEVICE_PATH}..."
if [ "$TPM_ENABLED" = true ] && [ ${#TANG_SERVERS[@]} -eq 0 ]; then
# TPM only
clevis luks bind -d "$DEVICE_PATH" tpm2 "{\"pcr_bank\":\"$TPM_PCR_BANK\",\"pcr_ids\":\"$TPM_PCR_IDS\"}"
elif [ "$TPM_ENABLED" = false ] && [ ${#TANG_SERVERS[@]} -gt 0 ]; then
# Tang only
for server in "${TANG_SERVERS[@]}"; do
read -r url thumbprint <<< "$server"
clevis luks bind -d "$DEVICE_PATH" tang "{\"url\":\"$url\",\"thp\":\"$thumbprint\"}"
done
elif [ "$TPM_ENABLED" = true ] && [ ${#TANG_SERVERS[@]} -gt 0 ]; then
# Both TPM and Tang (require both)
CONFIG="{\"t\":2,\"pins\":{"
CONFIG+="\"tpm2\":{\"pcr_bank\":\"$TPM_PCR_BANK\",\"pcr_ids\":\"$TPM_PCR_IDS\"},"
CONFIG+="\"tang\":{\"t\":1,\"tang\":["
for server in "${TANG_SERVERS[@]}"; do
read -r url thumbprint <<< "$server"
CONFIG+="{\"url\":\"$url\",\"thp\":\"$thumbprint\"},"
done
CONFIG="${CONFIG%,}]}}}"
clevis luks bind -d "$DEVICE_PATH" sss "$CONFIG"
fi
done
fi
# Enable Clevis for early boot (only needed for AlmaLinux < 8.7)
echo "[+] Configuring Clevis for early boot..."
OS_VERSION=$(cat /etc/redhat-release | grep -oE '[0-9]+\.[0-9]+' | head -1)
if [[ "$(echo "$OS_VERSION < 8.7" | bc)" -eq 1 ]]; then
echo " - Enabling clevis-luks-askpass.path for AlmaLinux $OS_VERSION"
systemctl enable clevis-luks-askpass.path || true
else
echo " - AlmaLinux $OS_VERSION: clevis-luks-askpass.path not needed"
fi
# Configure dropbear for remote unlock
echo "[+] Configuring dropbear SSH for remote unlock..."
# Create custom dracut module for dropbear SSH
mkdir -p /usr/lib/dracut/modules.d/60dropbear-ssh
# Create the module setup script
cat > /usr/lib/dracut/modules.d/60dropbear-ssh/module-setup.sh << 'EOF'
#!/bin/bash
check() {
require_binaries dropbear dbclient dropbearkey dropbearconvert || return 1
return 0
}
depends() {
echo network
}
install() {
inst_multiple dropbear dbclient dropbearkey dropbearconvert
# Create directories
inst_dir /etc/dropbear
inst_dir /var/log
inst_dir /root/.ssh
# Copy authorized keys if they exist
if [ -f /etc/dropbear/authorized_keys ]; then
inst /etc/dropbear/authorized_keys /root/.ssh/authorized_keys
fi
# Install ED25519 host key only
keyfile="/etc/dropbear/dropbear_ed25519_host_key"
if [ ! -f "$keyfile" ]; then
dropbearkey -t ed25519 -f "$keyfile" 2>/dev/null
fi
[ -f "$keyfile" ] && inst "$keyfile"
# Install the service
inst_simple "$moddir/dropbear.service" /etc/systemd/system/dropbear.service
systemctl -q --root "$initdir" enable dropbear.service
# Install unlock helper
inst_simple "$moddir/unlock-luks.sh" /usr/bin/unlock-luks
chmod 755 "$initdir/usr/bin/unlock-luks"
}
EOF
# Create systemd service for dropbear
cat > /usr/lib/dracut/modules.d/60dropbear-ssh/dropbear.service << 'EOF'
[Unit]
Description=Dropbear SSH Server
After=network-online.target
Wants=network-online.target
[Service]
Type=forking
ExecStart=/usr/sbin/dropbear -R -E -p 22
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
[Install]
WantedBy=sysinit.target
EOF
# Create unlock helper script
cat > /usr/lib/dracut/modules.d/60dropbear-ssh/unlock-luks.sh << 'EOF'
#!/bin/bash
echo "=== LUKS Remote Unlock Helper ==="
echo ""
echo "Checking for encrypted devices..."
# Show block devices if available
if command -v lsblk >/dev/null 2>&1; then
echo "Block devices:"
lsblk -o NAME,SIZE,TYPE,FSTYPE 2>/dev/null || echo " (lsblk not available)"
else
echo "Block devices: (listing /dev/sd* and /dev/md*)"
ls -la /dev/sd* /dev/md* 2>/dev/null || echo " No standard devices found"
fi
echo ""
echo "Encrypted devices status:"
# Check for LUKS devices waiting to be unlocked
for dev in /dev/mapper/luks-*; do
if [ -e "$dev" ]; then
echo " Found: $dev"
fi
done
# Check systemd-ask-password files directly
if [ -d /run/systemd/ask-password ]; then
echo ""
echo "Password prompts waiting:"
ls -la /run/systemd/ask-password/ 2>/dev/null
fi
echo ""
echo "Starting unlock process..."
echo "Enter your LUKS passphrase when prompted:"
echo ""
# Run the password agent
if command -v systemd-tty-ask-password-agent >/dev/null 2>&1; then
systemd-tty-ask-password-agent
else
echo "ERROR: systemd-tty-ask-password-agent not found!"
echo "Try running: /lib/systemd/systemd-tty-ask-password-agent"
fi
EOF
chmod +x /usr/lib/dracut/modules.d/60dropbear-ssh/*.sh
# Setup dropbear authorized keys
mkdir -p /etc/dropbear
echo "${SSH_KEY}" > /etc/dropbear/authorized_keys
chmod 600 /etc/dropbear/authorized_keys
# Generate ED25519 host key only (most secure)
echo "[+] Generating ED25519 SSH host key..."
# Use system SSH key if available, otherwise generate dropbear key
openssh_key="/etc/ssh/ssh_host_ed25519_key"
dropbear_key="/etc/dropbear/dropbear_ed25519_host_key"
if [ -f "$openssh_key" ] && command -v dropbearconvert >/dev/null 2>&1; then
echo " Converting existing OpenSSH ED25519 key to dropbear format..."
dropbearconvert openssh dropbear "$openssh_key" "$dropbear_key" 2>/dev/null || {
echo " Conversion failed, generating new dropbear key..."
dropbearkey -t ed25519 -f "$dropbear_key" | grep -v "Generating" || true
}
elif [ ! -f "$dropbear_key" ]; then
echo " Generating new ED25519 key..."
dropbearkey -t ed25519 -f "$dropbear_key" | grep -v "Generating" || true
# Also generate OpenSSH format to prevent key mismatch after boot
if command -v ssh-keygen >/dev/null 2>&1; then
echo " Generating matching OpenSSH key..."
mkdir -p /etc/ssh
# Extract public key and generate OpenSSH private key
dropbearkey -y -f "$dropbear_key" | grep "^ssh-" > "${openssh_key}.pub"
# Note: Direct conversion from dropbear to openssh private key requires dropbearconvert
# For now, we'll have different keys but document the solution
fi
fi
# Display SHA256 fingerprint
if command -v ssh-keygen >/dev/null 2>&1; then
fingerprint=$(dropbearkey -y -f "$dropbear_key" | ssh-keygen -lf - -E sha256 2>/dev/null | awk '{print $2}')
if [ -n "$fingerprint" ]; then
echo " SHA256 fingerprint: $fingerprint"
echo " Note: This is the initramfs (rescue) SSH fingerprint."
echo " The normal system SSH may have a different fingerprint."
fi
fi
# Configure dracut
cat > /etc/dracut.conf.d/60-dropbear-ssh.conf << 'EOF'
# Enable network and dropbear SSH
add_dracutmodules+=" network dropbear-ssh "
# Network configuration
kernel_cmdline="rd.neednet=1"
EOF
# Regenerate initramfs
echo "[+] Regenerating initramfs..."
dracut -f --regenerate-all
# Enable required services
echo "[+] Enabling services..."
# systemctl enable stratisd # Not needed without Stratis
systemctl enable sshd
# Secure SSH configuration
echo "[+] Securing SSH..."
{
# Disable root login
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
# Only allow SSH key authentication
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#*UsePAM.*/UsePAM no/' /etc/ssh/sshd_config
# Only allow specific user
echo "AllowUsers ${ALMA_USER}" >> /etc/ssh/sshd_config
echo " - Root login disabled"
echo " - Password authentication disabled"
echo " - Only user '${ALMA_USER}' allowed"
}
# Set SELinux to enforcing
echo "[+] Setting SELinux to enforcing..."
sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
echo "✅ Post-installation complete!"
echo ""
# Display SSH host key fingerprints
echo "SSH Host Key Fingerprints:"
ed25519_fp=""
dropbear_fp=""
if [ -f "/etc/ssh/ssh_host_ed25519_key.pub" ] && command -v ssh-keygen >/dev/null 2>&1; then
ed25519_fp=$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256 2>/dev/null | awk '{print $2}')
fi
if [ -f "/etc/dropbear/dropbear_ed25519_host_key" ] && command -v ssh-keygen >/dev/null 2>&1; then
dropbear_fp=$(dropbearkey -y -f /etc/dropbear/dropbear_ed25519_host_key 2>/dev/null | ssh-keygen -lf - -E sha256 2>/dev/null | awk '{print $2}')
fi
if [ -n "$ed25519_fp" ] && [ -n "$dropbear_fp" ] && [ "$ed25519_fp" = "$dropbear_fp" ]; then
echo " SSH (ED25519): $ed25519_fp (same for both rescue and normal)"
elif [ -n "$ed25519_fp" ] && [ -n "$dropbear_fp" ]; then
echo " Normal SSH (ED25519): $ed25519_fp"
echo " Rescue SSH (ED25519): $dropbear_fp"
elif [ -n "$ed25519_fp" ]; then
echo " Normal SSH (ED25519): $ed25519_fp"
elif [ -n "$dropbear_fp" ]; then
echo " Rescue SSH (ED25519): $dropbear_fp"
else
echo " No ED25519 keys found"
fi
echo ""
echo "IMPORTANT: Save the LUKS passphrase from install.conf securely!"
echo ""
echo "Next Steps:"
echo "1. Manually reboot the system when ready"
echo "2. SSH root@<server-ip> → run 'unlock-luks' → enter passphrase"
echo "3. System finalizes setup and reboots automatically"
echo "4. SSH root@<server-ip> → run 'unlock-luks' → enter passphrase again"
echo "5. System is ready → SSH as user '${ALMA_USER}'"
echo ""
echo "Future boots: Only one unlock needed (or automatic if Tang/Clevis configured)"

1
requirements.txt Normal file
View File

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