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:
60
README.md
60
README.md
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
21
install.sh
21
install.sh
@@ -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
194
lib/certs.sh
Normal 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."
|
||||
}
|
||||
@@ -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
103
scripts/cert-bootstrap.sh
Executable 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."
|
||||
29
uninstall.sh
29
uninstall.sh
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user