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
4 changes: 2 additions & 2 deletions .github/workflows/agent-deb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
runner: ubuntu-24.04-arm
steps:
- name: Checkout main repo
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down Expand Up @@ -133,7 +133,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout gh-pages (init if missing)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: gh-pages
path: pages
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/agent-k8s-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
sha_short: ${{ steps.tags.outputs.sha_short }}
steps:
- name: Checkout main repo
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
name: rust (clippy + test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
Expand Down Expand Up @@ -54,7 +54,7 @@ jobs:
run:
working-directory: web
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
Expand All @@ -64,13 +64,16 @@ jobs:
- run: npm run lint
- run: npm run typecheck
- run: npm test
- name: Production synthetic contract tests
working-directory: scripts
run: node --test production-synthetic.test.mjs

journey:
name: journey (browser + server + agent)
needs: [rust, web]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
Expand Down Expand Up @@ -117,7 +120,7 @@ jobs:
name: helm (lint + template)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: azure/setup-helm@v5
- name: Lint and render matrix
run: bash helm/shellfleet-agent/tests/render_test.sh
2 changes: 1 addition & 1 deletion .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/container-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
fi

- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Buildx
uses: docker/setup-buildx-action@v4
Expand Down
26 changes: 13 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Dockerfile.web
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Pinned to a specific Node 20 LTS digest channel (alpine 3.21) so a
# Pinned to a specific Node 22 LTS digest channel (alpine 3.23) so a
# silent upstream Node release can't change the produced bundle
# behaviour mid-deploy. Bump deliberately when it's time.
FROM docker.io/library/node:20.19-alpine3.21 AS builder
FROM docker.io/library/node:22.22-alpine3.23 AS builder
WORKDIR /app
COPY web/package*.json ./
RUN npm ci --no-audit --no-fund
COPY web/ ./
RUN npm run build

FROM docker.io/library/node:20.19-alpine3.21 AS runner
FROM docker.io/library/node:22.22-alpine3.23 AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ place instead of drifting in this file:
- **[Quickstart & environment variables](https://shellfleet.sppidy.in/docs.html#quickstart)** — deploy, reverse-proxy routes, the `.env`, and agent pairing.
- **[Operator CLI](https://shellfleet.sppidy.in/docs.html#cli)** — device authorization without copying browser cookies or dashboard API keys.
- **[Metrics](https://shellfleet.sppidy.in/docs.html#metrics)** — point the dashboard at your Prometheus; YAML panel templates.
- **[Production synthetics](docs/production-monitoring.md)** — continuously verify authenticated REST, SSE, WebSocket, and passkey paths through the public edge.
- **[Kubernetes](https://shellfleet.sppidy.in/docs.html#kubernetes)** / **[Helm](https://shellfleet.sppidy.in/docs.html#helm)** — the k8s agent flavor and every chart value.
- **[Webhooks](https://shellfleet.sppidy.in/docs.html#webhooks)** and **[Cloudflare](https://shellfleet.sppidy.in/docs.html#cloudflare)** — outbound events and edge setup.
- **[Enterprise Edition](https://shellfleet.sppidy.in/ee-docs.html)** — SSO/SCIM, passkeys, ACLs, multi-tenancy, runbooks, recording, drift, multi-source metrics with custom charts, SLA, cost, AI log analysis, Vault.
Expand Down
3 changes: 3 additions & 0 deletions deploy/systemd/shellfleet-production-synthetic-telegram.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[Service]
LoadCredential=telegram-bot-token:/etc/shellfleet/synthetic.telegram-bot-token
LoadCredential=telegram-chat-id:/etc/shellfleet/synthetic.telegram-chat-id
35 changes: 35 additions & 0 deletions deploy/systemd/shellfleet-production-synthetic.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[Unit]
Description=ShellFleet authenticated production synthetic probe
Documentation=https://github.com/sppidy/shellfleet/blob/main/docs/production-monitoring.md
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
DynamicUser=yes
EnvironmentFile=/etc/shellfleet/synthetic.env
Environment=SHELLFLEET_SYNTHETIC_STATE_FILE=/var/lib/shellfleet-synthetic/status.json
LoadCredential=jwt-secret:/etc/shellfleet/synthetic.jwt-secret
ExecStart=/usr/bin/node /usr/local/libexec/shellfleet-production-synthetic.mjs
StateDirectory=shellfleet-synthetic
UMask=0077
TimeoutStartSec=90
NoNewPrivileges=yes
CapabilityBoundingSet=
LockPersonality=yes
MemoryMax=128M
PrivateDevices=yes
PrivateTmp=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=strict
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
TasksMax=32
12 changes: 12 additions & 0 deletions deploy/systemd/shellfleet-production-synthetic.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Run the ShellFleet production synthetic probe every five minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
RandomizedDelaySec=30s
Persistent=yes
Unit=shellfleet-production-synthetic.service

[Install]
WantedBy=timers.target
92 changes: 92 additions & 0 deletions docs/production-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Production synthetic monitoring

`scripts/production-synthetic.mjs` exercises the public dashboard path instead
of probing the containers directly. Each run creates a 60-second JWT in memory
from the control plane's existing signing secret and checks:

- authenticated `GET /api/core/v1/fleet`, including an online-host floor and a
fresh system snapshot;
- authenticated `/api/core/v1/events`, waiting for a real fleet event through
the complete SSE stream;
- authenticated `/ui/ws`, including the Cloudflare/ingress upgrade and the
initial live agent list;
- passwordless passkey challenge creation, including the configured RP id.

The token and signing secret are never printed or stored by the probe. systemd
delivers the signing secret through its protected credentials directory rather
than an environment variable. The service runs as an isolated dynamic user,
records only health state, alerts once when a failure starts, re-alerts hourly
while it persists, and sends a recovery notice. Telegram is used when its two
optional systemd credentials are installed.

## Install on the control-plane host

Install the script and units, then provide a root-readable environment file:

```sh
sudo install -D -m 0755 scripts/production-synthetic.mjs \
/usr/local/libexec/shellfleet-production-synthetic.mjs
sudo install -D -m 0644 deploy/systemd/shellfleet-production-synthetic.service \
/etc/systemd/system/shellfleet-production-synthetic.service
sudo install -D -m 0644 deploy/systemd/shellfleet-production-synthetic.timer \
/etc/systemd/system/shellfleet-production-synthetic.timer
sudo install -d -m 0755 /etc/shellfleet
sudoedit /etc/shellfleet/synthetic.env
sudoedit /etc/shellfleet/synthetic.jwt-secret
```

Put only the raw JWT signing-secret value in `synthetic.jwt-secret`, without a
variable name or quotes, and set its mode to `0600`. Required environment
settings are `SHELLFLEET_SYNTHETIC_BASE_URL` and
`SHELLFLEET_SYNTHETIC_LOGIN`. `UI_URL` and the first entry in
`ALLOWED_GITHUB_USERS` are accepted as deployment-compatible fallbacks.

```dotenv
SHELLFLEET_SYNTHETIC_BASE_URL=https://fleet.example.com/
SHELLFLEET_SYNTHETIC_LOGIN=monitor
SHELLFLEET_SYNTHETIC_ROLE=viewer
SHELLFLEET_SYNTHETIC_MIN_ONLINE=1
SHELLFLEET_SYNTHETIC_MAX_SNAPSHOT_AGE_SECS=60
```

The selected login must already be allowed and present in the server database.
Use a viewer where possible. If Enterprise ACLs intentionally hide every agent
from that viewer, the WebSocket transport is still validated; its visible-agent
count is reported but is not used as the fleet availability floor.

```sh
sudo chmod 0600 /etc/shellfleet/synthetic.env \
/etc/shellfleet/synthetic.jwt-secret
sudo systemctl daemon-reload
sudo systemctl enable --now shellfleet-production-synthetic.timer
sudo systemctl start shellfleet-production-synthetic.service
sudo systemctl status shellfleet-production-synthetic.service
sudo journalctl -u shellfleet-production-synthetic.service -n 20 --no-pager
```

For Telegram alerts, put the raw bot token and chat ID in separate root-only
files, then install the supplied credential drop-in:

```sh
sudo install -D -m 0644 \
deploy/systemd/shellfleet-production-synthetic-telegram.conf \
/etc/systemd/system/shellfleet-production-synthetic.service.d/telegram.conf
```

The drop-in contains:

```ini
# /etc/systemd/system/shellfleet-production-synthetic.service.d/telegram.conf
[Service]
LoadCredential=telegram-bot-token:/etc/shellfleet/synthetic.telegram-bot-token
LoadCredential=telegram-chat-id:/etc/shellfleet/synthetic.telegram-chat-id
```

Direct `JWT_SECRET`, `TELEGRAM_BOT_TOKEN`, and `TELEGRAM_CHAT_ID` environment
variables remain supported for one-shot/manual runs, but the shipped systemd
unit intentionally keeps secrets out of its ordinary environment.

Tune `SHELLFLEET_SYNTHETIC_TIMEOUT_MS` (default `25000`) or
`SHELLFLEET_SYNTHETIC_ALERT_COOLDOWN_SECS` (default `3600`) only when the edge
has known longer latency. A failed run exits non-zero, so the service is also
compatible with systemd or external failure collectors.
Loading
Loading