Set up alpine test VPS with encrypted volume, installed KVM, created a first debian playground VM #1

Open
missytake wants to merge 7 commits from kvm-base into development
14 changed files with 300 additions and 4 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
pyinfra-debug.log
.venv/

View file

@ -12,3 +12,88 @@ or
- run `git pull` to fetch the newest version
- run `pyinfra @local deploy.py` to install/update `0x90.ssh_config` trustmebro
- run `pyinfra --dry inventory.py deploy.py` and check that you are on the same state that is already deployed
# Set up alpine on hetzner
This was only tested with a cloud VPS so far.
Source: <https://gist.github.com/c0m4r/e38d41d0e31f6adda4b4c5a88ba0a453>
(but it's less of a hassle than described there)
To create an alpine server on hetzner,
you need to first create a Debian VPS or something similar.
Then you boot into the rescue system.
Get the download link of the latest VIRTUAL x86_64 alpine iso
from <https://alpinelinux.org/downloads/>.
Login to the rescue system via console or SSH,
and write the ISO to the disk:
```
ssh root@xxxx:xxxx:xxxx:xxxx::1
wipefs -a /dev/sda
wget https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-virt-3.20.3-x86_64.iso # or whatever link you got from alpine
dd if=alpine-virt-3.20.3-x86_64.iso of=/dev/sda
reboot
```
Then open the server console (SSH doesn't work),
login to root (no password required),
and proceed with:
```
cp -r /.modloop /root
cp -r /media/sda /root
umount /.modloop /media/sda
rm /lib/modules
mv /root/.modloop/modules /lib
mv /root/sda /media
setup-alpine
```
Then select what you wish,
contrary to the guide above,
DHCP is actually fine.
The drive should be sda,
the installation type can be sys
(why go through the hassle).
Voilà! reboot and login.
Probably the first SSH login will be via root password,
as copy-pasting your public SSH key into the console doesn't work really.
Make sure the SSH config allows this
(and turn passwort root access off afterwards).
## Encrypting /var/lib/libvirt partition
**Status: tested with Hetzner VPS, not deployed in production yet**
Messing with file systems and partitions
should not be done by automation scripts,
so I created the LUKS-encrypted /dev/sdb partition manually.
(So far, /dev/sdb was added via a Hetzner volume,
but it can be any partition actually)
To create a partition in the VPS volume
(which was formatted to ext4 originally),
- I ran `fdisk /dev/sdb`,
- entered `o` to create a DOS partition table,
- added `n` to add a new primary partition, using all available space,
- and `w` to save to disk and exit.
Then I ran `cryptsetup luksFormat /dev/sdb1`
and entered the passphrase from `pass 0x90/ararat/sdb-crypt`
to create a LUKS volume.
Now I could decrypt the new volume with
`cryptsetup luksOpen /dev/sdb1 sdb_crypt`
and entering the passphrase from `pass 0x90/ararat/sdb-crypt`.
Finally, I ran `mkfs.ext4`
to create an ext4 file system
in the encrypted partition.

View file

@ -0,0 +1,100 @@
import os
from pyinfra import host, inventory
from pyinfra.operations import server, apk, files, openrc
from pyinfra.facts.server import Mounts
from pyinfra_util import get_pass
files.replace(
name="Enable TCP forwarding via SSH server",
path="/etc/ssh/sshd_config",
text="AllowTcpForwarding no",
replace="AllowTcpForwarding yes",
)
openrc.service(
name="Restart sshd",
service="sshd",
restarted=True,
)
files.replace(
name="Enable community repository",
path="/etc/apk/repositories",
text="#http://dl-cdn.alpinelinux.org/alpine/v3.20/community",
replace="http://dl-cdn.alpinelinux.org/alpine/v3.20/community",
)
apk.update()
apk.packages(
packages=["cryptsetup", "vim"]
)
mounts = host.get_fact(Mounts)
if "/var/lib/libvirt" not in mounts:
decryption_password = get_pass('0x90/ararat/sdb-crypt').strip()
if decryption_password:
server.shell(
name="Decrypt and mount /data",
commands=[
f" echo -n '{decryption_password}' | cryptsetup luksOpen --key-file - /dev/sdb1 sdb_crypt || true",
"mount /dev/mapper/sdb_crypt /var/lib/libvirt",
]
)
apk.packages(
packages=["libvirt-daemon", "qemu-img", "qemu-system-x86_64", "virt-install"]
)
openrc.service(
name="Start libvirtd",
service="libvirtd",
running=True,
enabled=False,
)
# add networking: https://wiki.alpinelinux.org/wiki/KVM#Networking
# modprobe tun
# echo "tun" >> /etc/modules-load.d/tun.conf
# cat /etc/modules | grep tun || echo tun >> /etc/modules
# if it doesn't exist, create debian base image (later: and other base images): https://mop.koeln/blog/creating-a-local-debian-vm-using-cloud-init-and-libvirt/#download-the-image
# for every active VM, if no image exists, run virt-install with the chosen base image and their cloud-init.yml file: https://mop.koeln/blog/creating-a-local-debian-vm-using-cloud-init-and-libvirt/#preparing-a-cloud-init-file
debian_image_path = "/var/lib/libvirt/images/debian-12-generic-amd64.qcow2"
files.download(
name="Download Debian 12 base image",
src="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2",
dest=debian_image_path,
)
for vm in inventory.groups.get("debian_vms"):
if os.path.isfile(f"{vm}/files/cloud-init.yml"):
files.put(
name=f"Upload {vm}-cloud-init.yml",
src=f"{vm}/files/cloud-init.yml",
dest=f"/root/{vm}-cloud-init.yml",
)
#virt-install
else:
if vm.data.get("authorized_keys"):
authorized_keys = "ssh_authorized_keys:\n - " + " - ".join(
[get_pass(f"0x90/ssh_keys/{admin}.pub") for admin in vm.data.get("authorized_keys")]
)
else:
authorized_keys = ""
files.template(
name=f"Upload {vm}-cloud-init.yml",
src="ararat/files/cloud-init.yml.j2",
dest=f"/root/{vm}-cloud-init.yml",
ssh_authorized_keys=authorized_keys,
)
memory = 1024
vcpus = 1
disk_size = 4
server.shell(
name=f"virt-install {vm}",
commands=[
f"virt-install --name {vm} --disk=size={disk_size},backing_store={debian_image_path} "
f"--memory {memory} --vcpus {vcpus} --cloud-init user-data=/root/{vm}-cloud-init.yml,disable=on "
"--network bridge=virbr0 --osinfo=debian12 || true",
]
)
# for every active VM, make sure an IP is assigned and traffic is passed to it

View file

@ -0,0 +1,25 @@
#cloud-config
keyboard:
layout: de
variant: nodeadkeys
locale: en_US
timezone: UTC
disable_root: false
users:
- name: root
shell: /bin/bash
{{ ssh_authorized_keys }}
- name: mop
# so our user can just sudo without any password
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
# content from $HOME/.ssh/id_rsa.pub on your host system
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZYJ91RLXRCQ4ZmdW6ucIltzukQ/k+lDOqlRIYwxNRv missytake@systemli.org
# Examples: https://cloudinit.readthedocs.io/en/latest/reference/examples_library.html#examples-library

View file

@ -1,5 +1,13 @@
targets = [
"@local",
("ararat.0x90.space", dict(ssh_port=42022)),
("baixun.0x90.space", dict(ssh_port=42023)),
localhost = "@local"
hypervisor = [("95.217.163.200", dict(ssh_user="root"))]
debian_vms = [
# "cloud",
(
"playground",
{
"authorized_keys": ["missytake", "hagi", "vmann"],
}
),
]

View file

@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: pyinfra-util
Version: 0.1

View file

@ -0,0 +1,7 @@
pyproject.toml
pyinfra_util/__init__.py
pyinfra_util/util.py
pyinfra_util.egg-info/PKG-INFO
pyinfra_util.egg-info/SOURCES.txt
pyinfra_util.egg-info/dependency_links.txt
pyinfra_util.egg-info/top_level.txt

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@
pyinfra_util

View file

@ -0,0 +1 @@
from .util import get_pass, deploy_tmux

View file

@ -0,0 +1,56 @@
"""
nginx deploy
"""
import subprocess
from pyinfra.operations import files, apt
def get_pass(filename: str) -> str:
"""Get the data from the password manager."""
try:
r = subprocess.run(["pass", "show", filename], capture_output=True)
except FileNotFoundError:
readme_url = "https://git.0x90.space/deltachat/secrets"
print(f"Please install pass and pull the latest version of our pass secrets from {readme_url}")
exit()
return r.stdout.decode('utf-8')
def deploy_tmux(home_dir="/root", escape_key="C-b", additional_config=[]):
apt.packages(
name="apt install tmux",
packages=["tmux"],
)
config = [
f"set-option -g prefix {escape_key}",
"set-option -g aggressive-resize on",
"set-option -g mouse on",
"set-option -g set-titles on",
"set-option -g set-titles-string '#I:#W - \"#H\"'",
"unbind-key C-b",
"bind-key ` send-prefix",
"bind-key a last-window",
"bind-key k kill-session",
]
for item in additional_config:
config.append(item)
for line in config:
files.line(
path=f"{home_dir}/.tmux.conf",
line=line,
)
dot_profile_add = """
# autostart tmux
if [ -t 0 -a -z "$TMUX" ]
then
test -z "$(tmux list-sessions)" && exec tmux new -s "$USER" || exec tmux new -A -s $(tty | tail -c +6) -t "$USER"
fi
"""
files.block(
name="connect to tmux session on login",
path=f"{home_dir}/.profile",
content=dot_profile_add,
try_prevent_shell_expansion=True,
)

View file

@ -0,0 +1,7 @@
[build-system]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "pyinfra-util"
version = "0.1"