Sign Sunshine certs with a 1Password-backed root CA

Replaces Sunshine's self-signed cert with one minted from a private root CA
whose key material lives in 1Password. Every host running install.sh fetches
the CA via 'op read', mints itself a host cert with SANs for <hostname>.lan
and the current LAN IP, and installs the CA into the system trust store.

Bootstrap (run once, anywhere)
- scripts/cert-bootstrap.sh: generates a 4096-bit RSA root CA (10y validity),
  uploads it as a Secure Note titled "Omarchy-Stream Root CA" in the Private
  vault with two fields: cert (text) and key (concealed). Refuses to overwrite
  an existing item without --force.

Per-host (lib/certs.sh)
- fetch_and_install_certs: reads op://Private/Omarchy-Stream Root CA/{cert,key}
  to a tmpfs-staged temp dir (XDG_RUNTIME_DIR), mints a host cert via openssl
  with serverAuth + clientAuth EKU, drops cert/key at ~/.config/sunshine/
  credentials/{cacert,cakey}.pem, installs the CA at
  /etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem and runs
  update-ca-trust.
- Idempotent: skips re-mint when on-disk cert is signed by the current CA,
  has the expected SANs, and isn't within 30 days of expiry. Override with
  FORCE_CERTS=1 or --force-certs.

install.sh
- Adds --no-certs, --force-certs flags; sources lib/certs.sh; runs cert step
  after permissions/config and before firewall so the service restart at the
  end of install picks up the new cert.

client/install-macos.sh
- After installing Moonlight, if `op` is available and signed in, fetches the
  CA and adds it as a trusted root to /Library/Keychains/System.keychain via
  `security add-trusted-cert -d -r trustRoot`. Skips cleanly when op isn't
  ready.

uninstall.sh
- Adds --remove-ca-trust to delete the system trust anchor. By default the
  CA is left in place since other tools may rely on it.

verify.sh
- Adds checks for: cert signed by omarchy-stream CA, cert >30 days from
  expiry, CA present in system trust store.

Docs
- README "Trusted TLS certs via 1Password" section: bootstrap flow, per-host
  flow, client trust matrix (Linux / macOS / iOS / Android / Apple TV),
  re-pairing note (first cert install on a host invalidates pinned Moonlight
  fingerprints), config env vars.
- client/README gains per-platform CA-trust install steps with concrete
  `op read` + platform-specific commands.
This commit is contained in:
2026-05-18 10:43:33 -06:00
parent 171ade4ff1
commit e878b392e4
8 changed files with 513 additions and 5 deletions

View File

