Skip to content

Latest commit

 

History

History
201 lines (146 loc) · 10.9 KB

File metadata and controls

201 lines (146 loc) · 10.9 KB

ButterStack Connector

An outbound-only daemon a game studio runs inside its own network so ButterStack can reach a private, on-premises Perforce, TeamCity, Jenkins, GitHub Enterprise Server, or Horde without the studio opening a single inbound port. One outbound TLS connection to one hostname on 443. A typed command allowlist, never a tunnel and never a shell. Credentials stay on the studio's disk and never cross the wire.

Status: released, and running in production. The Perforce path is proven end to end against a live Helix Core server: a real changelist reached a ButterStack project through the broker, with no inbound port opened anywhere on the studio's network. Supported backends records what each backend covers today.

What it is

The connector is a daemon the studio runs on its own hardware (or in a container on its own network). It opens exactly one outbound TLS connection to wss://connect.butterstack.com/connect on port 443, announces what it can do, and then executes only commands from a typed, versioned allowlist with constrained arguments, each one logged locally. Every credential the studio configures (Perforce tickets, TeamCity tokens) stays in the studio's config file and never crosses the wire. There is no inbound port, no tunnel, no shell, and no remote configuration: the broker cannot tell the connector where to find a credential.

Requirements

  • Go 1.23 or later (for building from source; release builds use 1.25)
  • Ruby 3.2+ (only for running the drill harness in test/)
  • A ButterStack account with a connector token issued from the project's Connectors UI

Install

From source:

go build -o butterstack-connector ./cmd/butterstack-connector

Docker image (from the in-tree Dockerfile, local build):

docker build -t butterstack-connector .

The Dockerfile produces a minimal image with a static Go binary and a Ruby runtime for the UAT entrypoint. In production, the entrypoint runs the connector directly from /usr/local/bin/butterstack-connector.

Docker image (published, recommended):

Pull the published image instead of building it yourself, and pin an exact version tag:

docker run -d \
  --name butterstack-connector \
  -v $(pwd)/connector.yml:/etc/butterstack/connector.yml:ro \
  ghcr.io/butterstack/butterstack-connector:v0.2.0

The image's entrypoint always reads its config from /etc/butterstack/connector.yml (the same path the -config flag defaults to), so mounting your connector.yml there read-only is all a container needs. See Image and versions below for the tagging policy.

Image and versions

The image is published at ghcr.io/butterstack/butterstack-connector. Releases are pinned by semver tag (e.g. v0.2.0); do not run :latest for anything you want to stay stable, since a new tag may ship a breaking change to the config schema or the compiled vocabulary. Each tag also carries provenance and sbom metadata from the build, and the same version is published as OS/arch archives on the GitHub Releases page.

Configure

Get a connector.yml from ButterStack

You do not hand-write the endpoint and token fields yourself. From your ButterStack project's Integrations > Connectors tab in the web app, create a new connector: this generates a connector.yml already scoped with your project's real endpoint and a freshly issued token, ready to download.

Once you have that file:

  1. Save it as connector.yml (or download it directly to your install location).
  2. Fill in the perforce: and/or teamcity: sections yourself with your own LAN details (server address, service-account user, ticket/token file paths, depot scope). These sections describe your own network and are never generated by ButterStack, since your credentials never leave it.
  3. Install it per the permissions steps below and point the daemon at it.

For example, a filled-in Perforce section might look like:

perforce:
  enabled: true
  port: ssl:<your-perforce-host>:1666
  user: butterstack-ro
  ticket_file: /etc/butterstack/p4.ticket
scopes:
  depot_scope:
    - //depot/your-studio/

Copy connector.example.yml to your install location (e.g. /etc/butterstack/connector.yml) and set its permissions to 0600. The daemon refuses to start if the config file or any *_file path is readable by group or other users.

install -m 0600 connector.example.yml /etc/butterstack/connector.yml

Every credential comes from this file, or from a *_file path it names. There is no environment-variable fallback, no flag that takes a secret, and no remote configuration.

Fields

