Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ on:
push:
branches: [main]
pull_request:
# weekly, to notice a pinned file that changed on GitHub or an app with a newer release
schedule:
- cron: '7 6 * * 1'

permissions:
contents: read
Expand All @@ -12,7 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- run: shellcheck -s sh install.sh
- run: sh -n install.sh
- run: shellcheck -s sh selfhostit
- run: sh -n selfhostit
- run: shellcheck -s sh install.sh selfhostit scripts/pin-apps.sh
- run: sh -n install.sh && sh -n selfhostit && sh -n scripts/pin-apps.sh

# Every file install.sh downloads must still have the SHA-256 it pins.
pins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- run: scripts/pin-apps.sh --check
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: release

# Publishes a release of the installer: install.sh and the selfhostit command as
# downloads, their SHA-256 sums, and a signed attestation of the workflow and commit
# they came from. People install from
# https://github.com/selfhostit/docs/releases/latest/download/install.sh.
#
# gh workflow run release.yml --repo selfhostit/docs -f version=v1.2.0
#
# Releases are immutable, so the files go in when the release is created.
on:
workflow_dispatch:
inputs:
version:
description: Version tag, like v1.2.0
required: true

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # to create the release and its tag
id-token: write # to sign the attestation
attestations: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Check the version
run: |
echo "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || { echo "not a version like v1.2.0: $VERSION"; exit 1; }
env:
VERSION: ${{ inputs.version }}
- name: Check the pinned app files
run: scripts/pin-apps.sh --check
- name: SHA-256 sums
run: sha256sum install.sh selfhostit > SHA256SUMS && cat SHA256SUMS
- name: Attest the files
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
install.sh
selfhostit
- name: Publish the release
run: |
gh release create "$VERSION" install.sh selfhostit SHA256SUMS \
--target "$GITHUB_SHA" --title "Installer $VERSION" --generate-notes
env:
VERSION: ${{ inputs.version }}
GH_TOKEN: ${{ github.token }}
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ another program already has it, the app gets the next free port, and keeps that
On a Raspberry Pi, a Linux server or a NAS you can reach over SSH:

```sh
curl -fsSL https://raw.githubusercontent.com/selfhostit/docs/main/install.sh | sh
curl -fsSL https://github.com/selfhostit/docs/releases/latest/download/install.sh | sh
```

