gfs

August 25, 2026 · View on GitHub

📦 Web and API door for groot .tar.gz archives when a VPS exists

Release Version Go License CI gghstats clones

Repo: github.com/hrodrig/groot-share · Releases: GitHub Releases · Spec: docs/SPECIFICATIONS.md · Alternatives: docs/ALTERNATIVES.md · Deploy: groot-share-selfhosted · Changelog: CHANGELOG.md · Roadmap: .planning/ROADMAP.md · Consensus: docs/GFS-CONSENSUS.md

gfs — archive door for groot captures: RBAC, audit, VPS + S3

The problem

Teams on a shared Kubernetes cluster need one catalog of groot diagnostic .tar.gz files — for incidents, RCA, and handoffs. Archives are often several GB. Producers include laptops (adhoc collect), bastion hosts (Docker / cron / systemd via groot-selfhosted), and in-cluster jobs (groot-trigger, CronJob).

The usual shortcuts fail:

  • Put the same S3-compatible bucket key on every laptop — revoke or leak hits the whole bucket; many providers (Contabo, Hetzner, Wasabi, MinIO, …) do not give you per-engineer IAM for free.
  • “Just use Cyberduck” — no per-user audit, retention, or upload API keyed to usernames; bastion, laptop, and cluster uploads stay disconnected unless you glue them.
  • “Run only groot-trigger” — great for starting a collect; it does not list, download, or retain archives (trigger SPEC).

How gfs solves it

gfs is a small VPS service: login + scoped API keys, HTTP ingest, optional SFTP inbox watcher, Captures UI, audit log, and retention — in front of groot archives. Laptops and bastion hosts never need long-lived AWS_* on every operator machine. On vps-s3, the cluster still prefers groot upload.s3 straight to the bucket (multi-GB skips the VPS); bastions may HTTP POST to gfs or use upload.s3 / upload.sftp per groot-selfhosted playbook. gfs lists the same prefix so HTTP, S3, and SFTP ingest appear in one place.

gfs is not universal. If you have only a bucket and no VPS, the better path is S3 only (groot upload.s3 + S3 client) — do not deploy gfs. Full decision matrix with trade-offs: docs/ALTERNATIVES.md.

Choosing an approach (at a glance)

SituationBest choice
Bucket only, no VPS, operators use S3 toolsS3 only — groot-selfhosted s3-contabo example
VPS, archives on disk, small teamgfs vps
Bucket + laptops/bastions without AWS_* + cluster multi-GBgfs vps-s3 + cluster upload.s3
Scheduled or bastion collect (jump host)groot-selfhosted standalone / Docker → gfs HTTP or upload.s3
“Generate capture” button in clustergroot-trigger + one of the storage rows above
SFTP drop boxgroot upload.sftp into GFS_SFTP_INBOX; gfs watcher ingests (source=sftp). Playbook: sftp-vps

→ Full comparison, pros/cons, and transparency about gfs limits

Not a replacement for groot. Collect, validate, and analyze stay in the groot CLI. groot-trigger starts on-demand collects in-cluster. groot-selfhosted ships CronJob / bastion playbooks. gfs appears only in topologies where a VPS is the operator-chosen door; groot-share-selfhosted ships VPS / vps-s3 deploy playbooks for gfs.

Related tools (same maintainer):

Table of contents

↑ Back to top

Groot family roles

RepoRole
grootCLI: collect / validate / inspect / analyze → .tar.gz; optional upload.s3 / upload.gcs / upload.sftp
groot-triggerIn-cluster HTTP → Job groot collect; fire-and-forget; optional post-collect upload
gfs (this repo)VPS web + API: auth, HTTP ingest, list, download, audit, retention (vps and vps-s3 topologies)
groot-share-selfhostedOperator deploy for gfs: VPS, vps-s3, systemd, Docker, Helm
groot-selfhostedOperator deploy for groot: CronJob, bastion, Helm; S3-only and SFTP-VPS playbooks

Operator topologies

Deploy-time choice. No per-upload “also S3” flag.

Topologygfs process?Where archives liveWho lists
VPS onlyyesVPS disk (home)gfs
S3 onlynoBucket via groot upload.s3S3 client (Cyberduck, aws, rclone, …)
VPS + S3yesStaging on VPS → bucket is homegfs (from bucket + HTTP/S3 ingest keys)

⚠️ Choose your topology up front — switching later is not a migration. GFS_TOPOLOGY is a deploy-time decision, not a runtime toggle. Archives are stored differently in each mode: on vps they live on the local disk (GFS_DATA_DIR); on vps-s3 the bucket is the source of truth and the VPS disk only holds in-transit staging. Captures uploaded in one mode are not automatically carried over to the other, so changing GFS_TOPOLOGY (or pointing GFS_DATA_DIR at a fresh path) afterwards can make previously uploaded captures disappear from the list even though the data still exists elsewhere. Pick the topology that matches your intended use before you start ingesting; if you must switch later, re-ingest the archives explicitly.

Details and open questions: docs/GFS-CONSENSUS.md. When not to use gfs: docs/ALTERNATIVES.md.

