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
6 changes: 4 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ You should receive an acknowledgement within three business days. We will valida
- Session passwords are kept in a per-account session vault. The accounts service stores each password only sealed to the account's vault public key (ephemeral ECDH P-256, HKDF-SHA256, AES-256-GCM bound to the session id and recipient). The matching private key is stored encrypted under a random vault key, and the vault key is wrapped only by a 160-bit recovery key the service never receives. Any way for the accounts service, the relay, or a copy of the database to open a sealed password, the private key, or the vault key is in scope, as is substituting a vault public key without the browser or the CLI refusing it. Two points are trust-on-first-use by design: the first key seen for a colleague, and the account key a machine linked before the vault existed learns on its next session. The web app served by shell.online performs the unlock and is trusted to, as it is trusted with a typed session password.
- Input typed into a session from the browser is recorded in the team's audit log and encrypted in the browser to the team's audit public key (ephemeral ECDH P-256, HKDF-SHA256, AES-256-GCM bound to the organization, session, entry kind, time and author). The matching private key reaches each member sealed to their session vault by a teammate's own vault key, and the service stores only the public key, the sealed copies and ciphertext. Any way for the accounts service, the relay, or a copy of the database to read audit input text or the team audit key is in scope, as is the service substituting a team key that members then encrypt to. Metadata stays readable by the service by design: who acted, in which session, the entry kind and time, and the lifecycle entries the service writes itself. Trust-on-first-use applies to a teammate's first-seen vault key, and the web app served by shell.online performs the encryption. Entries recorded before encryption existed may remain readable until an owner's or admin's browser encrypts them in place, and in database backups taken before then until those expire.
- The CLI generates an eight-character base64url password with 48 bits of entropy when no password is supplied. This is an explicit convenience/security tradeoff for task-bound shares, not a claim of passphrase-strength protection. `SHELL_ONLINE_E2EE_PASSWORD` accepts a longer unique password for sensitive or long-lived sessions; recipients should receive the URL and password through separate channels when appropriate.
- Persistent state files and Docker state volumes intentionally contain the host credential, browser password, and E2EE key material. Files created by shell.online must be owner-only. A saved password cannot be changed in place because the stable URL and key are bound to it; password rotation creates new state and a new URL. Disclosure caused by publishing, broadly mounting, or backing up that state outside shell.online is not a product vulnerability.
- Active-session records in the per-user local control directory intentionally retain the browser password so `shell list` can reconstruct usable access. The directory and records must remain owner-only and are deleted when their processes close.
- Persistent state files and Docker state volumes intentionally contain the host credential, browser password, and E2EE key material. Files created by shell.online must be owner-only. Live rotation replaces the salt, password, and key in that state before the host changes ciphers; the stable session path remains the same. Disclosure caused by publishing, broadly mounting, or backing up that state outside shell.online is not a product vulnerability.
- Active-session records in the per-user local control directory intentionally retain the browser password so `shell password <ID>` can recover it. Human `shell list` output does not print every password; `--json` deliberately includes them for agents. The directory and records must remain owner-only and are deleted when their processes close.
- `shell password rotate <ID>` changes only future access: it cannot erase output a viewer already received. The host rejects old-key input as soon as it swaps ciphers, the relay disconnects current viewers without receiving either credential, and the account registry atomically replaces sealed copies from the previous generation. Old URL/password pairs may still connect to an anonymous relay socket but cannot authenticate later encrypted frames.
- Removing a team member deletes every password copy still sealed to that account, but cannot make them forget a password or terminal output they already received. Rotate each active session that person could open when immediate revocation matters.
- Reports about leaked links are actionable when shell.online itself disclosed or made them predictable; links forwarded or published by their owner are not a product vulnerability.
- Availability reports should demonstrate a way to bypass the configured rate, frame-size, audience, or lifetime limits.
- The statistics dashboard is private and password-protected. Do not test it with credential stuffing or high-volume traffic.
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,16 @@ jobs:
- run: docker build --build-arg VERSION=test -t shell-online:test .
- run: test "$(docker run --rm --entrypoint shell shell-online:test --version)" = "shell test"
- run: test "$(docker image inspect shell-online:test --format '{{.Config.User}}')" = "shellonline"
- name: Build the standalone relay image
run: docker build -f standalone/Dockerfile -t shell-online-relay:test .
- name: Exercise standalone health and static delivery
run: |
docker run -d --name shell-online-relay -p 18080:8080 shell-online-relay:test
trap 'docker rm -f shell-online-relay' EXIT
for attempt in $(seq 1 30); do
curl -fsS http://127.0.0.1:18080/api/health && break
test "$attempt" -lt 30
sleep 1
done
curl -fsS http://127.0.0.1:18080/ | grep -q 'shell.online'
test "$(docker image inspect shell-online-relay:test --format '{{.Config.User}}')" = "shellonline"
42 changes: 42 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,45 @@ jobs:
cache-to: type=gha,mode=max,ignore-error=true
provenance: mode=max
sbom: true

