Compare commits

..

28 Commits

Author SHA1 Message Date
Dustin b5455e519a Revert "collectd: Run collectd in privileged container"
Unfortunately, running *collectd* in a container is not going to work.
Although containers can be configured to share some of the host's
namespaces, one notable exception is the mount namespace.  Naturally,
containers must have their own mount namespace, which prevents them from
seeing filesystems that are actually mounted on the host.  For
*collectd*, this effectively makes the `df` plugin useless, which
ultimately prevents us from monitoring disk space.

This reverts commit 4048e5cc0a.
2023-10-04 20:50:30 -05:00
Dustin 5862ff4cc2 local_exporter: Remove After=zincati dependency
For some reason, the *zincati.service* unit has an `After=` dependency
on *multi-user.target*.  This creates a dependency loop between
*local_exporter.service* and *zincati.service* if the former has an
`After=` dependency on the latter an an (implicit) `Before=` dependency
on *multi-user.target*.  systemd will resolve this loop by removing one
or the other units from the bootup sequence, so either Zincati or the
local exporter will not start at boot.

We can avoid this dependency loop by removing the `After=` dependency
from *local_exporter.service*.  This may cause requests for Zincati
metrics to fail if it happens to come in after the local exporter starts
but before Zincati does, but this is unlikely to actually be an issue.
2023-10-04 20:50:30 -05:00
Dustin dd3be7a24a collectd: Restart service automatically
The *collectd.service* unit may fail for various reasons.  Notably, if
the container image is not present, it may fail to start if it is
activated before the network is fully available.  Using systemd's
automatic restart mechanism will help ensure *collectd* is running
whenever possible.
2023-10-04 20:50:30 -05:00
Dustin 40bde4df26 flash: Clean up/add support for RPi 3
Although the official Fedora CoreOS documentation only provides
instructions for running CoreOS on a Raspberry Pi 4, it does actually
work on older boards as well.  `coreos-installer` creates a GPT disk
label, which the older devices do not support, but this can be worked
around using a hybrid MBR label.

Unfortunately, after I put all the effort into refactoring this script
and adding support for the older devices, I realized that it was rather
pointless as those boards simply do not have enough memory to be useful
Kubernetes nodes.  I was hoping to move the Zigbee and ZWave controllers
to a Raspberry Pi 3, but these processes take way too much memory for
that.
2023-10-04 20:50:30 -05:00
Dustin 364f4fed50 common: Add config shared by all hosts
The `common.yaml` Butane configuration file merges in all the other
various Butane configuration files that we want to share amonst all
CoreOS machines.  These include the authorized SSH keys list, collectd
deployment, SSH host certificate configuration, etc.
2023-10-03 20:07:29 -05:00
Dustin 859deb0664 sshkeys: Trust certificates issued by the CA
Now that we have an internal SSH certificate authority, instead of
explicitly listing all M×N keys for each user and client machine, we can
list only the CA certificate in the SSH authorized keys file for the
*core* user.  This will allow any user who presents a valid, signed SSH
certificate for the *core* principal to log in.
2023-10-03 20:06:37 -05:00
Dustin 88f165363d step-ssh: Automatically issue/renew SSH host certs
The `ssh-bootstrap` script, which is run by the *ssh-bootstrap.service*
systemd unit, requests SSH host certificates for each of the existing
SSH host keys.  The certificates are issued by the *POST /sshkeys/sign*
operation of *dch-webhooks* web service.

The *step-ssh-renew* timer/service runs `step ssh renew`, in a
container, on a weekly basis to renew the SSH host certificate.  A host
certificate must already exist, and its private key is used to
authenticate to the CA server.

Since `step ssh renew` can only operate on one certificate/key file at a
time, the `step-ssh-renew@.container` defines a template unit.  The
template instance specifies the key type (i.e. `rsa`, `ecdsa`, or
`ed25519`), which in turn defines which certificate and private key file
to use.  The timer unit activates a target unit, which depends on the
concrete service units.  Note that the target unit must have
`StopWhenUnneeded=yes` so that it can be restarted again the next time
the timer fires.
2023-10-03 20:06:37 -05:00
Dustin 4048e5cc0a collectd: Run collectd in privileged container
Installing packages with `rpm-ostree` is somewhat problematic.  Notably,
if a new package needs an update of an already-installed package (e.g.
shared library), the new package cannot be installed until a new version
of CoreOS is published with the updated dependency.

In order for collectd to be effective, the container it runs in has to
have most isolation features disabled.  Most importantly, the PID, UTS,
and network namespaces need to be shared with the host, so that
*collectd* can "see" the actual values.  Additionally, the default
SELinux policy for containerized processes denies practically all of the
instrumentation syscalls *collectd* needs, so it needs to run in the
unconfined `spc_t` domain.  Finally, the `/run` directory needs to be
shared with the host, so *collectd* can communicate with various daemons
via UNIX sockets.
2023-10-03 20:03:21 -05:00
Dustin ebdf587de1 local_exporter: Exporter for Zincati metrics
Zincati provides Prometheus metrics via a Unix socket.  In order for
these to be scraped by `vmagent`, they need to be exposed over HTTP.
The `local_exporter` is designed to do specifically this.