@@ -78,11 +78,16 @@ Every step is idempotent. In order:
./install.sh --no-sunshine # client-only (install Moonlight only)
./install.sh --no-moonlight # host-only
./install.sh --from-source # build sunshine from source (default uses sunshine-bin)
./install.sh --no-certs # skip the 1Password-backed cert step
./install.sh --force-certs # re-mint the host cert even if current
./install.sh --doctor # verification only (no install)
./scripts/cert-bootstrap.sh # one-time: generate root CA, push to 1Password
./uninstall.sh # remove packages and udev rule, keep user data
./uninstall.sh --purge # also delete ~/.config/sunshine
./uninstall.sh --keep-moonlight
./uninstall.sh --remove-ca-trust # also remove the omarchy-stream CA from system trust
```
Environment overrides:
@@ -116,6 +121,61 @@ Per-vendor encoder picks:
Everything else (bitrate, paired clients, app launchers) is set via the web UI.
## Trusted TLS certs via 1Password
The installer can replace Sunshine's default self-signed cert with one minted from a private root CA whose key material lives in 1Password. Result: no more browser warning on `https://<host>.lan:47990`, and any tool that respects the system trust store (curl, openssl, browsers using NSS) trusts the host directly.
### One-time bootstrap (run on any one machine)
```bash
./scripts/cert-bootstrap.sh
```
This:
1. Generates a 4096-bit RSA root CA (10-year validity).
2. Uploads it to 1Password as a Secure Note titled **Omarchy-Stream Root CA** in the **Private** vault, with two fields:
- `cert` (text): the PEM cert.
- `key` (concealed): the PEM private key.
3. Prints the `op://Private/Omarchy-Stream Root CA/{cert,key}` references for confirmation.
Refuses to overwrite an existing CA item unless you pass `--force` — replacing the CA invalidates every host cert previously minted from it.
### Per-host (built into install.sh)
`./install.sh` runs a cert step that:
1. Reads the CA from 1Password (the `op` CLI must be signed in, `eval $(op signin)` first).
2. Mints a host cert with SAN entries for `<hostname>.lan` and the host's current LAN IP, signed by the CA, valid 365 days.
3. Writes the cert / key into `~/.config/sunshine/credentials/{cacert,cakey}.pem` (the same paths Sunshine uses by default).
4. Installs the CA cert into `/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem` and runs `update-ca-trust`.
5. The next service restart picks up the new cert.
The step is idempotent: if the on-disk cert is signed by the current CA, has the right SANs, and isn't expiring within 30 days, it's left alone. Force a re-mint with `--force-certs`.
Skip the cert step entirely with `--no-certs` — Sunshine will fall back to generating its own self-signed cert as before.
### Clients trust the CA too
| Client | How |
|---|---|
| Another Linux host | Same `install.sh` — the cert step installs the CA into `/etc/ca-certificates` regardless of whether the host runs Sunshine. |
| macOS | `client/install-macos.sh` fetches the CA from 1Password and runs `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain`. |
| iOS / iPadOS | Email yourself the CA PEM, install as a profile, then enable full trust in Settings → General → About → Certificate Trust Settings. Documented in `client/README.md`. |
| Android | Settings → Security → Encryption & credentials → Install from storage → CA certificate. Documented in `client/README.md`. |
### Cert replacement and Moonlight re-pairing
Sunshine uses one cert for both the web UI and the pairing handshake. Replacing the cert invalidates the fingerprint pinned by previously-paired Moonlight clients. **After the first cert install on a host, re-pair every Moonlight client once** via the web UI. After that, the cert is stable and re-runs of `install.sh` don't re-mint unless the cert is expiring.
### Configuration
| Variable | Default | Effect |
|---|---|---|
| `OP_VAULT` | `Private` | 1Password vault that holds the CA item |
| `OP_CA_ITEM` | `Omarchy-Stream Root CA` | Title of the CA item |
| `CERT_DAYS` | `365` | Host cert validity (days) |
| `FORCE_CERTS` | `0` | Set to `1` to re-mint even when the existing cert is current |
## Clients
The host-side installer handles Linux clients via `moonlight-qt`. For everything else, see `client/README.md` for per-platform install plus the first-pair walkthrough.

View File