It installs Docker if it is missing (on Linux), asks which apps you want and where to keep
Expand All @@ -31,28 +31,36 @@ It can also set up [Tailscale](phones.md#tailscale-recommended), so your phones
over HTTPS, which they need to install an app and get notifications. You only open a link to
sign in; there are no keys or settings to copy. Then [put the apps on your phones](phones.md).

Prefer to read a script before you run it? Download it, read it, then run it:
Prefer to read and check a script before you run it? Download it, verify it, read it, then
run it:

```sh
curl -fsSLO https://raw.githubusercontent.com/selfhostit/docs/main/install.sh
curl -fsSLO https://github.com/selfhostit/docs/releases/latest/download/install.sh
gh attestation verify install.sh --repo selfhostit/docs
less install.sh
sh install.sh
```

The installer is a signed release, and it installs a pinned release of each app, checking
every file it downloads against its SHA-256.

[Security and privacy](security.md) says step by step what it does, what the apps protect
and what you are trusting.

## Security in short

The apps keep their data on your machine, in one folder each, with no selfhostit cloud and no
analytics. They listen on `127.0.0.1` only and are reached through Tailscale or your own
reverse proxy, never a forwarded port. Containers drop every Linux capability, a new install
asks for a one-time setup code, sign-ins are rate limited and the web apps send a Content
Security Policy. CI runs the tests on every change, with Actions pinned to commit hashes.
Updates follow versioned releases, and each image has a signed attestation of the commit that
built it. Not done yet: an independent audit.
Details, with links to the code: [Security and privacy](security.md). Report a problem to
kalfasyan@pm.me ([policy](https://github.com/selfhostit/.github/blob/main/SECURITY.md)).
reverse proxy, never a forwarded port. Containers run as a normal user with no Linux
capabilities and a read-only filesystem, a new install asks for a one-time setup code,
passwords are stored as bcrypt hashes, sign-ins are rate limited and the web apps send a
Content Security Policy. Every change, and every week, is scanned with CodeQL, govulncheck,
Grype and gitleaks. Updates follow versioned releases, each image has a signed attestation
of the commit that built it, and the installer pins each app's release with SHA-256 checks.
Not done yet: an independent audit.
Details, with links to the code: [Security and privacy](security.md). Report a problem
privately through the app's Security tab on GitHub or to kalfasyan@pm.me
([policy](https://github.com/selfhostit/.github/blob/main/SECURITY.md)).

## Guides

Expand Down
95 changes: 83 additions & 12 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
# Installs or updates selfhostit apps on this machine with Docker: a Raspberry Pi,
# a Linux server or VPS, or a NAS with SSH and Docker (Synology, QNAP, Unraid, ...).
#
# curl -fsSL https://raw.githubusercontent.com/selfhostit/docs/main/install.sh | sh
# curl -fsSL https://github.com/selfhostit/docs/releases/latest/download/install.sh | sh
#
# To read it before running it, download it first (curl -fsSLO <the same URL>),
# then run `sh install.sh`. security.md says what it does, step by step.
# That is the newest release of this installer. To read and check it before running it,
# download it first (curl -fsSLO <the same URL>), then run `sh install.sh`; security.md
# says what it does step by step and how to verify it.
#
# It installs a fixed release of each app, listed below with the SHA-256 of every file
# it downloads for it, and stops if a download doesn't match.
#
# It asks which apps you want, gives each one its own folder and port (the next
# free one when another program has the usual port), and runs
Expand Down Expand Up @@ -56,6 +60,71 @@ fetch() {
if command -v curl >/dev/null; then curl -fsSL "$1"; else wget -qO- "$1"; fi
}

# --- Pinned releases ------------------------------------------------------------

# The release of each app this installer sets up, and the SHA-256 of every file it
# downloads from it: app, tag, file, SHA-256. scripts/pin-apps.sh writes this list
# after a release, and CI checks it against GitHub.
pins() {
cat <<-'EOF'
# pins start
splitwiz v1.0.0 scripts/install.sh c7ce9f5027d5e9f85473734edc4f5ec50b4ad611f12ac7836efd8c51e4a18f0f
splitwiz v1.0.0 docker-compose.yml 84d675d0e3f26e823014d111f1454d1dab9a913a126114e27e7e28bc6429d642
should-we v2.2.0 docker-compose.yml 57a54827444aefe86c60a7adcf4980cd3bfc57c844bcef946e12265e43d2b27b
flowy v1.0.0 deploy/install.sh 023ce978e85a53569b5a1851bc0c3d2be3e18a871c5a80187e560bb39cb14157
flowy v1.0.0 deploy/docker-compose.yml abad48850d4362542d2f9cf5aad47c8a3237ae2f2576e028f67c6a6ba7536692
flowy v1.0.0 deploy/docker-compose.tailscale.yml ab341609f947775a155d97532241fdcf1afdfc028641062aeb8ebbd59b0ba3ae
flowy v1.0.0 deploy/tailscale-serve.json a04bce87030f08074a9e9ebf5e79d0ce7507531eecdde73ad27e17098fb77a9d
flowy v1.0.0 deploy/.env.example 60a5432413fab9203a3f8dcc60f900b8448765ee708c8f1eede57e3e162ea6f5
troye v0.1.0 deploy/install.sh 718645408f9941e7266d2f5ad2e944e01bcf5e3e8574b97e27e600e4e4f1affa
troye v0.1.0 deploy/docker-compose.yml 93d1aab609ec3c680bcdb4eb1e059bceca98cee1607df3cb66bf0f8e24e7ab4e
troye v0.1.0 deploy/docker-compose.tailscale.yml b2c98d1a3374481dac364231ac1559daa924e104a7bff04dcb4d936f06f5faea
troye v0.1.0 deploy/tailscale-serve.json a04bce87030f08074a9e9ebf5e79d0ce7507531eecdde73ad27e17098fb77a9d
troye v0.1.0 deploy/docker-compose.host.yml 04812d2b8d78e5e0f8af6c94d71be23e246c03a39446fa853a262e13fdacc53a
troye v0.1.0 deploy/.env.example de3c1e179895c0737793537a9c1c416ba20221fafe55d648d92a78ac9c7c4759
shoppa v0.1.0 deploy/install.sh 63269c1b7a241135b529ab771b091783455de64b4ca677a98b51e1eec246278c
shoppa v0.1.0 deploy/docker-compose.yml 5713a851bf43dcce1dbfce424a2e5ad5ea7a1eaca640c5654c8161341d561a79
shoppa v0.1.0 deploy/docker-compose.tailscale.yml 3c37d7a5b654176f3de8bee616f0e3164979e43d47d59dd33b38b29e4e215f5c
shoppa v0.1.0 deploy/tailscale-serve.json a04bce87030f08074a9e9ebf5e79d0ce7507531eecdde73ad27e17098fb77a9d
shoppa v0.1.0 deploy/.env.example b506f4d369127aa9ea32145a26d60a51f26f65704c43a1177ecd428abab084e2
calio v0.1.0 deploy/install.sh 8208c1c22ff27ed90bbf1cbc211ca6360953e76c0070b2c9d613b0426db53079
calio v0.1.0 deploy/docker-compose.yml f6ba1105ed63552c6c299fabd91a8a5cd05638ce1255e7c5e0399cdfa5540b4f
calio v0.1.0 deploy/docker-compose.tailscale.yml bbe8e5437acc4bfebfc6573f3ee5320a28cccc396d951587e23e1be550f54451
calio v0.1.0 deploy/docker-compose.funnel.yml 3bb101d4cc6e33304d077312071cd5f5ec1f6ad6a70d2032ca313d3d0a1df49b
calio v0.1.0 deploy/tailscale-serve.json a04bce87030f08074a9e9ebf5e79d0ce7507531eecdde73ad27e17098fb77a9d
calio v0.1.0 deploy/tailscale-feed-serve.json ff25f48fd37d41560c3f588553451217b152b64a9a57342d60b5152606ffc343
calio v0.1.0 deploy/.env.example 66c2ef5426e323127f4684ddf4d49f5abcea67948f39ddb3d795157ec39c11eb
# pins end
EOF
}

sha256() {
if command -v sha256sum >/dev/null; then
sha256sum | cut -d' ' -f1
elif command -v shasum >/dev/null; then
shasum -a 256 | cut -d' ' -f1
else
die "sha256sum or shasum is needed to check the downloads"
fi
}

# fetch_app APP DIR: downloads APP's pinned files into DIR, keeping their paths,
# and stops unless each one has its pinned SHA-256
fetch_app() {
pins | grep -v '^#' | while read -r name tag path sum; do
[ "$name" = "$1" ] || continue
mkdir -p "$2/$(dirname "$path")"
fetch "https://raw.githubusercontent.com/selfhostit/$name/$tag/$path" >"$2/$path" ||
die "could not download $path of $name $tag"
got=$(sha256 <"$2/$path")
[ "$got" = "$sum" ] || die "$path of $name $tag is not the reviewed file (SHA-256 $got); not installing it"
done
[ -n "$(pins | awk -v a="$1" '$1 == a')" ] || die "no pinned release of $1"
}

# app_tag APP: the pinned release of APP
app_tag() { pins | awk -v a="$1" '$1 == a { print $2; exit }'; }

# --- Docker -------------------------------------------------------------------

# Synology keeps docker in /usr/local/bin, which is not always on the PATH
Expand Down Expand Up @@ -161,24 +230,23 @@ fi

# --- Install ------------------------------------------------------------------

# Each app's files are downloaded into a scratch folder and checked first (fetch_app).
# The plasto-based apps (Flowy, Troye, Shoppa, Calio) come with deploy/install.sh, which takes
# the folder as its argument and PORT for a new install (or the next free port
# when that one is taken; Splitwiz's installer does the same). An empty TS_AUTHKEY
# skips their own Tailscale question: the shared node below covers them.
# when that one is taken; Splitwiz's installer does the same). Run from the scratch
# folder, they use the checked files next to them. An empty TS_AUTHKEY skips their
# own Tailscale question: the shared node below covers them.
install_plasto() { # app port dir
fetch "https://raw.githubusercontent.com/selfhostit/$1/main/deploy/install.sh" |
$sudo env PORT="$2" TS_AUTHKEY= sh -s -- "$3"
$sudo env PORT="$2" TS_AUTHKEY= sh "$tmp/$1/deploy/install.sh" "$3"
}

install_splitwiz() { # port dir
fetch https://raw.githubusercontent.com/selfhostit/splitwiz/main/scripts/install.sh |
SPLITWIZ_DIR="$2" SPLITWIZ_PORT="$1" sh
SPLITWIZ_DIR="$2" SPLITWIZ_PORT="$1" sh "$tmp/splitwiz/scripts/install.sh"
}

install_should_we() { # port dir
$sudo mkdir -p "$2/pb_data"
fetch https://raw.githubusercontent.com/selfhostit/should-we/main/docker-compose.yml |
$sudo tee "$2/docker-compose.yml" >/dev/null
$sudo cp "$tmp/should-we/docker-compose.yml" "$2/docker-compose.yml"
if [ ! -f "$2/.env" ]; then
port=$(free_port "$1")
[ "$port" = "$1" ] || echo "Port $1 is taken, so should-we gets port $port."
Expand All @@ -188,11 +256,14 @@ install_should_we() { # port dir
(cd "$2" && $sudo docker compose pull --quiet && $sudo docker compose up -d --remove-orphans)
}

tmp=$(mktemp -d)
trap 'rm -r "$tmp"' EXIT
for app in $chosen; do
port=$(info "$app" | cut -d'|' -f1)
dir=$base/$app
echo
say "== $app"
say "== $app $(app_tag "$app")"
fetch_app "$app" "$tmp/$app"
$sudo mkdir -p "$dir"
# Run each installer from its own folder, so none of them mistakes this
# folder for a checkout of its repository.
Expand Down
4 changes: 2 additions & 2 deletions linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ssh you@homeserver.local
## 2. Install the apps

```sh
curl -fsSL https://raw.githubusercontent.com/selfhostit/docs/main/install.sh | sh
curl -fsSL https://github.com/selfhostit/docs/releases/latest/download/install.sh | sh
```

The installer:
Expand Down Expand Up @@ -49,7 +49,7 @@ network or the internet. Then [install the apps on your phones](phones.md).
Name the apps after `sh -s --`, and set the folder with `SELFHOSTIT_DIR`:

```sh
curl -fsSL https://raw.githubusercontent.com/selfhostit/docs/main/install.sh |
curl -fsSL https://github.com/selfhostit/docs/releases/latest/download/install.sh |
SELFHOSTIT_DIR=/srv/apps sh -s -- splitwiz flowy
```

Expand Down
2 changes: 1 addition & 1 deletion other-nas.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ file can run them.
Debian VM or LXC): sign in and run the installer, with `sudo` if your user can't use Docker.

```sh
curl -fsSL https://raw.githubusercontent.com/selfhostit/docs/main/install.sh | sudo sh
curl -fsSL https://github.com/selfhostit/docs/releases/latest/download/install.sh | sudo sh
```

On Unraid it suggests `/mnt/user/appdata`; anywhere else, pick a folder on your data pool
Expand Down
70 changes: 70 additions & 0 deletions scripts/pin-apps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/sh
# Pins install.sh to the newest release of every app: for each app it finds the
# release tag, downloads the files the installer uses from that tag, and writes
# their SHA-256 into the pins list in install.sh. Run it after an app's release,
# review the diff, and commit.
#
# scripts/pin-apps.sh pin every app to its newest release
# scripts/pin-apps.sh --check only check that the pinned files match GitHub (CI)
set -eu

cd "$(dirname "$0")/.."
check=n
[ "${1:-}" = --check ] && check=y

raw=https://raw.githubusercontent.com/selfhostit
sha256() { if command -v sha256sum >/dev/null; then sha256sum | cut -d' ' -f1; else shasum -a 256 | cut -d' ' -f1; fi; }

latest_release() { # app
curl -fsSI "https://github.com/selfhostit/$1/releases/latest" |
sed -n 's|.*[Ll]ocation: .*/releases/tag/\([^[:space:]]*\).*|\1|p' | tr -d '\r' | head -n 1
}

# files APP TAG: the files the installer downloads for APP at TAG
files() {
case $1 in
splitwiz) echo "scripts/install.sh docker-compose.yml" ;;
should-we) echo "docker-compose.yml" ;;
*)
# the plasto apps list their deploy files in deploy/install.sh
list=$(curl -fsSL "$raw/$1/$2/deploy/install.sh" | sed -n 's/^files="\(.*\)"$/\1/p')
[ -n "$list" ] || { echo "no files= line in $1 $2 deploy/install.sh" >&2; exit 1; }
echo deploy/install.sh
for f in $list; do echo "deploy/$f"; done
;;
esac
}

if [ $check = y ]; then
bad=0
sed -n '/^ # pins start/,/^ # pins end/p' install.sh | grep -v '# pins' | while read -r app tag path sum; do
got=$(curl -fsSL "$raw/$app/$tag/$path" | sha256)
if [ "$got" = "$sum" ]; then
echo "ok $app $tag $path"
else
echo "WRONG $app $tag $path: GitHub has $got"
exit 1
fi
newest=$(latest_release "$app")
[ "$newest" = "$tag" ] || echo "note $app has a newer release, $newest; run scripts/pin-apps.sh"
done || bad=1
exit $bad
fi

apps=$(sed -n 's/^apps="\(.*\)"$/\1/p' install.sh)
list=$(mktemp)
for app in $apps; do
tag=$(latest_release "$app")
[ -n "$tag" ] || { echo "$app has no release" >&2; exit 1; }
for path in $(files "$app" "$tag"); do
printf '\t%s %s %s %s\n' "$app" "$tag" "$path" "$(curl -fsSL "$raw/$app/$tag/$path" | sha256)" >>"$list"
done
echo "$app $tag"
done
awk -v list="$list" '
/^\t# pins start/ { print; while ((getline line < list) > 0) print line; skip = 1; next }
/^\t# pins end/ { skip = 0 }
!skip { print }
' install.sh >install.sh.tmp && mv install.sh.tmp install.sh
rm -f "$list"
echo "Pinned; review with: git diff install.sh"
Loading
Loading