Field Type Secret Source Description
endpoint string no file The broker URL. Must be wss:// with no query string, no userinfo, no fragment. This is the one hostname your egress rule needs.
endpoint_ca_file string no file Optional. Pins the trust anchor for the endpoint, for a private CA or a TLS-inspecting proxy. There is no option to skip verification for the broker connection.
token string yes file The connector token issued in the ButterStack UI. Shown exactly once at issue time. Format: bsc_<project-id>_<secret>.
token_file string no file Alternative to token: path to a file containing the token (for vault-injected secrets). Mutually exclusive with token. The file must be mode 0600.
connector_id string no file A name for this host, shown in the Connection Status panel.
log_dir string no file Local audit log directory. One JSON line per command, including every denial. Defaults to a logs/ directory next to the config file.
max_concurrent int no file Commands executed in parallel. Range: 1-32. Default: 4.
scopes.depot_scope list no file Literal Perforce depot prefixes (no wildcards). A path whose literal prefix is not inside one of these is denied.
scopes.allowed_build_types list no file For the reserved teamcity.build.queue verb. Not compiled in v0.
scopes.repo_allowlist list no file For the reserved ghes.* verbs. Not compiled in v0.
toggles.content_verbs bool no file Content-class verbs are off in v0 at the schema level. This switch cannot turn one on yet.
perforce.enabled bool no file Enable the Perforce tool.
perforce.binary string no file Path to the p4 CLI. Default: p4.
perforce.port string no file Helix Core server address (e.g. ssl:perforce.studio.lan:1666).
perforce.user string no file A read-only Perforce user.
perforce.ticket string yes file The Perforce ticket. Passed to p4 via the P4PASSWD environment variable so it does not appear in the process list.
perforce.ticket_file string no file Alternative to perforce.ticket. Must be mode 0600.
perforce.timeout duration no file Timeout for p4 commands. Default: 20s.
teamcity.enabled bool no file Enable the TeamCity tool.
teamcity.url string no file The TeamCity server URL on your LAN (e.g. https://teamcity.studio.lan).
teamcity.token string yes file A project-limited TeamCity access token with a read-only role. Never crosses the wire.
teamcity.token_file string no file Alternative to teamcity.token. Must be mode 0600.
teamcity.ca_file string no file Optional. Trust anchor for a self-signed certificate on your LAN TeamCity. Scoped to this server only, not the broker connection.
teamcity.allow_insecure_tls bool no file Skip TLS verification for the LAN TeamCity. There is no equivalent for the broker connection, which always verifies.
teamcity.timeout duration no file Timeout for TeamCity REST calls. Default: 10s.

Two rules the daemon enforces at startup rather than documents:

  • connector.yml and every *_file must be mode 0600 or stricter, or it refuses to start.
  • The endpoint must be wss:// with no query string. A copy-pasted ?token=... URL cannot start the daemon at all.

Run

Foreground:

./butterstack-connector -config /etc/butterstack/connector.yml

Print the compiled vocabulary:

./butterstack-connector -print-vocabulary

systemd unit (example):

[Unit]
Description=ButterStack Connector
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=butterstack-connector
ExecStart=/usr/local/bin/butterstack-connector -config /etc/butterstack/connector.yml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Docker Compose (example):

services:
  connector:
    image: butterstack-connector
    build: .
    entrypoint:
      - /usr/local/bin/butterstack-connector
      - -config
      - /etc/butterstack/connector.yml
    volumes:
      - ./connector.yml:/etc/butterstack/connector.yml:ro
    restart: unless-stopped

Verify

The drill harness (test/drills.rb) runs seven drills from design note section 4.3 against the mock broker (test/mock_broker.rb), plus a round-trip phase and the broker-side half of drill (f). Requires Ruby 3.2+ and a built binary.

make check        # go vet + go test + drills
make test         # go vet + go test only
make drills       # drills only (needs a built binary)
make build        # build the binary
make vocabulary   # print the compiled allowlist

make build uses docker run golang:1.23-alpine by default. If you have Go on the host, GO=go make build uses it directly.

See test/README.md for details on the individual drills and the mock broker.

Supported backends

Backend Status Compiled verbs
Perforce (Helix Core) Implemented p4.describe, p4.changes
TeamCity Implemented teamcity.server.info, teamcity.build.get
Jenkins Planned (reserved) jenkins.build.trigger (denied in v0)
GitHub Enterprise Server Planned (reserved) ghes.commit.get (denied in v0)
Horde Planned (reserved) horde.server.info (denied in v0)

System verbs (sys.ping, sys.version, sys.capabilities) are always compiled and touch no studio tool.

Reserved verbs are listed in the vocabulary so that the schema is self-documenting and the drills exercise their denial path, but they cannot be executed in this build. p4.file_contents is reserved as a content-class verb and is off at both the schema level and the config level. teamcity.build.queue is reserved as a mutation-class verb. No mutating verb and no content-class verb is compiled in v0.

See internal/vocab/vocab.go for the full allowlist.

Protocol

See PROTOCOL.md for the wire-level protocol details: transport, authentication, frame format, vocabulary resolution, and the audit log.