Unfortunately, the Zincati metrics socket is only accessible by the
*zincati* user, so the `local_exporter` also needs to run as that user.
Hopefully, the user ID will remain consistent in future versions of
CoreOS.
2023-10-03 15:29:58 -05:00
Dustin 517151f2c8 sshkeys: Add Luma's SSH public key 2023-09-21 22:34:14 -05:00
Dustin cb282f0bce nvr1: Deploy notify-shutdown service 2023-09-21 22:34:14 -05:00
Dustin 11cd8ce8e9 notify-shutdown: Send a message on shutdown
Since Fedora CoreOS machines tend to reboot at seemingly random times
to apply updates, it would be nice to get a notification when they go
down.
2023-09-21 22:34:14 -05:00
Dustin 8828bb3069 nvr1: Deploy nginx
Deploying nginx on the NVR server to proxy for Frigate.
2023-09-21 22:34:14 -05:00
Dustin 9fd3aa0cd3 frigate: Configure nginx reverse proxy
Using nginx, we can expose the Frigate web server via HTTPS.  Since
Frigate has no built-in authentication, we need to use Authelia via the
nginx proxy auth feature.
2023-09-21 22:32:59 -05:00
Dustin d907b47db1 fetchcert: Add script to fetch certs from K8s
Since Fedora CoreOS machines are not managed by Ansible, we need another
way to keep the HTTPS certificate up-to-date.  To that end, I've added
the `fetchcert.sh` script, along with a corresponding systemd service
and timer unit, that will fetch the latest certificate from the Secret
resource managed by the Kubernetes API.  The script authenticates with
a long-lived bearer token associated with a particular Kubernetes
service account and downloads the current Secret to a local file.  If
the certificate in the Secret is different than the one already in
place, the certificate and key files are updated and nginx is reloaded.
2023-09-21 22:30:23 -05:00
Dustin 222f40426a nginx: Deploy nginx in a container 2023-09-21 22:29:51 -05:00
Dustin a32e6676eb nvr1: Install collectd
Also enabling the `md` plugin, which is disabled by default, to monitor
the software RAID array where Frigate recordings are stored.
2023-09-21 22:29:51 -05:00
Dustin d22a65c1bd collectd: Install and configure collectd
The `collectd.yaml` Butane configuration fragment configures the machine
to install *collectd* and its various plugin packages directly on the
host using `rpm-ostree` (via *install-packages.service*).
2023-09-21 22:29:51 -05:00
Dustin 2048713452 packages: Add framework for installing packages
Some machines may need to install multiple packages for separate use
cases.  Requiring each use case to define a systemd unit that runs
`rpm-ostree install` directly would be cumbersome and also quite slow,
as each one would have to run in turn.  Instead, now there is a single
*install-packages.service* which installs all of the packages listed in
files in `/etc/ignition/packages.d`.  On first boot, all files in that
directory are read and all the packages they list will be installed in a
single `rpm-ostree install` invocation.
2023-09-21 22:29:51 -05:00
Dustin 22c085b35d frigate: Disable systemd filesystem isolation
When`ProtectSystem` is enabled, systemd sets up a separate mount
namespace for the service.  Unfortunately, this appears to interfere
with Podman and prevents it from cleaning up containers on shutdown.
2023-09-21 22:29:51 -05:00
Dustin dffa17410f frigate: Enable Frigate+ integration
To keep the API key a secret, we're encrypting the environment file in
the repository with GnuPG.  The decrypted copy only lives in the work
tree and is never committed. Changes have to be re-encrypted and
committed.
2023-09-21 22:29:51 -05:00
Dustin b80bee461a frigate: Pass DRI device for hardware acceleration
Enabling hardware acceleration using VA-API dramatically reduces
`ffmpeg` CPU usage.  For this to work, the Frigate container needs
access to the DRI device node.
2023-09-19 10:46:52 -05:00
Dustin ddd137a2e9 frigate: Manage state dir with tmpfiles.d
Since *frigate.service* runs as root, the directories created by
`StateDirectory` are owned by root.  The processes inside the container,
therefore, cannot access them.  Thus, we have to use `systemd-tmpfiles`
to create the state directories with the appropriate permissions.
2023-09-19 10:44:34 -05:00
Dustin 2a0b23c9a8 meta: Add Makefile
When developing Butane/Ignition files, I frequently forget to update the
parent files after making a change to an included file.  This causes a
lot of wasted time re-provisioning, only to discover that my change
did not take effect.  To alleviate this, we'll use `make` with some
macro magic to scan the Butane files for their dependencies, and let it
generate whatever Ignition files need updating any time a dependant file
changes.

I've also added a "publish" step to the Makefile, since I also
frequently forget to upload the regenerated Ignition files to the
server, causing the same headaches.
2023-09-16 08:15:08 -05:00
Dustin 2efce551ba zram: Configure swap-on-zram
CoreOS does not enable swap-on-zram by default.
2023-09-16 08:15:08 -05:00
Dustin 1a60688cc1 nvr1: Deploy Frigate on the nvr1.p.b 2023-09-16 08:13:03 -05:00
Dustin 533cdc2c09 frigate: Run Frigate in a container
The *frigate* container must run as root, so we use a custom user
namespace to map root in the container to an unprivilged user on the
host.