publish-standalone-relay:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: metadata
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/teoslayer/shell.online-relay
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,prefix=sha-
labels: |
org.opencontainers.image.title=shell.online standalone relay
org.opencontainers.image.description=Portable single-node shell.online WebSocket relay
org.opencontainers.image.url=https://shell.online/self-hosting/
org.opencontainers.image.source=https://github.com/TeoSlayer/shell.online
org.opencontainers.image.licenses=MIT
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: standalone/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha,scope=standalone-relay
cache-to: type=gha,scope=standalone-relay,mode=max,ignore-error=true
provenance: mode=max
sbom: true
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,52 @@ All notable user-visible changes are recorded here. Versions follow [Semantic Ve

## Unreleased

## [0.12.2] — 2026-09-12

### Added

- A standalone, single-node relay for ordinary Docker hosts. It uses Node.js,
WebSockets, local metadata state and Caddy-managed TLS, and requires no
Cloudflare account or credentials.
- A versioned self-hosting documentation page and a release image at
`ghcr.io/teoslayer/shell.online-relay` for amd64 and arm64.
- `shell password <ID>` retrieves an active session password from the local
owner-only record. `shell password rotate <ID>` changes credentials without
restarting the process and persists the new generation for stable sessions.

### Changed

- `--no-e2ee` output refers to the configured relay instead of assuming every
deployment runs on Cloudflare.
- Session assignments and permission handoffs now update in place, without a
reconnect or a window where the former writer can still send input.

### Fixed

- Keep `--auto-close today` valid throughout the final second of the local
day, rather than expiring at the instant that second begins.
- Keep notifications, audit entries, session password shares, and member
removal inside the active organization.
- Escape CLI login callback content, keep the mobile account menu usable, and
reject invalid terminal dimensions before they reach a PTY.

### Security

- Password rotation switches the host cipher before disconnecting existing
viewers, atomically replaces the owner's sealed account-vault copy, and
removes stale teammate copies. The relay receives neither old nor new
plaintext credentials.
- A verified browser cache can no longer overwrite a newer vault generation.
Vault credentials are tried first and replace stale local cache entries only
after successfully opening a live encrypted frame.
- Removing a team member now deletes every session-password copy sealed to
that account. Owners must still rotate active sessions to revoke passwords a
former member may already have seen.
- Targeted email invitations require a verified Firebase email. Team-key and
session-key shares now reject invalid P-256 identities and oversized or
malformed ciphertext, and key distributors must already hold the team key.
- The accounts app now sends a restrictive browser security policy from both
its Node server and Cloudflare Worker deployment.

## [0.12.1] — 2026-09-12

Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ Homebrew 6 asks you to trust a third-party tap once. Older versions have no
Installers verify checksums. Release binaries and `SHA256SUMS` are available on
the [releases page](https://github.com/TeoSlayer/shell.online/releases).

## Platform compatibility

| OS | Architectures | Verification |
| --- | --- | --- |
| macOS | amd64, arm64 | Build |
| Windows | 386, amd64, arm64 | Native ConPTY on amd64; build on others |
| Linux | 386, amd64, armv5/6/7, arm64, LoongArch64, MIPS/MIPSLE/MIPS64/MIPS64LE, PPC64/PPC64LE, RISC-V 64, s390x | Runtime under QEMU |
| FreeBSD | 386, amd64, armv7, arm64 | Build |
| OpenBSD | 386, amd64, armv7, arm64, ppc64, riscv64 | Build |
| NetBSD | 386, amd64, armv7, arm64 | Build |
| DragonFly BSD | amd64 | Build |
| Solaris | amd64 | Build |

See [platform details](https://shell.online/platforms/) for PTY, router, ROS,
installer, and test caveats.

## Usage

```sh
Expand All @@ -53,6 +69,8 @@ shell --auto-close 5m <command> # set an earlier deadline
shell --persistent <file> <command> # reuse a URL and password

shell list # list local sessions (adapts to terminal width)
shell password <id> # retrieve an active password locally
shell password rotate <id> # revoke it without restarting the process
shell attach <id> # attach locally
shell kill <id> # stop a session
```
Expand All @@ -72,6 +90,10 @@ type with the permissions of the wrapped process; use `--read-only` when viewers
should only watch. See the [security model](https://shell.online/security/) and
[the security policy](.github/SECURITY.md).

Active passwords remain recoverable on their owner machine; account-linked
passwords are also sealed into the user's E2EE vault. Without either owner-held
copy there is intentionally no service-side recovery key.

## Accounts and containers

Accounts are optional. `shell login` groups sessions from linked machines in
Expand All @@ -85,6 +107,10 @@ docker compose up -d
docker compose logs shell-online
```

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=TeoSlayer/shell.online&type=Date)](https://www.star-history.com/#TeoSlayer/shell.online&Date)

## Documentation

- [Quick start](https://shell.online/docs/)
Expand All @@ -94,7 +120,7 @@ docker compose logs shell-online
- [End-to-end encryption](https://shell.online/e2ee/)
- [Containers](https://shell.online/docker/)
- [Platforms](https://shell.online/platforms/)
- [Self-hosting](docs/self-hosting.md)
- [Self-hosting](https://shell.online/self-hosting/) — Docker or Cloudflare

## Development

Expand All @@ -109,9 +135,9 @@ npm run test:app

See [the contribution guide](.github/CONTRIBUTING.md) before opening a pull request.

## Star History
## Contributors

[![Star History Chart](https://api.star-history.com/svg?repos=TeoSlayer/shell.online&type=Date)](https://www.star-history.com/#TeoSlayer/shell.online&Date)
[![shell.online contributors](https://contrib.rocks/image?repo=TeoSlayer/shell.online)](https://github.com/TeoSlayer/shell.online/graphs/contributors)

MIT licensed. See [`LICENSE`](LICENSE).

Expand Down
Loading
Loading