@@ -51,6 +51,56 @@ to the same Apple ID.
Use `moonlight-qt` from your distro's package manager (Flatpak on Steam Deck,
`pacman`/`apt` elsewhere). It is the same Qt-based client as macOS and Windows.
## Trusting the omarchy-stream CA
If the host was installed with the cert step (default), its Sunshine web UI
uses a cert signed by a private root CA whose key is stored in 1Password. To
avoid the browser warning on `https://<host>.lan:47990`, install the CA on each
client device. This is a one-time step per device.
### macOS
`./install-macos.sh` handles this automatically if `op` is signed in. To do it
manually:
```bash
op read --no-newline "op://Private/Omarchy-Stream Root CA/cert" > /tmp/ca.pem
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain /tmp/ca.pem
rm /tmp/ca.pem
```
### iOS / iPadOS
1. In the 1Password app, open the **Omarchy-Stream Root CA** item in the
**Private** vault. Copy the `cert` field into the body of an email and send
it to yourself with the file extension `.crt` or `.pem` (Mail handles inline
PEM oddly; an attachment is cleanest).
2. Open the email on the iOS device and tap the attachment. iOS will offer to
install a configuration profile.
3. Settings → General → VPN & Device Management → Downloaded Profile → Install.
4. **Important second step**: Settings → General → About → Certificate Trust
Settings → enable full trust for the omarchy-stream Root CA. Without this,
Safari still warns.
### Android
1. From 1Password, save the CA `cert` field to a `.crt` file on the device
(Downloads folder is fine).
2. Settings → Security → Encryption & credentials → Install a certificate → CA
certificate.
3. Acknowledge the warning, select the file, give it a name.
(Path varies slightly by Android version / OEM skin — search Settings for "CA
certificate" if the path above doesn't match.)
### Apple TV
Cert profiles can be installed by emailing the cert to an account on the
Apple TV and tapping it, or by using Apple Configurator from a Mac. For
LAN-only use the self-signed warning is also tolerable — Apple TV has no
browser, so the cert is only relevant if you ever inspect the host directly.
## First pair
The pairing flow is the same on every client.

View File

@@ -51,6 +51,43 @@ else
exit 1
fi
# --- CA trust install (1Password-backed) ------------------------------------
# If `op` is available and signed in, fetch the omarchy-stream Root CA from
# 1Password and add it to the System keychain as a trusted root. This makes
# Safari / Chrome / curl trust the Sunshine web UI on every host without the
# self-signed warning. Skip cleanly if op isn't available or not signed in;
# the user can re-run after `eval $(op signin)`.
SKIP_CA="${SKIP_CA:-0}"
OP_VAULT="${OP_VAULT:-Private}"
OP_CA_ITEM="${OP_CA_ITEM:-Omarchy-Stream Root CA}"
if [[ $SKIP_CA -eq 1 ]]; then
info "Skipping CA install (SKIP_CA=1)"
elif ! command -v op >/dev/null 2>&1; then
info "1Password CLI ('op') not found. Skipping CA trust install."
info "Install it from https://1password.com/downloads/command-line/ and re-run for trusted certs."
elif ! op whoami >/dev/null 2>&1; then
info "1Password CLI is not signed in. Skipping CA trust install."
info "Sign in with 'eval \$(op signin)' and re-run for trusted certs."
else
step "Installing omarchy-stream CA into System keychain"
ca_tmp="$(mktemp -t omarchy-ca.XXXXXX.pem)"
trap 'rm -f "$ca_tmp"' EXIT
if op read --no-newline "op://${OP_VAULT}/${OP_CA_ITEM}/cert" >"$ca_tmp" 2>/dev/null && [[ -s "$ca_tmp" ]]; then
info "Fetched CA from 1Password (vault: ${OP_VAULT})"
info "Adding to /Library/Keychains/System.keychain — you may be prompted for your password."
if sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain "$ca_tmp"; then
ok "CA installed as trusted root."
else
warn "security add-trusted-cert failed. You can add the cert manually via Keychain Access."
fi
else
warn "Could not read 'cert' field from item '${OP_CA_ITEM}' in vault '${OP_VAULT}'."
warn "Run scripts/cert-bootstrap.sh on a Linux host first, then re-run this script."
fi
fi
step "Next steps"
info "App location: /Applications/Moonlight.app"
info "Launch: open -a Moonlight"

View File

@@ -26,6 +26,8 @@ source "$SCRIPT_DIR/lib/service.sh"
source "$SCRIPT_DIR/lib/verify.sh"
# shellcheck source=lib/headless.sh
source "$SCRIPT_DIR/lib/headless.sh"
# shellcheck source=lib/certs.sh
source "$SCRIPT_DIR/lib/certs.sh"
usage() {
cat <<EOF
@@ -42,12 +44,16 @@ Options:
--from-source Build Sunshine from source (equivalent to SUNSHINE_PKG=sunshine)
--headless Force headless streaming mode (wlr capture of HEADLESS-1)
--mirror Force mirror mode (KMS capture of the real display)
--no-certs Skip the 1Password-backed cert step (use Sunshine's self-signed)
--force-certs Re-mint the host cert even if the current one is valid
--doctor Run only the post-install verification checks
-h, --help Show this help
Environment overrides:
SUNSHINE_PKG AUR package to use for Sunshine (default: sunshine-bin)
Set to 'sunshine' to build from source instead.
OP_VAULT 1Password vault that holds the root CA (default: Private)
OP_CA_ITEM Item title in that vault (default: Omarchy-Stream Root CA)
EOF
}
@@ -58,6 +64,7 @@ INSTALL_MOONLIGHT=1
WRITE_CONFIG=1
DOCTOR_ONLY=0
MODE_OVERRIDE=""
INSTALL_CERTS=1
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -66,6 +73,8 @@ while [[ $# -gt 0 ]]; do
--no-moonlight) INSTALL_MOONLIGHT=0 ;;
--no-sunshine) INSTALL_SUNSHINE=0 ;;
--no-config) WRITE_CONFIG=0 ;;
--no-certs) INSTALL_CERTS=0 ;;
--force-certs) export FORCE_CERTS=1 ;;
--from-source) export SUNSHINE_PKG=sunshine ;;
--headless) MODE_OVERRIDE="headless" ;;
--mirror) MODE_OVERRIDE="mirror" ;;
@@ -133,6 +142,18 @@ main() {
info "Skipping sunshine.conf (--no-config)"
fi
if [[ $INSTALL_CERTS -eq 1 ]]; then
step "Installing CA-signed Sunshine cert from 1Password"
if fetch_and_install_certs; then
CERTS_REPLACED=1
else
warn "Cert install failed — falling back to Sunshine's self-signed cert."
warn "Run scripts/cert-bootstrap.sh to create the CA item, then re-run install.sh."
fi
else
info "Skipping cert step (--no-certs)"
fi
if [[ $FIREWALL -eq 1 ]]; then
step "Configuring firewall for Sunshine ports"
open_sunshine_ports

194
lib/certs.sh Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Sunshine TLS cert management, backed by a root CA stored in 1Password.
#
# Lifecycle:
# 1. cert-bootstrap.sh (one time, anywhere) generates the CA and uploads it
# to 1Password.
# 2. install.sh on each host calls fetch_and_install_certs, which:
# - reads the CA cert + key from 1Password (op CLI must be signed in)
# - mints a host cert with SANs for <hostname>.lan and the LAN IP
# - drops cert/key into ~/.config/sunshine/credentials/{cacert,cakey}.pem
# - installs the CA into /etc/ca-certificates so the host trusts itself
#
# Replacing Sunshine's cert invalidates the fingerprint that previously-paired
# Moonlight clients pinned. After first cert install, re-pair each client once.
: "${OP_VAULT:=Private}"
: "${OP_CA_ITEM:=Omarchy-Stream Root CA}"
: "${CERT_DAYS:=365}"
: "${CERT_RENEW_THRESHOLD_DAYS:=30}"
SUNSHINE_CRED_DIR="$HOME/.config/sunshine/credentials"
SUNSHINE_CERT="$SUNSHINE_CRED_DIR/cacert.pem"
SUNSHINE_KEY="$SUNSHINE_CRED_DIR/cakey.pem"
SYSTEM_TRUST_ANCHOR="/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem"
# --- 1Password helpers ----------------------------------------------------
op_require_signin() {
if ! command -v op >/dev/null 2>&1; then
err "1Password CLI ('op') not found on PATH."
err "Install it: yay -S 1password-cli"
return 1
fi
if ! op whoami >/dev/null 2>&1; then
err "Not signed in to 1Password CLI."
err "Sign in first (in this shell): ${BOLD}eval \$(op signin)${RESET}"
err "Or set OP_SERVICE_ACCOUNT_TOKEN in the environment for non-interactive use."
return 1
fi
return 0
}
# Read a single field from the CA item. Returns 0 if the field exists.
op_read_ca_field() {
local field="$1"
op read --no-newline "op://${OP_VAULT}/${OP_CA_ITEM}/${field}" 2>/dev/null
}
# --- Cert state inspection ------------------------------------------------
# True if the on-disk cert is valid and not expiring within the renew threshold.
cert_is_current() {
[[ -f "$SUNSHINE_CERT" ]] || return 1
local expires_in_seconds
expires_in_seconds=$(( CERT_RENEW_THRESHOLD_DAYS * 86400 ))
openssl x509 -in "$SUNSHINE_CERT" -checkend "$expires_in_seconds" -noout >/dev/null 2>&1
}
# True if the on-disk cert is signed by the CA at $1 (pem path).
cert_signed_by_ca() {
local ca_pem="$1"
[[ -f "$SUNSHINE_CERT" && -f "$ca_pem" ]] || return 1
openssl verify -CAfile "$ca_pem" "$SUNSHINE_CERT" >/dev/null 2>&1
}
cert_has_san_for() {
local needle="$1"
[[ -f "$SUNSHINE_CERT" ]] || return 1
openssl x509 -in "$SUNSHINE_CERT" -noout -ext subjectAltName 2>/dev/null \
| grep -qE "(DNS:${needle}\b|IP Address:${needle}\b)"
}
# --- Cert minting ---------------------------------------------------------
# Pick a non-link-local IPv4 on a globally-scoped interface.
detect_lan_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $4}' | cut -d/ -f1 | head -n1
}
# Mint a host cert under a temp dir using the CA files passed in.
# Args: tmpdir ca_cert ca_key hostname_lc lan_ip
# Writes: $tmpdir/host-cert.pem, $tmpdir/host-key.pem
mint_host_cert() {
local tmpdir="$1" ca_cert="$2" ca_key="$3" host_lc="$4" lan_ip="$5"
cat >"$tmpdir/host.cnf" <<EOF
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = ${host_lc}.lan
O = omarchy-stream
[v3_req]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${host_lc}.lan
IP.1 = ${lan_ip}
EOF
openssl genrsa -out "$tmpdir/host-key.pem" 2048 2>/dev/null
openssl req -new \
-key "$tmpdir/host-key.pem" \
-out "$tmpdir/host.csr" \
-config "$tmpdir/host.cnf" 2>/dev/null
openssl x509 -req \
-in "$tmpdir/host.csr" \
-CA "$ca_cert" -CAkey "$ca_key" -CAcreateserial \
-out "$tmpdir/host-cert.pem" \
-days "$CERT_DAYS" -sha256 \
-extensions v3_req -extfile "$tmpdir/host.cnf" 2>/dev/null
}
# --- System trust store ---------------------------------------------------
install_ca_to_system_trust() {
local ca_pem="$1"
# Idempotent: compare sha256 first to avoid pointless update-ca-trust runs.
if [[ -f "$SYSTEM_TRUST_ANCHOR" ]] \
&& cmp -s "$ca_pem" "$SYSTEM_TRUST_ANCHOR"; then
ok "CA already in system trust store"
return 0
fi
info "Installing CA into $SYSTEM_TRUST_ANCHOR"
as_root install -m 0644 "$ca_pem" "$SYSTEM_TRUST_ANCHOR"
as_root update-ca-trust extract >/dev/null
ok "System trust store refreshed (update-ca-trust)"
}
# --- Top-level orchestration ---------------------------------------------
# Called from install.sh. Honors FORCE_CERTS=1 to bypass freshness check.
fetch_and_install_certs() {
op_require_signin || return 1
local host_lc lan_ip
host_lc="${HOSTNAME_SHORT,,}"
lan_ip="$(detect_lan_ip)"
if [[ -z "$lan_ip" ]]; then
err "Could not detect a LAN IP — no globally-scoped IPv4 found."
return 1
fi
info "Cert subject: CN=${host_lc}.lan, SAN: DNS:${host_lc}.lan, IP:${lan_ip}"
# Stage CA in tmpfs (XDG_RUNTIME_DIR is tmpfs on Arch/systemd).
local tmpdir
tmpdir="$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/omarchy-certs.XXXXXX")"
chmod 700 "$tmpdir"
# shellcheck disable=SC2064
trap "rm -rf '$tmpdir'" EXIT
info "Fetching CA from 1Password (vault: ${OP_VAULT}, item: ${OP_CA_ITEM})"
if ! op_read_ca_field cert >"$tmpdir/ca-cert.pem" || [[ ! -s "$tmpdir/ca-cert.pem" ]]; then
err "Failed to read 'cert' field from 1Password item ${OP_CA_ITEM}."
err "Run scripts/cert-bootstrap.sh first to create the CA item."
return 1
fi
if ! op_read_ca_field key >"$tmpdir/ca-key.pem" || [[ ! -s "$tmpdir/ca-key.pem" ]]; then
err "Failed to read 'key' field from 1Password item ${OP_CA_ITEM}."
return 1
fi
chmod 600 "$tmpdir/ca-key.pem"
# Always install CA into system trust regardless of host-cert freshness.
install_ca_to_system_trust "$tmpdir/ca-cert.pem"
# Decide whether to mint a new host cert.
if [[ "${FORCE_CERTS:-0}" -eq 0 ]] \
&& cert_is_current \
&& cert_signed_by_ca "$tmpdir/ca-cert.pem" \
&& cert_has_san_for "${host_lc}.lan" \
&& cert_has_san_for "${lan_ip}"; then
ok "Sunshine cert is current, signed by CA, and matches expected SANs — skipping mint"
return 0
fi
info "Minting host cert (${CERT_DAYS} days)"
mint_host_cert "$tmpdir" "$tmpdir/ca-cert.pem" "$tmpdir/ca-key.pem" "$host_lc" "$lan_ip"
mkdir -p "$SUNSHINE_CRED_DIR"
install -m 0644 "$tmpdir/host-cert.pem" "$SUNSHINE_CERT"
install -m 0600 "$tmpdir/host-key.pem" "$SUNSHINE_KEY"
ok "Installed Sunshine cert at $SUNSHINE_CERT"
ok "Installed Sunshine key at $SUNSHINE_KEY"
warn "Cert replaced. Previously-paired Moonlight clients must re-pair via https://localhost:47990."
}