For some reason, Podman (on CoreOS anyway) fails to stop a container
that uses a separate network namespace.  It reports "invalid argument"
when attempting to unmount the `netns` file, which then causes the
container to get "stuck" in `Storage` state.  Rebooting the host is
apparently the only way to get the container to start again correctly.
Fortunately, there's no particular reason to use an alternate network
namespace for Frigate, so it can use the host's network and avoid this
problem.
2023-09-16 08:06:07 -05:00
Dustin 1d71f874cf gasket-driver: Install Coral EdgeTPU driver
The *gasket-driver* container installs the `gasket` and `apex` kernel
modules, which provide the driver for the Google Coral EdgeTPU AI
accellerator module.  The container image must be built ahead of time,
of course, and contains modules built for a specific Fedora kernel
version.

The udev rule has two purposes: to set the permissions on the device
node so that any user on the system can access it, and to "tag" the
device so that systemd will generate a `.device` unit for it.  The
latter allows other units (e.g. Frigate) to express a `Requires=` and
`After=` dependency on the device unit, so that they do not start until
the driver is loaded.
2023-09-16 07:58:48 -05:00
44 changed files with 981 additions and 40 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.gpg diff=gpg

3
.gitignore vendored
View File

@ -1 +1,4 @@
*.ign *.ign
frigate.env
*.token
RPi4boot

1
65-apex.rules Normal file
View File

@ -0,0 +1 @@
SUBSYSTEM=="apex", MODE="0666", TAG+="systemd"

28
Makefile Normal file
View File

@ -0,0 +1,28 @@
.PHONY: \
all \
clean \
publish
.DEFAULT_GOAL := all
clean:
rm -f *.ign
define genrules
$(patsubst %.yaml,%.ign,$(1)): $(1) $$(shell sed -rn 's/.*local: (.*)/\1/p' $(1))
butane -d . $$< > $$@
all: $(patsubst %.yaml,%.ign,$(1))
endef
$(foreach t,$(wildcard *.yaml),$(eval $(call genrules,$(t))))
%.env: %.env.gpg
gpg2 --decrypt $< > $@
%.token: %.token.gpg
gpg2 --decrypt $< > $@
publish: \
nvr1.ign
rsync -rti $^ files.pyrocufflink.blue:public_html/

View File

@ -2,8 +2,9 @@
# vim: set sw=4 ts=4 sts=4 et : # vim: set sw=4 ts=4 sts=4 et :
inotifywait -e CLOSE_WRITE -m . \ inotifywait -e CLOSE_WRITE -m . \
| stdbuf -o 0 grep -F .yaml \ | stdbuf -o 0 grep -v .ign \
| while read _ _ f; do | while read _ _ f; do
printf 'Regenerating %s from %s ...\n' "${f%.yaml}.ign" "${f}" [ -f "${f}" ] || continue
butane -d . ${f} > ${f%.yaml}.ign printf '%s changed, running make ...\n' "${f}"
make && make publish
done done

57
collectd.yaml Normal file
View File

@ -0,0 +1,57 @@
variant: fcos
version: 1.4.0
ignition:
config:
merge:
- local: packages.yaml
storage:
files:
- path: /etc/ignition/packages.d/collectd
mode: 0644
contents:
inline: |
collectd
collectd-chrony
collectd-disk
collectd-sensors
collectd-write_prometheus
- path: /etc/collectd.d/df.conf
mode: 0644
contents:
inline: |
LoadPlugin df
<Plugin df>
FSType overlay
IgnoreSelected true
</Plugin>
- path: /etc/collectd.d/plugins.conf
mode: 0644
contents:
inline: |
LoadPlugin chrony
LoadPlugin cpufreq
LoadPlugin disk
LoadPlugin entropy
LoadPlugin processes
LoadPlugin swap
LoadPlugin tcpconns
LoadPlugin thermal
LoadPlugin uptime
- path: /etc/collectd.d/prometheus.conf
mode: 0644
contents:
inline: |
LoadPlugin write_prometheus
<Plugin write_prometheus>
Port 9103
</Plugin>
systemd:
units:
- name: collectd.service
enabled: true

11
common.yaml Normal file
View File

@ -0,0 +1,11 @@
variant: fcos
version: 1.4.0
ignition:
config:
merge:
- local: sshkeys.ign
- local: collectd.ign
- local: local_exporter.ign
- local: notify-shutdown.ign
- local: step-ssh.ign

36
fetchcert.service Normal file
View File

@ -0,0 +1,36 @@
[Unit]
Description=Fetch HTTPS certificate from Kubernetes Secret API
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/sh /etc/fetchcert/fetchcert.sh default pyrocufflink-cert
ProtectSystem=strict
ReadWritePaths=/etc/pki/nginx
CapabilityBoundingSet=CAP_CHOWN
DeviceAllow=
DevicePolicy=closed
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateUsers=yes
PrivateTmp=yes
ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources

54
fetchcert.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/sh
# vim: set sw=4 ts=4 sts=4 et :
namespace=$2
secret=$3
keyout=/etc/pki/nginx/private/server.key
crtout=/etc/pki/nginx/server.crt
tmpdir=$(mktemp -d)
trap 'rm -rf "${tmpdir}"' INT TERM QUIT EXIT
cat > "${tmpdir}"/ca.crt <<EOF
-----BEGIN CERTIFICATE-----
MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIyMDgwMTAyNTUzM1oXDTMyMDcyOTAyNTUzM1owFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMs6
2PUOzIClsAgPv1Mn9CTwzSFMntAn7OppwK5BQ4E5Vd1yMjz3p0uA1ZINv1ORorG0
mLl95C7y+CWUGPx+stHKQr/40sLGyypbX+AfjoPzHiDbIcbZEff8X5RwKqzmT9V7
Yt29KewADod0z+fqNYa62MJDaUunfwaV8kKFU/WJM8IKv2eJxAtWzvK3iHAFhx0j
Xo4TlyINL9V9UMKLf12w6CA3G41uZIBCN3G7aJEm++eGoMdrPZUXlbCpbSztO85/
hbulVs+0hCIxWiI+mRmB5OoWlRYL4jA45oK/RtpEqSwZ95zlGNAChmH7rb0pTtNf
N0/C2wKAEL4POLx9kscCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFHYActCjEWdtsA+Ju25gxJh/vaLQMBUGA1UdEQQO
MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAAfkYHecXUwyqvMSXmqr
ETqEzDCBini14s89VDhaDHOXBID9TKMVyeePdEYcPAJz3wo8fbx/+TL37K6hEuo+
7bUaamaumznsjg9L0Hth19GvuRKMXJlEpndRmE5K9hnaDLr94MLg9n1qGcEOt6tw
O6X5qqHf9AuuL39vt1+kSw6PeZZFZNMDZ8BdiTssw4btjQ2bsWu0wSiOSz/F8iRf
2vN5An5dheroDsFs4dZ9gnJ69TmqV1YqQxfRWqCxzfNJbgVm6AoBPwhL1JRuAU4N
3nCNoM9n2tLFDojT4un1778UVU91PtcBVdM9Nq+RC2jhXIyLBqsEK0ofOqFYqj3F
0EQ=
-----END CERTIFICATE-----
EOF
curl -fsSL \
-H 'Accept: application/json' \
-H "Authorization: Bearer $(cat /etc/fetchcert/token)" \
--cacert "${tmpdir}"/ca.crt \
https://kubernetes.pyrocufflink.blue:6443/api/v1/namespaces/${namespace}/secrets/${secret} \
-o "${tmpdir}"/secret.json \
|| exit
jq -r '.data["tls.key"]' "${tmpdir}"/secret.json \
| base64 -d > "${tmpdir}"/server.key
jq -r '.data["tls.crt"]' "${tmpdir}"/secret.json | \
base64 -d > "${tmpdir}"/server.crt
if [ "$(b2sum < "${tmpdir}"/server.crt)" != "$(b2sum < "${crtout}")" ]; then
install -m u=rw,go= -o 101 -g 101 "${tmpdir}"/server.key "${keyout}"
install -m u=rw,go=r -o root -g root "${tmpdir}"/server.crt "${crtout}"
chcon -t container_file_t "${keyout}" "${crtout}"
echo 'Certificate updated, reloading nginx ...' >&2
podman exec -it systemd-nginx nginx -s reload
fi

9
fetchcert.timer Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=Periodically fetch certificate from Kubernetes
[Timer]
OnCalendar=*-*-* 0:0:0
RandomizedDelaySec=8h
[Install]
WantedBy=timers.target

22
fetchcert.yaml Normal file
View File

@ -0,0 +1,22 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/fetchcert/fetchcert.sh
mode: 0755
contents:
local: fetchcert.sh
- path: /etc/systemd/system/fetchcert.service
mode: 0644
contents:
local: fetchcert.service
- path: /etc/systemd/system/fetchcert.timer
mode: 0644
contents:
local: fetchcert.timer
systemd:
units:
- name: fetchcert.timer
enabled: true

View File

@ -1,35 +0,0 @@
#!/bin/sh
# vim: set sw=4 ts=4 sts=4 et :
set -eu
ignition=${1}
dev=${2:-/dev/disk/by-id/usb-Generic_STORAGE_DEVICE_000000001206-0:0}
if [ -z "${ignition}" ]; then
printf 'usage: %s ignition\n' "${0##*/}" >&2
exit 2
fi
if [ $(id -u) -ne 0 ]; then
sudo=sudo
fi
${sudo} coreos-installer install \
-a aarch64 \
-s stable \
-i "${ignition}" \
--console ttyS0,115200n8 \
"${dev}"
sync; sync; sync
until [ -n "${efi_part}" ]; do
efi_part=$(
lsblk -o LABEL,PATH \
| awk '$1=="EFI-SYSTEM"{print $2}'
)
done
pmount "${efi_part}" coreos-efi
rsync -ah --ignore-existing RPi4boot/boot/efi/ /media/coreos-efi/
sync; sync; sync
pumount coreos-efi

139
flash.zsh Executable file
View File

