This commit is contained in:
Matthew Tran 2025-05-04 22:36:28 -07:00
parent e774f3ebdc
commit 1788061642
8 changed files with 88 additions and 66 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ __pycache__
config/server.json config/server.json
config/*.bu config/*.bu
config/*.ign config/*.ign
config/*.iso
# website # website
website/sendgrid.key website/sendgrid.key

View File

@ -14,47 +14,34 @@ Services deployed on [matthewtran.com](https://matthewtran.com).
## setup ## setup
1. Designate one computer as the configuration server. Create `config/server.json` which contains the configuration for the server to be provisioned. Reference `config/server.default` for fields. Run the following. 1. Create `config/server.json` and run `config/provision.py`.
- `config/provision.py` 2. On the server to be provisioned, boot a [Fedora CoreOS installation media](https://fedoraproject.org/coreos/download?stream=stable) and run the install command.
2. Create a [Fedora CoreOS](https://fedoraproject.org/coreos/download?stream=stable) installation media and boot it on the server to be provisioned. Run the following on it and reboot. 3. To configure the OpenWrt router, run `/opt/router.py --provision <interface>` on the server. Then reboot the router and server.
- `sudo coreos-installer install /dev/<boot drive> --ignition-url http://<config server ip>/server.ign --insecure-ignition` 4. Add the following DNS entries at the registrar.
## update
quick dev => scp dockerfiles => rebuild locally
final dev => reprovision + wipe home so images rebuilds
logs => sudo -u game podman logs container
1. Install [Ubuntu Desktop 24.04.1 LTS](https://ubuntu.com/download/desktop) with TPM-backed FDE. Server currently has a [bug](https://bugs.launchpad.net/ubuntu/+source/cryptsetup/+bug/1980018) that makes TPM-backed FDE hard.
- You may need to manually enable IPv6 on the network connection. Use `Automatic` not `Automatic, DHCP only`.
- Add an SSH key if you need remote access, setup will disable password authentication.
- Clone this repo and `cd` into it.
2. Set up the server.
- `scripts/setup_server.py`
3. Set up the OpenWrt 24.10 router. Copy SSH keys first to make it easier. Use a strong root password.
- `scripts/setup_router.py <interface>`
4. Reboot the router and server.
5. Configure, build, and start services.
- Create `website/sendgrid.key` with a [SendGrid API key](https://app.sendgrid.com/settings/api_keys).
- Create `terraria/config.txt` and `terraria/password.txt` if needed.
- Create `nas/mounts.json` which contains a list of `"<name>":"<directory>"` for the SMB share.
- Create `nas/users.json` which contains a list of `"<user>":"<password>"` for the SMB share.
- `scripts/setup_repo.py`
- Restore backups if needed. Make sure to set correct ownership. For example, `chown -R 2000:2000 website/gitea`.
- `docker compose build`
- `docker compose up -d`
6. Optionally, add additional drives. This script formats the drive as LUKS/BTRFS with the key file stored in `/opt/luks` and auto-mounts on boot. Make sure to backup the key file elsewhere.
- `scripts/setup_drive.py <drive> <mount>`
7. Optionally, run `scripts/setup_peer.py <name>` for each WireGuard client.
8. Optionally, add the following DNS entries at the registrar.
| hosts | type | data | | hosts | type | data |
| ----------------------- | ------ | ------------------------ | | ----------------------- | ------ | ------------------------ |
| `@`, `git`, `wg`, `www` | `A` | `<public IPv4>` | | `@`, `git`, `wg`, `www` | `A` | `<public IPv4>` |
| `@`, `git`, `www` | `AAAA` | `<delegated prefix>::69` | | `@`, `git`, `www` | `AAAA` | `<delegated prefix>::69` |
| `wg` | `AAAA` | `<delegated prefix>::1` | | `wg` | `AAAA` | `<delegated prefix>::1` |
5. Optionally, run `config/peer.py` for each WireGuard client.
## backup ## development
1. For quick iteration, run `config/update.py`. This copies over sources, rebuilds images, and restarts containers.
2. After development, it's best to reprovision (see above) with `wipe=false` for drives you want to keep. Then run `/opt/router.py` on the server and reboot.
## maintenance
logs => sudo -u game podman logs container
TODO mdadm status? backup?
Run `scripts/backup.py` and save the resultant `data.zip` somewhere. Also run the following commands for BTRFS maintenance. I should probably automate this. Run `scripts/backup.py` and save the resultant `data.zip` somewhere. Also run the following commands for BTRFS maintenance. I should probably automate this.
``` ```

View File

@ -1,4 +1,4 @@
#!/usr/bin/sudo /usr/bin/python3 #!/usr/bin/env python3
import os import os
import shutil import shutil
@ -14,3 +14,10 @@ if __name__ == "__main__":
"website/gitea", "website/gitea",
], check=True) ], check=True)
shutil.chown(out, os.getlogin(), os.getlogin()) shutil.chown(out, os.getlogin(), os.getlogin())
# TODO backup and scp
# TODO restore
# fix permissions
# no wipe folders we didn't save
# may need to chown 777 for gitea
# TODO router wipe + peer.py

View File

@ -3,7 +3,7 @@
import subprocess import subprocess
import sys import sys
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
from setup_router import WG_IPV4, WG_IPV6, run, key from router import WG_IPV4, WG_IPV6, run, key
def ips(): def ips():
try: try:

View File

@ -2,7 +2,11 @@
import base64 import base64
import json import json
import http.server
import os
import secrets import secrets
import socket
import socketserver
import subprocess import subprocess
import yaml import yaml
from pathlib import Path from pathlib import Path
@ -40,8 +44,8 @@ PORTS = {
} }
def check_keys(): def check_keys():
if "home_key" not in cfg["core"]: if "stash_key" not in cfg["core"]:
print(f'cfg["core"]["home_key"] doesn\'t exist, try "{base64.b64encode(secrets.token_bytes(64)).decode("utf-8")}"') print(f'cfg["core"]["stash_key"] doesn\'t exist, try "{base64.b64encode(secrets.token_bytes(64)).decode("utf-8")}"')
exit(1) exit(1)
for i, d in enumerate(cfg["drives"]): for i, d in enumerate(cfg["drives"]):
if "key" not in d: if "key" not in d:
@ -58,11 +62,11 @@ def add_root_drive():
{ {
"number": 4, "number": 4,
"label": "root", "label": "root",
"size_mib": 16384, "size_mib": 65536,
"resize": True, "resize": True,
}, },
{ {
"label": "home", "label": "stash",
"size_mib": 0, "size_mib": 0,
}, },
], ],
@ -78,10 +82,10 @@ def add_root_drive():
"clevis": { "tpm2": True }, "clevis": { "tpm2": True },
}, },
{ {
"name": "home", "name": "stash",
"device": "/dev/disk/by-partlabel/home", "device": "/dev/disk/by-partlabel/stash",
"wipe_volume": cfg["core"]["home_wipe"], "wipe_volume": cfg["core"]["stash_wipe"],
"key_file": { "inline": base64.b64decode(cfg["core"]["home_key"]) }, "key_file": { "inline": base64.b64decode(cfg["core"]["stash_key"]) },
}, },
], ],
"filesystems": [ "filesystems": [
@ -92,14 +96,20 @@ def add_root_drive():
"label": "root", "label": "root",
}, },
{ {
"path": "/var/home", "path": "/var/mnt/stash",
"device": "/dev/mapper/home", "device": "/dev/mapper/stash",
"format": "xfs", "format": "ext4",
"wipe_filesystem": cfg["core"]["home_wipe"], "wipe_filesystem": cfg["core"]["stash_wipe"],
"with_mount_unit": True, "with_mount_unit": True,
}, },
], ],
"directories": [], "directories": [
{
"path": f"/var/mnt/stash",
"user": { "name": "core" },
"group": { "name": "core" },
},
],
"files": [], "files": [],
} }
@ -152,6 +162,7 @@ def add_packages():
"ExecStart=/usr/bin/rpm-ostree install -y --allow-inactive " + " ".join([ "ExecStart=/usr/bin/rpm-ostree install -y --allow-inactive " + " ".join([
"avahi", "avahi",
"htop", "htop",
"python3",
"vim", "vim",
]), ]),
"ExecStart=/bin/touch /etc/rpm/%N.stamp", "ExecStart=/bin/touch /etc/rpm/%N.stamp",
@ -228,6 +239,11 @@ def copy_source():
"user": { "name": user }, "user": { "name": user },
"group": { "name": user }, "group": { "name": user },
}) })
but["storage"]["files"].append({
"path": "/var/opt/router.py",
"mode": 0o755,
"contents": { "inline": open("config/router.py", "rb").read() },
})
def build_images(): def build_images():
but["storage"]["directories"].append({ "path": "/etc/containers/systemd/users" }) but["storage"]["directories"].append({ "path": "/etc/containers/systemd/users" })
@ -365,17 +381,17 @@ if __name__ == "__main__":
run_containers() run_containers()
advertise_services() advertise_services()
# generate ignition file
# TODO update router scripts bc DUID => make fixed??
# TODO script to backup => restore backup if desired => fix permissions
# may need to chown 777 for gitea restore
# TODO generate ISO, else nginx if --insecure
# TODO full wipe test (wipefs all) => check folder permissions secure
with open("config/server.bu", "w") as f: with open("config/server.bu", "w") as f:
f.write(yaml.dump(but, sort_keys=False)) f.write(yaml.dump(but, sort_keys=False))
subprocess.check_output(["butane", "-p", "-s", "-o", "config/server.ign", "config/server.bu"]) subprocess.check_output(["butane", "-p", "-s", "-o", "config/server.ign", "config/server.bu"])
print("NOTE - TPM may need to be cleared after enough provisions.") # host ignition file
ip = socket.gethostbyname(socket.gethostname())
print("WARNING - Using unencrypted connections without authentication, ensure LAN is secure!") print("WARNING - Using unencrypted connections without authentication, ensure LAN is secure!")
print("NOTE - TPM may need to be cleared after enough provisions.")
print(f"NOTE - Run \"sudo coreos-installer install /dev/<boot drive> --ignition-url http://{ip}/server.ign --insecure-ignition\"")
print("NOTE - Starting HTTP server, ctrl-c to exit...")
os.chdir("config")
with socketserver.TCPServer(("", 80), http.server.SimpleHTTPRequestHandler) as httpd:
httpd.serve_forever()

View File

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import hashlib import hashlib
import subprocess import subprocess
import sys
DP_LEN = 64 # xfinity delegated prefix length DP_LEN = 64 # xfinity delegated prefix length
WRT_ULA = "fd16:8f4d:f516::" # OpenWrt random WRT_ULA = "fd16:8f4d:f516::" # OpenWrt random
@ -30,7 +30,19 @@ def key():
return (pub.decode("utf-8"), priv.decode("utf-8")) return (pub.decode("utf-8"), priv.decode("utf-8"))
if __name__ == "__main__": if __name__ == "__main__":
ETH = sys.argv[1] # e.g. enp5s0 parser = argparse.ArgumentParser()
parser.add_argument("--provision", type=str, help="network interface to set static IP for, e.g. enp5s0")
args = parser.parse_args()
# update static IP
if args.provision is None:
run([
f"uci set dhcp.@host[0].duid='{duid()}'",
"uci commit dhcp",
"service dnsmasq restart",
"service odhcpd restart",
])
exit(0)
# basic setup # basic setup
run([ run([
@ -43,8 +55,7 @@ if __name__ == "__main__":
# static IP # static IP
run([ run([
"uci add dhcp host", "uci add dhcp host",
"uci set dhcp.@host[-1].name='matt-ryzen'", f"uci set dhcp.@host[-1].mac='{mac(args.provision)}'",
f"uci set dhcp.@host[-1].mac='{mac(ETH)}'",
f"uci set dhcp.@host[-1].ip='{IPV4}'", f"uci set dhcp.@host[-1].ip='{IPV4}'",
f"uci set dhcp.@host[-1].duid='{duid()}'", f"uci set dhcp.@host[-1].duid='{duid()}'",
f"uci set dhcp.@host[-1].hostid='{IPV6}'", f"uci set dhcp.@host[-1].hostid='{IPV6}'",

View File

@ -4,8 +4,8 @@
"ssh_keys": [ "ssh_keys": [
"ssh-ed25519 AAAA..." "ssh-ed25519 AAAA..."
], ],
"home_key": "<LUKS key>", "stash_key": "<LUKS key>",
"home_wipe": false, "stash_wipe": false,
"data_dir": "/var/home/core/matthewtrancom_data" "data_dir": "/var/home/core/matthewtrancom_data"
}, },
"drives": [ "drives": [

View File

@ -19,7 +19,7 @@ update() {
sleep 86400 sleep 86400
} }
update & update &
./ip.py & python3 ip.py &
# run server # run server
nginx -c ~/server.conf nginx -c ~/server.conf