View File

@@ -101,6 +101,30 @@ verify_install() {
fi
fi
local sunshine_cert="$HOME/.config/sunshine/credentials/cacert.pem"
if [[ -f "$sunshine_cert" ]]; then
local issuer
issuer="$(openssl x509 -in "$sunshine_cert" -noout -issuer 2>/dev/null || true)"
if grep -qF 'omarchy-stream' <<<"$issuer"; then
ok "Sunshine cert is signed by the omarchy-stream CA"
if openssl x509 -in "$sunshine_cert" -checkend $((30 * 86400)) -noout >/dev/null 2>&1; then
ok "Sunshine cert valid for >30 days"
else
warn "Sunshine cert expires within 30 days — re-run install.sh to renew"
fi
else
info "Sunshine cert is Sunshine's self-signed default (no CA chain)"
fi
else
info "Sunshine cert not present yet (will be generated on first start)"
fi
if [[ -f /etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem ]]; then
ok "omarchy-stream CA installed in system trust store"
else
info "omarchy-stream CA not in system trust store (only matters if --no-certs was used)"
fi
if systemctl --user is-active --quiet sunshine.service; then
ok "sunshine.service is active"
if ss -ltn 2>/dev/null | grep -q ':47990 '; then

103
scripts/cert-bootstrap.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# One-time bootstrap: generate a root CA for omarchy-stream and upload it to
# 1Password. Run this on ONE machine; every host install.sh thereafter pulls
# the CA from 1Password to mint per-host certs.
#
# Re-running this script will refuse to overwrite an existing item unless
# --force is passed. The CA's private key is the trust root for every paired
# host; replacing it forces re-trust on every device.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
source "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/certs.sh
source "$SCRIPT_DIR/../lib/certs.sh"
FORCE=0
CA_VALID_DAYS=3650
CA_CN="omarchy-stream Root CA"
CA_O="omarchy-stream"
usage() {
cat <<EOF
Usage: $(basename "$0") [--force]
Generates a new root CA and uploads it to 1Password.
--force Overwrite an existing CA item in the vault (DANGEROUS: invalidates
every host cert previously signed by the existing CA).
Configuration (env vars, all optional):
OP_VAULT 1Password vault (default: Private)
OP_CA_ITEM Item title in that vault (default: Omarchy-Stream Root CA)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--force) FORCE=1 ;;
-h|--help) usage; exit 0 ;;
*) err "Unknown option: $1"; usage; exit 2 ;;
esac
shift
done
require_not_root
op_require_signin
# Check whether the item already exists. `op item get` exits 0 if found.
if op item get "$OP_CA_ITEM" --vault "$OP_VAULT" >/dev/null 2>&1; then
if [[ $FORCE -eq 0 ]]; then
err "Item '$OP_CA_ITEM' already exists in vault '$OP_VAULT'."
err "Re-running cert-bootstrap with --force will replace it, which invalidates"
err "every host cert minted from the existing CA. If you just want to refresh"
err "host certs on this machine, run install.sh (with FORCE_CERTS=1 to mint)."
exit 1
fi
warn "Overwriting existing CA item (--force given)"
op item delete "$OP_CA_ITEM" --vault "$OP_VAULT" >/dev/null
fi
tmpdir="$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/omarchy-ca-bootstrap.XXXXXX")"
chmod 700 "$tmpdir"
trap "rm -rf '$tmpdir'" EXIT
step "Generating root CA (4096-bit RSA, ${CA_VALID_DAYS} days)"
openssl genrsa -out "$tmpdir/ca-key.pem" 4096 2>/dev/null
chmod 600 "$tmpdir/ca-key.pem"
openssl req -new -x509 \
-key "$tmpdir/ca-key.pem" \
-out "$tmpdir/ca-cert.pem" \
-days "$CA_VALID_DAYS" \
-sha256 \
-subj "/CN=${CA_CN}/O=${CA_O}" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
ok "Generated CA cert and key"
info "CA fingerprint (SHA256):"
openssl x509 -in "$tmpdir/ca-cert.pem" -noout -fingerprint -sha256 \
| sed 's/^/ /'
step "Uploading to 1Password (vault: $OP_VAULT, item: $OP_CA_ITEM)"
op item create \
--category "Secure Note" \
--vault "$OP_VAULT" \
--title "$OP_CA_ITEM" \
"notesPlain=Root CA for omarchy-stream Sunshine certs. Bootstrapped $(date -Iseconds) on $(hostname -s)." \
"cert[text]=$(cat "$tmpdir/ca-cert.pem")" \
"key[concealed]=$(cat "$tmpdir/ca-key.pem")" \
>/dev/null
ok "Uploaded CA to 1Password"
info ""
info "References for install.sh / lib/certs.sh:"
info " op://${OP_VAULT}/${OP_CA_ITEM}/cert"
info " op://${OP_VAULT}/${OP_CA_ITEM}/key"
info ""
info "Next: on each host (including this one if it'll be a Sunshine host), run:"
info " ./install.sh"
info "and the cert step will pull the CA from 1Password and mint a host cert."