@ -0,0 +1,139 @@
#!/bin/zsh
# vim: set ft=sh sw=4 ts=4 sts=4 et :
set -eu
function copy_firmware() {
local dev="$1"
local efi_part
local mountpoint=/run/media/coreos-efi
if [ ! -d RPi4boot ]; then
download_firmware
fi
printf 'Waiting for device to settle ...' >&2
until [ -n "${efi_part-}" ]; do
printf '.'
efi_part=$(
lsblk -o LABEL,PATH "${dev}" \
| awk '$1=="EFI-SYSTEM"{print $2}'
)
sleep 0.25
done
printf '\n'
mkdir -p "${mountpoint}"
mount "${efi_part}" "${mountpoint}"
rsync -rtiOh --ignore-existing RPi4boot/boot/efi/ "${mountpoint}"
sync; sync; sync
umount "${mountpoint}"
rmdir "${mountpoint}"
}
function download_firmware() {
local rpm
echo 'Downloading Raspberry Pi Firmware blobs ...'
mkdir RPi4boot
podman run --rm -it \
-v $(PWD)/RPi4boot:/RPi4boot:rw,z \
registry.fedoraproject.org/fedora:38 \
dnf install -y \
--downloadonly \
--forcearch=aarch64 \
--destdir=/RPi4boot \
uboot-images-armv8 \
bcm283x-firmware \
bcm283x-overlays
--
for rpm in RPi4boot/*.rpm; do
rpm2cpio "${rpm}" | cpio -idv -D RPi4boot/
done
cp RPi4boot/usr/share/uboot/rpi_3/u-boot.bin RPi4boot/boot/efi/rpi3-u-boot.bin
cp RPi4boot/usr/share/uboot/rpi_4/u-boot.bin RPi4boot/boot/efi/rpi4-u-boot.bin
cp RPi4boot/usr/share/uboot/rpi_arm64/u-boot.bin RPi4boot/boot/efi/rpi-u-boot.bin
}
function hybridize_gpt() {
local dev="$1"
echo 'Creating Hybrid MBR partition table ...'
sgdisk -h 2:EE "${dev}"
echo type=0c,bootable | sfdisk -Y mbr -N 1 "${dev}"
}
function install_coreos() {
local ignition="$1"
local dev="$2"
coreos-installer install \
-a aarch64 \
-s stable \
-i "${ignition}" \
--console ttyS0,115200n8 \
"${dev}"
sync; sync; sync
}
function parse_args() {
pi=4
while [ $# -gt 0 ]; do
case "$1" in
--pi)
shift
pi=${1}
;;
--pi=*)
pi=${1#--pi=}
;;
*)
if [ -z "${ignition-}" ]; then
ignition="${1}"
elif [ -z "${dev-}" ]; then
dev="${1}"
else
usage >&2
exit 2
fi
;;
esac
shift
done
if [ -z "${ignition-}" ]; then
usage >&2
exit 2
fi
if [ -z "${dev-}" ]; then
dev=/dev/disk/by-id/usb-Generic_STORAGE_DEVICE_000000001206-0:0
fi
}
function usage() {
printf 'usage: %s [--pi PI] ignition [device]\n' "${0##*/}"
}
parse_args "$@"
case "${pi}" in
0*|1)
printf 'Raspberry Pi model %s is not supported\n' "${pi}" >&2
exit 1
;;
esac
if [ $(id -u) -ne 0 ]; then
exec sudo "$0" "$@"
fi
install_coreos "${ignition}" "${dev}"
case "${pi}" in
2|3)
hybridize_gpt "${dev}"
;;
esac
copy_firmware "${dev}"

28
frigate.container Normal file
View File

@ -0,0 +1,28 @@
[Unit]
Description=Frigate NVR
Wants=network-online.target
After=network-online.target
Requires=dev-apex_0.device
After=dev-apex_0.device
[Container]
Image=ghcr.io/blakeblackshear/frigate:0.12.1
PodmanArgs=--uidmap 0:209:1
PodmanArgs=--gidmap 0:209:1
PodmanArgs=--uidmap 1:6000001:65536
PodmanArgs=--gidmap 1:6000001:65536
PodmanArgs=--shm-size 256m
EnvironmentFile=/etc/sysconfig/frigate
Volume=/var/lib/frigate/media:/media/frigate:rw,z
Volume=/var/lib/frigate/tmp:/tmp:rw,z
Volume=/var/lib/frigate/config:/config:rw,z
AddDevice=/dev/apex_0
AddDevice=/dev/dri/renderD128
Network=host
[Service]
UMask=0077
Restart=always
[Install]
WantedBy=multi-user.target

BIN
frigate.env.gpg Normal file

Binary file not shown.

77
frigate.nginx Normal file
View File

