cleaned up repo
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ __pycache__/
|
||||
*.pyo
|
||||
iso/secrets/enroll-token
|
||||
iso/secrets/provisioner-url
|
||||
result
|
||||
|
||||
51
index.html
51
index.html
@@ -1,51 +0,0 @@
|
||||
<section id="usage-ways">
|
||||
<h2>Three Ways to Use Sovran_SystemsOS</h2>
|
||||
<div class="usage-option">
|
||||
<h3><img src="path/to/desktop-icon.png" alt="Desktop Option"> Desktop</h3>
|
||||
<p>Enjoy a full-fledged desktop experience.</p>
|
||||
</div>
|
||||
<div class="usage-option">
|
||||
<h3><img src="path/to/bitcoin-icon.png" alt="Node Option"> Node</h3>
|
||||
<p>Leverage the power of Node for your applications.</p>
|
||||
</div>
|
||||
<div class="usage-option">
|
||||
<h3><img src="path/to/server-icon.png" alt="Server Option"> Server</h3>
|
||||
<p>Utilize cutting-edge server capabilities.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="download">
|
||||
<h2>Download Sovran_SystemsOS</h2>
|
||||
<p>Sovran_SystemsOS is free to download for all three options: Desktop, Node, and Server.</p>
|
||||
<p><strong>Installation Instructions:</strong></p>
|
||||
<ul>
|
||||
<li>For Mac: <a href="#mac-install">Installation Guide</a></li>
|
||||
<li>For Windows: <a href="#windows-install">Installation Guide</a></li>
|
||||
<li>For Linux: <a href="#linux-install">Installation Guide</a></li>
|
||||
</ul>
|
||||
<a href="#download-link" class="btn btn-primary">Download Now</a>
|
||||
</section>
|
||||
<section id="donations">
|
||||
<h2>Support Us Through Donations</h2>
|
||||
<p>Help us keep Sovran_SystemsOS alive and thriving!</p>
|
||||
<div class="donation-tier">
|
||||
<h3>Regular Donation</h3>
|
||||
<p>Encouraging daily users to support us!</p>
|
||||
</div>
|
||||
<div class="donation-tier royal">
|
||||
<h3>Royal Donation - $1500</h3>
|
||||
<p>Receive a white-glove installation on curated hardware with 1 year private tech support and 1 year domain purchase.</p>
|
||||
<p>Hardware Specs: AMD Ryzen 5 7535U | DDR5 32GB RAM | 1TB NVME | 4TB SSD NVME | 2x 1 Gigbit LAN Ports | 1 Year Tech Support | 1 Year Warranty</p>
|
||||
</div>
|
||||
<a href="https://zaps.sovransystems.com/" class="btn btn-donate">Donate Now</a>
|
||||
</section>
|
||||
<style>
|
||||
#usage-ways {
|
||||
background-color: #2b2b2b;
|
||||
color: white;
|
||||
}
|
||||
.donation-tier {
|
||||
background-color: #5a5a5a;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,308 +0,0 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.sovranProvisioner;
|
||||
|
||||
# ── Python provisioning API ──────────────────────────────────────────────────
|
||||
provisionerScript = pkgs.writeTextFile {
|
||||
name = "sovran-provisioner-app.py";
|
||||
text = ''
|
||||
#!/usr/bin/env python3
|
||||
"""Sovran Systems — Machine Provisioning Server"""
|
||||
import subprocess, secrets, json, time, os, fcntl, threading
|
||||
from datetime import datetime, timezone
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
STATE_DIR = os.environ.get("SOVRAN_STATE_DIR", "/var/lib/sovran-provisioner")
|
||||
TOKEN_FILE = os.environ.get("SOVRAN_ENROLL_TOKEN_FILE", f"{STATE_DIR}/enroll-token")
|
||||
HEADSCALE_USER = os.environ.get("HEADSCALE_USER", "sovran-deploy")
|
||||
KEY_EXPIRY = os.environ.get("KEY_EXPIRY", "1h")
|
||||
HEADSCALE_DOMAIN = os.environ.get("HEADSCALE_DOMAIN", "localhost")
|
||||
RATE_LIMIT_MAX = int(os.environ.get("RATE_LIMIT_MAX", "10"))
|
||||
RATE_LIMIT_WINDOW = int(os.environ.get("RATE_LIMIT_WINDOW", "60"))
|
||||
|
||||
_rate_lock = threading.Lock()
|
||||
rate_state = {"count": 0, "window_start": time.time()}
|
||||
|
||||
def get_enroll_token():
|
||||
try:
|
||||
with open(TOKEN_FILE, "r") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
def check_rate_limit():
|
||||
now = time.time()
|
||||
with _rate_lock:
|
||||
if now - rate_state["window_start"] > RATE_LIMIT_WINDOW:
|
||||
rate_state["count"] = 0
|
||||
rate_state["window_start"] = now
|
||||
rate_state["count"] += 1
|
||||
return rate_state["count"] <= RATE_LIMIT_MAX
|
||||
|
||||
def validate_token(req):
|
||||
token = req.headers.get("Authorization", "").replace("Bearer ", "")
|
||||
expected = get_enroll_token()
|
||||
if not expected:
|
||||
return False
|
||||
return secrets.compare_digest(token, expected)
|
||||
|
||||
def create_headscale_key():
|
||||
result = subprocess.run(
|
||||
["headscale", "preauthkeys", "create",
|
||||
"--user", HEADSCALE_USER,
|
||||
"--expiration", KEY_EXPIRY,
|
||||
"--ephemeral",
|
||||
"--output", "json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"headscale error: {result.stderr}")
|
||||
data = json.loads(result.stdout)
|
||||
return data.get("key", data.get("preAuthKey", {}).get("key", ""))
|
||||
|
||||
_reg_lock = threading.Lock()
|
||||
|
||||
def load_registrations():
|
||||
path = f"{STATE_DIR}/registrations.json"
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
def save_registration(entry):
|
||||
path = f"{STATE_DIR}/registrations.json"
|
||||
with _reg_lock:
|
||||
regs = load_registrations()
|
||||
regs.append(entry)
|
||||
# Keep last 1000 entries
|
||||
regs = regs[-1000:]
|
||||
with open(path, "w") as f:
|
||||
fcntl.flock(f, fcntl.LOCK_EX)
|
||||
json.dump(regs, f, indent=2)
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
@app.route("/register", methods=["POST"])
|
||||
def register():
|
||||
if not check_rate_limit():
|
||||
return jsonify({"error": "rate limited"}), 429
|
||||
if not validate_token(request):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
|
||||
try:
|
||||
key = create_headscale_key()
|
||||
except RuntimeError as e:
|
||||
app.logger.error(f"Headscale key creation failed: {e}")
|
||||
return jsonify({"error": "internal server error"}), 500
|
||||
|
||||
entry = {
|
||||
"hostname": body.get("hostname", "unknown"),
|
||||
"mac": body.get("mac", "unknown"),
|
||||
"ip": request.remote_addr,
|
||||
"registered_at": datetime.now(timezone.utc).isoformat(),
|
||||
"key_prefix": key[:12] + "..." if key else "none",
|
||||
}
|
||||
save_registration(entry)
|
||||
app.logger.info(f"Machine registered: {entry}")
|
||||
|
||||
return jsonify({
|
||||
"headscale_key": key,
|
||||
"login_server": f"https://{HEADSCALE_DOMAIN}",
|
||||
})
|
||||
|
||||
@app.route("/machines", methods=["GET"])
|
||||
def list_machines():
|
||||
if not validate_token(request):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
return jsonify(load_registrations())
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=9090)
|
||||
'';
|
||||
};
|
||||
|
||||
provisionerPython = pkgs.python3.withPackages (ps: [ ps.flask ]);
|
||||
|
||||
provisionerApp = pkgs.writeShellScriptBin "sovran-provisioner" ''
|
||||
exec ${provisionerPython}/bin/python3 ${provisionerScript}
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.sovranProvisioner = {
|
||||
enable = lib.mkEnableOption "Sovran Systems provisioning server";
|
||||
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Domain for the provisioning API (e.g. prov.sovransystems.com)";
|
||||
};
|
||||
|
||||
headscaleDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Domain for the Headscale coordination server (e.g. hs.sovransystems.com)";
|
||||
};
|
||||
|
||||
headscaleUser = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "sovran-deploy";
|
||||
description = "Headscale user/namespace for deployed machines";
|
||||
};
|
||||
|
||||
adminUser = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "admin";
|
||||
description = "Headscale user/namespace for admin workstations";
|
||||
};
|
||||
|
||||
keyExpiry = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "1h";
|
||||
description = "How long each auto-generated Headscale pre-auth key lives";
|
||||
};
|
||||
|
||||
rateLimitMax = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 10;
|
||||
description = "Max registrations per rate-limit window";
|
||||
};
|
||||
|
||||
rateLimitWindow = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 60;
|
||||
description = "Rate-limit window in seconds";
|
||||
};
|
||||
|
||||
stateDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/lib/sovran-provisioner";
|
||||
description = "Directory for provisioner state (enrollment token, logs)";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
# ── Headscale ────────────────────────────────────────────────────────────
|
||||
services.headscale = {
|
||||
enable = true;
|
||||
address = "127.0.0.1";
|
||||
port = 8080;
|
||||
settings = {
|
||||
server_url = "https://${cfg.headscaleDomain}";
|
||||
database = {
|
||||
type = "sqlite3";
|
||||
sqlite.path = "/var/lib/headscale/db.sqlite";
|
||||
};
|
||||
prefixes = {
|
||||
v4 = "100.64.0.0/10";
|
||||
v6 = "fd7a:115c:a1e0::/48";
|
||||
};
|
||||
derp = {
|
||||
server = {
|
||||
enabled = true;
|
||||
region_id = 999;
|
||||
stun_listen_addr = "0.0.0.0:3478";
|
||||
};
|
||||
urls = [];
|
||||
auto_update_enabled = false;
|
||||
};
|
||||
dns = {
|
||||
magic_dns = true;
|
||||
base_domain = "sovran.tail";
|
||||
nameservers.global = [ "1.1.1.1" "9.9.9.9" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ── Caddy reverse proxy ───────────────────────────────────────────────────
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"${cfg.headscaleDomain}" = {
|
||||
extraConfig = "reverse_proxy localhost:8080";
|
||||
};
|
||||
"${cfg.domain}" = {
|
||||
extraConfig = "reverse_proxy localhost:9090";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# ── Provisioner init service (generate token + create headscale users) ────
|
||||
systemd.services.sovran-provisioner-init = {
|
||||
description = "Initialize Sovran provisioner state";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "sovran-provisioner.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
mkdir -p ${cfg.stateDir}
|
||||
|
||||
# Auto-generate enrollment token on first boot if not already present
|
||||
TOKEN_FILE="${cfg.stateDir}/enroll-token"
|
||||
if [ ! -f "$TOKEN_FILE" ]; then
|
||||
${pkgs.openssl}/bin/openssl rand -hex 32 > "$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
echo "Generated new enrollment token: $(cat $TOKEN_FILE)"
|
||||
fi
|
||||
|
||||
# Ensure headscale users exist
|
||||
${pkgs.headscale}/bin/headscale users create ${cfg.headscaleUser} 2>/dev/null || true
|
||||
${pkgs.headscale}/bin/headscale users create ${cfg.adminUser} 2>/dev/null || true
|
||||
|
||||
# Initialize registrations log
|
||||
[ -f "${cfg.stateDir}/registrations.json" ] || echo "[]" > "${cfg.stateDir}/registrations.json"
|
||||
'';
|
||||
path = [ pkgs.headscale pkgs.openssl pkgs.coreutils ];
|
||||
};
|
||||
|
||||
# ── Provisioning API service ──────────────────────────────────────────────
|
||||
systemd.services.sovran-provisioner = {
|
||||
description = "Sovran Systems Provisioning API";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" "headscale.service" "sovran-provisioner-init.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
environment = {
|
||||
SOVRAN_ENROLL_TOKEN_FILE = "${cfg.stateDir}/enroll-token";
|
||||
SOVRAN_STATE_DIR = cfg.stateDir;
|
||||
HEADSCALE_USER = cfg.headscaleUser;
|
||||
KEY_EXPIRY = cfg.keyExpiry;
|
||||
HEADSCALE_DOMAIN = cfg.headscaleDomain;
|
||||
RATE_LIMIT_MAX = toString cfg.rateLimitMax;
|
||||
RATE_LIMIT_WINDOW = toString cfg.rateLimitWindow;
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${provisionerApp}/bin/sovran-provisioner";
|
||||
User = "sovran-provisioner";
|
||||
Group = "sovran-provisioner";
|
||||
StateDirectory = "sovran-provisioner";
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
# Give access to headscale CLI
|
||||
SupplementaryGroups = [ "headscale" ];
|
||||
};
|
||||
path = [ pkgs.headscale ];
|
||||
};
|
||||
|
||||
# ── System user for provisioner ───────────────────────────────────────────
|
||||
users.users.sovran-provisioner = {
|
||||
isSystemUser = true;
|
||||
group = "sovran-provisioner";
|
||||
home = cfg.stateDir;
|
||||
};
|
||||
users.groups.sovran-provisioner = {};
|
||||
|
||||
# ── Firewall ──────────────────────────────────────────────────────────────
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
networking.firewall.allowedUDPPorts = [ 3478 ]; # STUN for DERP
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user