View File

@@ -9,16 +9,20 @@ source "$SCRIPT_DIR/lib/common.sh"
PURGE=0
KEEP_MOONLIGHT=0
REMOVE_CA_TRUST=0
while [[ $# -gt 0 ]]; do
case "$1" in
--purge) PURGE=1 ;;
--keep-moonlight) KEEP_MOONLIGHT=1 ;;
--purge) PURGE=1 ;;
--keep-moonlight) KEEP_MOONLIGHT=1 ;;
--remove-ca-trust) REMOVE_CA_TRUST=1 ;;
-h|--help)
cat <<EOF
Usage: $(basename "$0") [--purge] [--keep-moonlight]
Usage: $(basename "$0") [--purge] [--keep-moonlight] [--remove-ca-trust]
--purge Also delete ~/.config/sunshine and ~/.local/share/sunshine
--keep-moonlight Do not uninstall moonlight-qt
--purge Also delete ~/.config/sunshine and ~/.local/share/sunshine
--keep-moonlight Do not uninstall moonlight-qt
--remove-ca-trust Remove the omarchy-stream CA from /etc/ca-certificates
(default: leave it — other hosts/services may rely on it)
EOF
exit 0 ;;
*) err "Unknown option: $1"; exit 2 ;;
@@ -57,9 +61,24 @@ if [[ -f /etc/udev/rules.d/60-uinput.rules ]]; then
as_root udevadm control --reload-rules
fi
if [[ $REMOVE_CA_TRUST -eq 1 ]]; then
step "Removing omarchy-stream CA from system trust store"
anchor="/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem"
if [[ -f "$anchor" ]]; then
as_root rm -f "$anchor"
as_root update-ca-trust extract >/dev/null
ok "Removed $anchor and refreshed trust store"
else
info "CA anchor not present; nothing to remove"
fi
fi
if [[ $PURGE -eq 1 ]]; then
step "Purging Sunshine user data"
rm -rf "$HOME/.config/sunshine" "$HOME/.local/share/sunshine"
fi
ok "Uninstall complete. Firewall rules and 'input' group membership were left in place."
if [[ $REMOVE_CA_TRUST -eq 0 ]]; then
info "The omarchy-stream CA was left in /etc/ca-certificates (--remove-ca-trust to drop it)."
fi