@ -0,0 +1,77 @@
# vim: set sw=4 ts=4 sts=4 et :
resolver 127.0.0.53;
set $upstream_authelia https://auth.pyrocufflink.blue/api/verify;
## Virtual endpoint created by nginx to forward auth requests.
location /authelia {
## Essential Proxy Configuration
internal;
proxy_pass $upstream_authelia;
## Headers
## The headers starting with X-* are required.
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Forwarded-Method $request_method;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
## Basic Proxy Configuration
proxy_pass_request_body off;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead
proxy_redirect http:// $scheme://;
proxy_http_version 1.1;
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
proxy_buffers 4 32k;
client_body_buffer_size 128k;
## Advanced Proxy Configuration
send_timeout 5m;
proxy_read_timeout 240;
proxy_send_timeout 240;
proxy_connect_timeout 240;
}
location / {
## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource.
auth_request /authelia;
## Set the $target_url variable based on the original request.
## Comment this line if you're using nginx without the http_set_misc module.
set_escape_uri $target_url $scheme://$http_host$request_uri;
## Uncomment this line if you're using NGINX without the http_set_misc module.
# set $target_url $scheme://$http_host$request_uri;
## Save the upstream response headers from Authelia to variables.
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
## Inject the response headers from the variables into the request made to the backend.
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Name $name;
proxy_set_header Remote-Email $email;
## If the subreqest returns 200 pass to the backend, if the subrequest returns 401 redirect to the portal.
error_page 401 =302 https://auth.pyrocufflink.blue/?rd=$target_url;
proxy_pass http://127.0.0.1:5000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
}

2
frigate.sysusers Normal file
View File

@ -0,0 +1,2 @@
g frigate 209
u frigate 209:209 "Frigate" /var/lib/frigate /sbin/nologin

4
frigate.tmpfiles Normal file
View File

@ -0,0 +1,4 @@
d /var/lib/frigate 0755 frigate frigate
d /var/lib/frigate/config 0755 frigate frigate
d /var/lib/frigate/media 0755 frigate frigate
d /var/lib/frigate/tmp 0755 frigate frigate

BIN
frigate.token.gpg Normal file

Binary file not shown.

34
frigate.yaml Normal file
View File

@ -0,0 +1,34 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/containers/systemd/frigate.container
mode: 0644
contents:
local: frigate.container
- path: /etc/sysusers.d/frigate.conf
mode: 0644
contents:
local: frigate.sysusers
- path: /etc/tmpfiles.d/frigate.conf
mode: 0644
contents:
local: frigate.tmpfiles
- path: /etc/sysconfig/frigate
mode: 0640
user:
id: 0
group:
id: 209
contents:
local: frigate.env
- path: /etc/nginx/default.d/frigate.conf
mode: 0644
contents:
local: frigate.nginx
systemd:
units:
- name: frigate.service
enabled: true

17
gasket-driver.container Normal file
View File

@ -0,0 +1,17 @@
[Unit]
Description=Install the gasket/apex kernel modules
Wants=network-online.target
After=network-online.target
[Container]
Image=git.pyrocufflink.net/containerimages/gasket-driver:%v
PodmanArgs=--privileged
[Service]
Type=oneshot
RemainAfterExit=true
ExecStop=/usr/sbin/rmmod apex
ExecStop=/usr/sbin/rmmod gasket
[Install]
WantedBy=multi-user.target

19
gasket-driver.yaml Normal file
View File

@ -0,0 +1,19 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/containers/systemd/gasket-driver.container
mode: 0644
contents:
local: gasket-driver.container
- path: /etc/udev/rules.d/65-apex.rules
mode: 0644
contents:
local: 65-apex.rules
systemd:
units:
- name: gasket-driver.service
enabled: true

17
install-packages.service Normal file
View File

@ -0,0 +1,17 @@
# vim: set ft=systemd :
[Unit]
Description=Install collectd
After=network-online.target
Wants=network-online.target
Before=zincati.service
ConditionPathExists=/etc/ignition/packages.d
ConditionPathExists=/etc/ignition/packages.installed
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh /etc/ignition/install-packages.sh
ExecStartPost=/bin/touch /etc/ignition/packages.installed
[Install]
WantedBy=multi-user.target