Features

  • Session login + scoped API keys (upload | read)
  • Roles: viewer, uploader, admin (RBAC on every route)
  • Server-rendered HTML: Captures, Upload, Activity, Settings, admin Users
  • HTTP ingest of groot .tar.gz (browser or POST /v1/archives)
  • Cluster archives via shared S3 prefix (source=s3) on vps-s3
  • SFTP inbox watcher (GFS_SFTP_INBOX) for groot upload.sftp — Captures pill SFTP (source=sftp)
  • Audit log (upload / download / delete / user actions; no secrets in rows)
  • Per-user pin strip on Captures (quick access; archive_pins cascades on user delete)
  • Captures inventory summary: count, bytes on disk, distinct cluster slugs, in-transit count, storage topology
  • Captures facet bar: cluster chips with counts, search (filename substring), time-window chips (24h / 7d / 30d / all); state in URL query params
  • Responsive archive list: sortable table on desktop, card layout at ≤ 719px with Download as a primary button per card
  • Inline dropzone upload on Captures: drag-and-drop a .tar.gz, file name + size before send, live progress + cancel, inline duplicate/too-large notices (no page navigation)
  • Activity filters: actor / action / time-window bar on the Activity page; admin CSV + JSON export of the full audit log
  • Typed-name confirm on destructive actions (delete archive, remove user, revoke key) — type the name to enable the button
  • Completeness badge on local captures (Complete / N of M jobs failed / Failed) read from the groot manifest with a capped peek
  • Share-link admin UI — create time-limited external links (preset/custom TTL), copy-once, list and revoke per capture (admin-only)
  • Retention: keep_last or max_age_days (defaults 20 / 90)
  • Fail-closed config (GFS_TOPOLOGY, bootstrap admin, bucket creds)
  • Supply chain aligned with groot-trigger (GNU Make, GoReleaser, distroless, CI)

Quick start (local)

Requirements: Go 1.26+ (see go.mod).

git clone https://github.com/hrodrig/groot-share.git
cd groot-share
cp .env.example .env    # edit GFS_BOOTSTRAP_* (min 8 chars)
make serve              # builds bin/gfs, loads .env, listens on :8080

Open http://127.0.0.1:8080/login with the bootstrap credentials from .env. Data and SQLite live under GFS_DATA_DIR (default ./var/lib/gfs).

Probes: GET /healthz, GET /readyz.

↑ Back to top

Configuration

Environment-only (GFS_*). See docs/SPECIFICATIONS.md §5 for the full contract.

VariablePurpose
GFS_TOPOLOGYRequired. vps or vps-s3. Deploy-time choice — do not switch after ingesting captures (see Operator topologies); captures in one mode are not migrated to the other.
GFS_DATA_DIRRequired. SQLite + home/staging root
GFS_LISTENBind address (default :8080)
GFS_BOOTSTRAP_ADMIN / GFS_BOOTSTRAP_PASSWORDFirst admin when user table is empty; ignored once users exist
GFS_BOOTSTRAP_ADMIN_NAMEFirst admin display name (default Administrator)
GFS_S3_* + AWS_*Required for vps-s3 (bucket, endpoint, prefix, path-style)
GFS_KEEP_LAST / GFS_MAX_AGE_DAYSRetention (defaults 20 / 90)
GFS_MAX_UPLOAD_BYTESUpload cap (default 32 GiB)
GFS_LOGIN_SIMPLEtrue: white /login only (no product chrome). Default off
GFS_LOGIN_RATE_LIMITCap POST /login per IP and per username (default 20/1m). 0 disables
GFS_BRAND_SUBApp-bar tag (default archive door). Example: ACME CORP. - hides
GFS_FOOTERFooter text (default gfs vX · groot · groot-share). - hides
GFS_SFTP_INBOXAbsolute groot upload.sftp drop dir. Empty = off. See sftp-vps
GFS_SFTP_POLLInbox poll interval (default 30s)

Copy .env.example for local dev; never commit .env. Change the sample bootstrap password before first start.

Deploy behind a trusted reverse proxy for TLS (groot-share-selfhosted). Absolute links use Host / X-Forwarded-Proto — the proxy must overwrite those headers (see SECURITY.md).

↑ Back to top

HTTP API (summary)

EndpointAuthNotes
GET /healthznoneLiveness
GET /readyznoneSQLite (+ bucket on vps-s3)
POST /loginnoneSession cookie
GET /sessionCaptures HTML
POST /v1/archivessession or api_key (upload)Ingest .tar.gz
GET /v1/archivessession or api_key (read)JSON list
GET /v1/archives/{id}/filesession or api_key (read)Download
DELETE /v1/archives/{id}admin sessionDelete
GET /v1/auditsession or api_key (read)Audit JSON
GET/POST /v1/users, PATCH /v1/me, …admin / sessionUser and key management

Behavior contract: docs/SPECIFICATIONS.md.

↑ Back to top

Build and test

make build          # bin/gfs
make test           # race detector
make cover          # COVER_MIN=80
make ci             # fmt-check + lint + gocyclo + test
make release-check  # release gate (maintainers)

Install to $GOPATH/bin: make install.

↑ Back to top

Documentation

DocContents
docs/ALTERNATIVES.mdProblem narrative, comparisons, best choice per situation, gfs limits
docs/SPECIFICATIONS.mdApproved behavior contract
docs/GFS-CONSENSUS.mdProduct freeze and topology decisions
CHANGELOG.mdRelease notes
.planning/ROADMAP.mdPhases and progress
groot-share-selfhostedOperator deploy: Compose, systemd, Helm (vps / vps-s3)
AGENTS.mdContributor / agent constraints
SECURITY.mdVulnerability reporting

English for all repo artifacts.

↑ Back to top

License

MIT — see LICENSE.