8
install-packages.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/sh
# vim: set sw=4 ts=4 sts=4 et :
if [ ! -d /etc/ignition/packages.d ]; then
exit 0
fi
cat /etc/ignition/packages.d/* | xargs rpm-ostree install --apply-live -y

View File

@ -3,8 +3,9 @@ version: 1.4.0
ignition: ignition:
config: config:
merge: merge:
- local: sshkeys.ign - local: common.ign
- local: kubelet.ign - local: kubelet.ign
storage: storage:
files: files:
- path: /etc/hostname - path: /etc/hostname

9
local_exporter.config Normal file
View File

@ -0,0 +1,9 @@
# vim: set ft=toml :
[service]
address = "0.0.0.0"
port = 9598
tls = false
[bridge.selectors.zincati]
kind = "uds"
path = "/run/zincati/public/metrics.promsock"

23
local_exporter.container Normal file
View File

@ -0,0 +1,23 @@
[Unit]
Description=Bridge for local Prometheus metrics
After=network.target
[Container]
Image=git.pyrocufflink.net/containerimages/local_exporter:latest
Exec=serve
Volume=/run:/run:rw
Volume=/etc/local_exporter:/etc/local_exporter:ro
PublishPort=9598:9598
# Must run as user "zincati"
User=981
ReadOnly=yes
VolatileTmp=yes
# container_t does not have permission to write to var_run_t
SecurityLabelDisable=yes
NoNewPrivileges=yes
[Service]
Restart=always
[Install]
WantedBy=multi-user.target

21
local_exporter.yaml Normal file
View File

@ -0,0 +1,21 @@
variant: fcos
version: 1.4.0
storage:
directories:
- path: /etc/local_exporter
files:
- path: /etc/containers/systemd/local_exporter.container
mode: 0644
contents:
local: local_exporter.container
- path: /etc/local_exporter/config.toml
mode: 0644
contents:
local: local_exporter.config
systemd:
units:
- name: local_exporter.service
enabled: true

76
nginx.conf Normal file
View File

@ -0,0 +1,76 @@
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
return 301 https://$host$request_uri;
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
# Settings for a TLS enabled server.
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name _;
root /usr/share/nginx/html;
ssl_certificate "/etc/pki/nginx/server.crt";
ssl_certificate_key "/etc/pki/nginx/private/server.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-CCM:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-CCM:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES256-CCM:AES128-GCM-SHA256:AES128-CCM:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-CCM:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-CCM:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:PSK-AES256-GCM-SHA384:PSK-CHACHA20-POLY1305:PSK-AES256-CCM:PSK-AES128-GCM-SHA256:PSK-AES128-CCM:PSK-AES256-CBC-SHA:PSK-AES128-CBC-SHA256:PSK-AES128-CBC-SHA:DHE-PSK-AES256-GCM-SHA384:DHE-PSK-CHACHA20-POLY1305:DHE-PSK-AES256-CCM:DHE-PSK-AES128-GCM-SHA256:DHE-PSK-AES128-CCM:DHE-PSK-AES256-CBC-SHA:DHE-PSK-AES128-CBC-SHA256:DHE-PSK-AES128-CBC-SHA:ECDHE-PSK-CHACHA20-POLY1305:ECDHE-PSK-AES256-CBC-SHA:ECDHE-PSK-AES128-CBC-SHA256:ECDHE-PSK-AES128-CBC-SHA:RSA-PSK-AES256-GCM-SHA384:RSA-PSK-CHACHA20-POLY1305:RSA-PSK-AES128-GCM-SHA256:RSA-PSK-AES256-CBC-SHA:RSA-PSK-AES128-CBC-SHA256:RSA-PSK-AES128-CBC-SHA;
ssl_prefer_server_ciphers on;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}

23
nginx.container Normal file
View File

@ -0,0 +1,23 @@
[Unit]
Description=nginx
Wants=network.target
After=network.target
[Container]
Image=docker.io/library/nginx:1.25
User=101
Group=101
Volume=%E/nginx:/etc/nginx:ro
Volume=%E/pki/nginx:/etc/pki/nginx:ro
Tmpfs=/var/cache/nginx
Tmpfs=/var/run/nginx
ReadOnly=true
AddCapability=CAP_NET_BIND_SERVICE
Network=host
[Service]
Restart=always
ExecReload=/usr/bin/podman exec -i systemd-%N nginx -s reload
[Install]
WantedBy=multi-user.target

22
nginx.yaml Normal file
View File

@ -0,0 +1,22 @@
variant: fcos
version: 1.4.0
ignition:
config:
merge:
- local: fetchcert.ign
storage:
files:
- path: /etc/containers/systemd/nginx.container
mode: 0644
contents:
local: nginx.container
- path: /etc/nginx/nginx.conf
mode: 0644
contents:
local: nginx.conf
directories:
- path: /etc/nginx/conf.d
- path: /etc/nginx/default.d
- path: /etc/pki/nginx

38
notify-shutdown.service Normal file
View File

@ -0,0 +1,38 @@
[Unit]
Description=Send notification on machine shutdown
RefuseManualStop=yes
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStop=/usr/bin/curl -d '%H is going down' https://ntfy.pyrocufflink.blue/alerts
DynamicUser=yes
CapabilityBoundingSet=
DeviceAllow=
DevicePolicy=closed
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateUsers=yes
PrivateTmp=yes
ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
[Install]
WantedBy=multi-user.target

14
notify-shutdown.yaml Normal file
View File

@ -0,0 +1,14 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/systemd/system/notify-shutdown.service
mode: 0644
contents:
local: notify-shutdown.service
systemd:
units:
- name: notify-shutdown.service
enabled: true

38
nvr1.yaml Normal file
View File

@ -0,0 +1,38 @@
variant: fcos
version: 1.4.0
ignition:
config:
merge:
- local: common.ign
- local: zram.ign
- local: gasket-driver.ign
- local: frigate.ign
- local: nginx.ign
kernel_arguments:
should_exist:
- console=ttyS0,115200
storage:
files:
- path: /etc/hostname
mode: 0644
contents:
inline: nvr1.pyrocufflink.blue
- path: /etc/systemd/system/var-lib-frigate.mount
mode: 0644
contents:
inline: |
[Mount]
What=/dev/disk/by-label/frigate
Where=/var/lib/frigate
- path: /etc/collectd.d/md.conf
mode: 0644
contents:
inline: |
LoadPlugin md
- path: /etc/fetchcert/token
mode: 0600
contents:
local: frigate.token

16
packages.yaml Normal file
View File

@ -0,0 +1,16 @@
variant: fcos
version: 1.4.0
storage:
directories:
- path: /etc/ignition/packages.d
mode: 0755
files:
- path: /etc/ignition/install-packages.sh
mode: 0755
contents:
local: install-packages.sh
- path: /etc/systemd/system/install-packages.service
mode: 0644
contents:
local: install-packages.service

13
ssh-bootstrap.service Normal file
View File

@ -0,0 +1,13 @@
# vim: set ft=systemd :
[Service]
Description=Bootstrap SSH host certificates
ConditionPathExistsGlob=!/etc/ssh/ssh_host_*_key-cert.pub
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/sh /etc/ssh/bootstrap.sh
[Install]
WantedBy=multi-user.target

35
ssh-bootstrap.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/sh
# vim: set sw=4 ts=4 sts=4 et :
gen_sshd_config() {
{
for x in ssh_host_*_key-cert.pub; do
printf 'HostCertificate /etc/ssh/%s\n' "${x}"
done
} > sshd_config.d/10-hostcertificate.conf
}
parse_response() {
jq -r '.certificates | to_entries | .[] | .key + " " + .value' \
| while read filename contents; do
[ -n "${filename}" ] || continue
echo "${contents}" > "${filename}" || return
done
}
request_sign() {
set -- \
https://bootstrap.pyrocufflink.blue/sshkeys/sign \
-H 'Accept: application/json' \
-F hostname=$(hostname -f)
for f in /etc/ssh/ssh_host_*_key.pub; do
set -- "$@" -F keys=@"${f}"
done
curl -fsSL "$@"
}
cd /etc/ssh || exit
request_sign | parse_response
gen_sshd_config
systemctl reload sshd

View File

@ -4,4 +4,4 @@ passwd:
users: users:
- name: core - name: core
ssh_authorized_keys: ssh_authorized_keys:
- sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAINZCN2cxMDwedJ1Ke23Z3CZRcOYjqW8fFqsooRus7RK0AAAABHNzaDo= dustin@rosalina.pyrocufflink.blue - cert-authority ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBImIoTTmhynCVy/vJ/Q2bWydzqVsvwhGvDgBbklw0eDt8UEbbP9HHPhxiMDtiAhbvRTg5BhYVAlR1MgdooT5dwQ=

3
step-ssh-renew.env Normal file
View File

@ -0,0 +1,3 @@
STEP_CA_URL=https://ca.pyrocufflink.blue:32599
STEP_ROOT=/etc/pki/ca-trust/source/anchors/dch-root-ca.crt
STEP_PROVISIONER=sshpop

6
step-ssh-renew.target Normal file
View File

@ -0,0 +1,6 @@
[Unit]
Description=Renew SSH host certificates
StopWhenUnneeded=yes
Wants=step-ssh-renew@ed25519.service
Wants=step-ssh-renew@ecdsa.service
Wants=step-ssh-renew@rsa.service

11
step-ssh-renew.timer Normal file
View File

@ -0,0 +1,11 @@
[Unit]
Description=Periodically renew SSH host certificates
[Timer]
Unit=%N.target
OnCalendar=Tue *-*-* 00:00:00
RandomizedDelaySec=48h
Persistent=yes
[Install]
WantedBy=timers.target

20
step-ssh-renew@.container Normal file
View File

@ -0,0 +1,20 @@
[Unit]
Description=Renew SSH host %I certificate
After=network-online.target
Wants=network-online.target
ConditionPathExists=/etc/ssh/ssh_host_%I_key-cert.pub
[Container]
ContainerName=step-ssh-renew-%I
Image=docker.io/smallstep/step-cli:0.25.0
EnvironmentFile=/etc/sysconfig/step-ssh-renew
Exec=step ssh renew -f /etc/ssh/ssh_host_%I_key-cert.pub /etc/ssh/ssh_host_%I_key
Volume=/etc/ssh:/etc/ssh:rw
Volume=/etc/pki:/etc/pki:ro
# Required in order to be able to write to /etc/ssh
SecurityLabelDisable=true
User=0
Group=0
[Service]
Type=oneshot

29
step-ssh.yaml Normal file
View File

@ -0,0 +1,29 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/ssh/bootstrap.sh
mode: 0755
contents:
local: ssh-bootstrap.sh
- path: /etc/containers/systemd/step-ssh-renew@.container
mode: 0644
contents:
local: step-ssh-renew@.container
- path: /etc/sysconfig/step-ssh-renew
mode: 0600
contents:
local: step-ssh-renew.env
- path: /etc/systemd/system/ssh-bootstrap.service
mode: 0644
contents:
local: ssh-bootstrap.service
- path: /etc/systemd/system/step-ssh-renew.target
mode: 0644
contents:
local: step-ssh-renew.target
- path: /etc/systemd/system/step-ssh-renew.timer
mode: 0644
contents:
local: step-ssh-renew.timer

10
zram.yaml Normal file
View File

@ -0,0 +1,10 @@
variant: fcos
version: 1.4.0
storage:
files:
- path: /etc/systemd/zram-generator.conf
mode: 0644
contents:
inline: |
[zram0]