Shipwright Harness

Security

This page is a security architecture overview for anyone evaluating Shipwright before deploying it against their own infrastructure and repositories. It covers the reference deployment topology, network and egress posture, human and machine authentication, Kubernetes RBAC, secrets handling, third-party integration scopes, and current logging and compliance posture. Every claim below is either how the software behaves today or an explicit statement of what it does not yet do — read the gaps as plainly as the features.

Shipwright is self-hosted: you run it in your own Kubernetes cluster, against your own GitHub App and Slack app, with your own database. No Shipwright-operated service ever sees your source code, your task data, or your credentials.

Deployment architecture

The reference deployment is a Helm chart (charts/shipwright) packaging a small set of services, each independently optional except the admin service:

Service Port Role
Admin 3001 CRUD API + web UI — agent management, provisioning, task/PR browsing, chat
Metrics 3460 Stateless JSON API + dashboard, no database
Task store 3000 Opt-in, Postgres-backed work queue (tasks, PRs, scoped tokens)
Chat 3000 Opt-in, Postgres-backed chat threads between you and an agent
MCP server 3010 Opt-in, proxies MCP tool calls to the task-store API
Agent One Kubernetes Deployment per agent, provisioned dynamically

Task store, chat, and the MCP server are disabled by default. Per-agent Deployments are only created when agent.provisioning.enabled=true; each provisioned agent gets its own Secret and PersistentVolumeClaim.

Aspect AWS (EKS) GCP (GKE)
Ingress AWS Load Balancer Controller provisions an ALB from the chart’s Ingress resource (ingressClassName: alb) Gateway API — the chart renders a Gateway and HTTPRoutes targeting the gke-l7-global-external-managed GatewayClass
TLS Terminates at the ALB, via an ACM certificate annotation or cert-manager cert-manager issues TLS via a ClusterIssuer, plus an HTTP→HTTPS redirect route
Database Optional bundled PostgreSQL (Bitnami subchart), off by default — bring your own Optional bundled PostgreSQL (Bitnami subchart), off by default — bring your own
Storage Chart-managed PersistentVolumeClaim per provisioned agent, same on both targets Chart-managed PersistentVolumeClaim per provisioned agent, same on both targets

AWS (EKS)

The AWS Load Balancer Controller provisions an ALB from the chart’s Ingress resource (ingressClassName: alb). TLS terminates at the ALB, either via an ACM certificate referenced in an annotation or via cert-manager. auth.mode=google (or okta) is required for any internet-reachable EKS deployment.

GCP (GKE)

GKE uses the Gateway API instead of a plain Ingress: the chart renders a Gateway and HTTPRoutes targeting the gke-l7-global-external-managed GatewayClass, with cert-manager issuing TLS via a ClusterIssuer. The chart also renders an HTTP→HTTPS redirect route so plaintext requests never reach a service.

Other targets

Plain Ingress is also supported with nginx (Minikube, bare-metal) or Traefik, and the chart can optionally bundle PostgreSQL (Bitnami subchart), ingress-nginx, Traefik, and cert-manager as dependencies — all off by default, so a production install can bring its own database and ingress controller instead.

Network & egress controls

Shipwright does not implement an egress allowlist or firewall inside the application itself. Restricting which external hosts a Shipwright service or agent pod can reach is an infrastructure decision you make at deploy time — a Kubernetes NetworkPolicy, a cloud VPC egress rule, or an equivalent control at your network boundary. Every service assumes outbound internet access by default unless you add your own network policy. The required outbound destinations are:

Destination Purpose Required/Optional
GitHub API Reading/writing repository contents, pull requests, Actions status Required
Slack API Bot messaging, Socket Mode event delivery Required
Anthropic API The model calls that power agent reasoning and code generation Required
Your SSO provider (Google or Okta OAuth) Admin login when auth.mode=google or auth.mode=okta Required only when internet-reachable auth is enabled
Sentry Error/log reporting Optional — only when SENTRY_DSN is set (self-hostable in-cluster — see Logging, monitoring & observability)

Shipwright has no subprocessor relationship of its own — the only third parties that ever sit in the data path are whichever of the above you configure yourself, each under your own account and terms with that provider.

The one exception is voice. Speech-to-text and text-to-speech can run fully self-hosted with zero third-party network calls: agent.voice.provider=whisper runs speech-to-text as a self-hosted Whisper pod, and Piper — the default text-to-speech engine — is a self-hosted binary baked into the agent image and invoked as a local subprocess (stdin in, WAV file out), never making a network call. Opting into agent.voice.provider=groq or setting agent.voice.elevenlabs.apiKey each independently reintroduce egress to that respective third party. Voice is the only subsystem where egress is architecturally eliminable; treat everything else as requiring outbound internet access unless you’ve fenced it off yourself.

Security: if you require a fully egress-locked deployment, plan on writing your own NetworkPolicy (or cloud firewall rules) — Shipwright does not ship one, and does not enforce any egress restriction on your behalf.

Human access

The admin service supports three authentication modes, set via auth.mode:

Mode Use case Notes
open Local development only No real authentication — a deliberate insecure escape hatch; the chart overrides NODE_ENV to route around the only runtime guard that exists, so it is not blocked in production and must never be selected for an internet-reachable deployment
google Production Google OAuth 2.0
okta Production Okta OIDC

google and okta can be configured simultaneously — the login page shows both providers — and both gate on the same SHIPWRIGHT_ADMIN_ALLOWED_EMAILS allowlist, a comma-separated list of email addresses. Only listed emails may sign in, regardless of which provider they authenticate through. Session state is an admin_session httpOnly JWT cookie valid for 8 hours.

A human doesn’t need to be an admin to get scoped access: adding their email as an AgentMember on a specific agent grants them visibility into that one agent’s detail page and Slack access, without putting them on the admin allowlist or exposing any other agent.

Security: auth.mode=open performs no authentication — anyone who can reach the admin service is treated as a logged-in user. Use it only on a local cluster or behind a private network you fully control.

Programmatic access

The admin API checks three auth paths, in order:

  1. Admin API key — a bearer token matching an entry in SHIPWRIGHT_ADMIN_API_KEYS (comma-separated name:token:scope tuples). Full admin access.
  2. Per-agent bearer token — a bearer token scoped to one agent id. Cross-agent access returns 403.
  3. Session cookie — the same admin_session cookie used by the web UI.

Kubernetes service accounts & RBAC

RBAC for agent provisioning is entirely opt-in: nothing related to it is rendered by the Helm chart unless agent.provisioning.enabled=true (default false). When it is enabled, the chart grants the admin ServiceAccount exactly the verbs the provisioner exercises — nothing broader:

Resource Verbs
Deployments (apps) create, get, list, patch, update, delete
Secrets (core) create, get, delete
PersistentVolumeClaims (core) create, get, delete

By default this is a namespace-scoped Role + RoleBinding in the release namespace. If you set agent.provisioning.namespace to provision agents into a different namespace, the chart renders a ClusterRole + ClusterRoleBinding with the same verb set instead, so provisioning still reaches no further than these specific operations.

Provisioned agents run under their own, separate ServiceAccount — distinct from the admin ServiceAccount that provisions them — and that agent ServiceAccount is granted no additional RBAC by the chart. An agent pod runs with the cluster’s default (empty) permissions unless you explicitly add more.

Secrets & service-to-service auth

Secret values — env vars, Slack tokens, Anthropic API keys — are encrypted at the service layer with AES-256-GCM when SHIPWRIGHT_ENCRYPTION_KEY (a 64-character hex string, 32 bytes) is set. If this key is left unset, secrets are stored in plain text and a warning is logged at startup; setting it is something you should do before any production use.

The following table enumerates each credential managed by Shipwright in per-agent Secrets, how it is stored, and its scope:

Secret/Token Env var Storage Scope
Agent API token SHIPWRIGHT_AGENT_API_KEY SHA-256 hash persisted in the database; raw 64-char hex value shown once at creation; injected into the agent’s Kubernetes Secret (token key) Scoped to a single agent id; cross-agent access returns 403
Task-store token SHIPWRIGHT_TASK_STORE_TOKEN Minted per-agent by the admin provisioner; stored in the agent’s Secret (task-store-token key); revoked automatically when the agent is deleted Scoped to that single agent
Chat-service token SHIPWRIGHT_CHAT_SERVICE_TOKEN Minted per-agent by the admin provisioner; stored in the agent’s Secret (chat-service-token key); revoked automatically when the agent is deleted Scoped to that single agent; only present when chat is enabled

GitHub App integration

Shipwright authenticates to GitHub either as a GitHub App (recommended — GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY) or, as a fallback, a personal access token (GH_TOKEN). The default GitHub App permission manifest requests exactly:

Permission Access
Contents Write
Pull requests Write
Actions Read
Workflows Write

There is no webhook receiver in the codebase — the app manifest sets no webhook configuration, and Shipwright does not accept inbound GitHub webhook events. All GitHub interaction is Shipwright calling out to the GitHub API, never GitHub calling in.

Slack App integration

Slack access is configured per deployment with three required env vars — SLACK_BOT_TOKEN (bot OAuth token), SLACK_APP_TOKEN (Socket Mode app-level token), and SLACK_SIGNING_SECRET (verifies inbound request signatures) — plus optional vars (SLACK_ADMIN_TOKEN for privileged operations, SLACK_ALERT_CHANNEL, SLACK_OWNER_USER).

Shipwright’s Slack app manifest is centrally codified in the codebase at admin/src/slack-provisioning-client.ts. The buildAgentManifest() function constructs a Slack App Manifest object with configured display_information, bot_user, app_home, and assistant_view. Bot permissions are centrally defined in the exported AGENT_BOT_SCOPES array, which is embedded into the manifest’s oauth_config.scopes.bot. This manifest-driven approach ensures consistent provisioning: apps.manifest.create provisions new apps during initial setup, and apps.manifest.update keeps the manifest in sync during agent lifecycle management.

Bot scopes — the following table enumerates every permission the agent’s bot user holds:

Scope Grants
Messaging
chat:write Send messages to channels, DMs, and group conversations
im:write Send direct messages to users
files:read Read file metadata and contents
files:write Upload and modify files
reactions:write Add and remove emoji reactions
assistant:write Post messages to Slack Assistant threads
History-read
channels:history Read message history from public channels
groups:history Read message history from private channels
im:history Read direct message history
mpim:history Read group DM message history
Directory-read
channels:read List public channels and channel information
users:read List and read user information
users:read.email Access user email addresses
Mention & misc.
app_mentions:read Receive notifications when the agent is mentioned
reactions:read Read emoji reactions on messages

Pull request review & merge

Self-review is disabled by default: a freshly provisioned agent’s policy (state/agent-policy.md, seeded from the shipped template) sets allow_self_review: false, so an agent is excluded from reviewing its own open PRs. Automated merging is disabled by default too — the shipwright-deploy cron ships off, so nothing merges without a human clicking merge in the GitHub UI.

If Shipwright itself performs a merge — because you’ve enabled shipwright-deploy, or a human runs the merge command directly — it squash-merges the PR (gh pr merge --squash). Self-review (allow_self_review, also off by default) only lets that merge go through in repos where GitHub doesn’t require a review at all; if branch protection does require one, self-review doesn’t satisfy it, the merge is rejected, and Shipwright releases its claim on the task and marks the task/PR blocked so a human can add a real approval.

For environments that need a specific, named human to be the actual approver — not just “not the same agent,” but a person you’ve designated (SOC 2-style segregation of duties) — add your reviewers as GitHub CODEOWNERS for the repository, or at minimum for .github/workflows/**, and require review from Code Owners in branch protection. Neither self-review nor another agent’s review satisfies a Code Owners requirement; only a human on that list does. Keep shipwright-deploy disabled in this configuration (its default state) so every merge stays an explicit human action.

One agent per user

Every GitHub PR/commit and Slack message an agent sends carries that agent’s own bot identity, so attribution is a function of how many humans share one agent. Running one Shipwright agent per human user (or per accountable team) keeps every change traceable to a single owner; sharing one bot identity across many humans makes attribution ambiguous.

Two independent, optional scoping controls narrow an agent’s exposure further:

Control Default Effect
restrictSlackToMembers false When true, only Slack users registered as that agent’s members can DM or mention it; when false, any user in the Slack workspace can
reviewAuthorAllowlist empty (anyone) A list of GitHub logins whose pull requests the agent will review. An empty allowlist means any authenticated GitHub user’s PRs are eligible for review
patchAuthorAllowlist empty (self-authored only) A list of GitHub logins naming additional authors whose PRs this agent will treat as patch candidates. Patch work normally only touches the agent’s own PRs; this allowlist adds other authors to the candidate pool. An empty allowlist means only self-authored PRs are eligible for patching

Admins vs. agent members

Access control is layered around two roles:

Role Grant mechanism Scope/Visibility
Admin Listed in SHIPWRIGHT_ADMIN_ALLOWED_EMAILS, or holding an admin API key Can see and manage every agent in the deployment: provisioning, env vars, crons, tools, tokens, and deletion
Agent Member Added as an AgentMember (by email) to one specific agent Can see that agent’s detail page and, if Slack access is restricted to members, message it — without any visibility into or control over the rest of the fleet

This lets you grant a teammate scoped visibility into “their” agent without making them an admin over every agent in the deployment.

Logging, monitoring & observability

Sentry integration is opt-in and inert by default — zero telemetry is sent unless SENTRY_DSN is set, independently, on each service (admin, metrics, task-store, agent).

Data Captured by Sentry?
Unhandled exceptions and 5xx errors, with stack traces Yes
console.log / console.warn / console.error calls, forwarded as structured logs Yes
Request path and method for HTTP errors Yes
Caller identity — which admin or agent token triggered an error Yes
Authorization and Cookie header values Redacted — replaced with [Filtered] regardless of configuration
Request or response bodies No — never attached to an event
The live value of any secret-shaped env var Redacted — scrubbed wherever it appears, including nested inside longer strings

Self-hosted Sentry is fully supported: point SENTRY_DSN at your own instance and everything above applies the same way.

Shipwright does not currently provide a tool-call-level audit log — there is no immutable, queryable record of every individual tool invocation an agent makes. Sentry’s caller-identity capture on the error path is not a substitute for a full audit trail; if you need one today, you’ll need to build it on top of your own logging pipeline.

Compliance posture

Control/Certification Status Notes
SOC 2 Not currently held No third-party audit performed
ISO 27001 Not currently held No third-party audit performed
ISO 42001 Not currently held No third-party audit performed
Tool-call-level audit logging Not currently offered See “Logging, monitoring & observability” above — Sentry’s caller-identity capture on the error path is not a substitute
Automated credential rotation Not currently offered
Third-party pen test None performed to date
Vulnerability disclosure In place GitHub private security advisories, 7-day acknowledgment commitment — see SECURITY.md

Because Shipwright is self-hosted and runs entirely inside your own infrastructure, most infrastructure-level controls — physical security, cloud provider compliance certifications, and so on — are inherited from your own cloud provider rather than being a separate Shipwright-operated surface to certify. The same self-hosted model applies to data retention: all persistent data (tasks, PRs, chat history, and so on) lives in your own Postgres database, so retention is entirely your call — Shipwright imposes no retention policy of its own.

FAQ

Where does my source code go? Nowhere outside your own infrastructure. Shipwright is self-hosted — the agent runs in your Kubernetes cluster, against your own GitHub App and repositories. There is no third-party code execution and no Shipwright-operated service that ever receives your source.

Does Shipwright train on my code? No. There is no training pipeline — Shipwright has no third-party code custody of any kind. The only third party that ever sees code content is whichever Anthropic API account you configure yourself, under your own Anthropic API terms.

How do I report a security vulnerability? Via GitHub private security advisories, not a public issue: github.com/app-vitals/shipwright/security/advisories/new. A maintainer will acknowledge the report within 7 days and work with you on a fix and coordinated disclosure — see SECURITY.md for the full policy. See the Compliance posture table above for more on vulnerability disclosure procedures.

What happens if SHIPWRIGHT_ENCRYPTION_KEY is never set? Secrets (env values, Slack tokens, API keys) are stored in plain text in the database, with a warning logged at startup. There’s no forced failure — the service still runs — so this is something you need to set deliberately before any production use.

Can I run with zero third-party network calls? Voice can: self-hosted Whisper for speech-to-text plus the default Piper text-to-speech engine make zero outbound calls. Every other Shipwright service assumes outbound internet access by default (to reach Anthropic, GitHub, and Slack). Restricting egress further is your responsibility, via your own Kubernetes NetworkPolicy or cloud firewall rules. See the Network & egress controls table above for the required and optional external destinations.

Is there an audit log of every action an agent takes? Not a tool-call-level one, today. Sentry (when enabled) captures caller identity on the error path, but that’s not a full audit trail of every tool invocation. See the Compliance posture table above for the current status of tool-call-level audit logging.

Does Shipwright have SOC 2 or similar certifications? No, not currently. See the Compliance posture table above for details on current certification status.

Do I need to expose the admin service to the public internet? No. networking.type=ClusterIP (the default) keeps every service reachable only via kubectl port-forward inside the cluster. Public exposure via Ingress or Gateway API is a choice you make when you’re ready for it, and auth.mode=google or auth.mode=okta is required the moment you do. See the Human access table above for authentication mode requirements.

Does Shipwright support SCIM provisioning? No, not today. There is no SCIM code in the codebase — user access is managed via the SHIPWRIGHT_ADMIN_ALLOWED_EMAILS allowlist and AgentMember records described above, not an automated identity-provider sync. See the Admins vs. agent members table above for how access control is currently managed.

Can I IP-allowlist the admin service? There’s no built-in IP-allowlist feature. The substitute is keeping networking.type=ClusterIP (the default — see above) so the service is never reachable outside the cluster in the first place, plus whatever allowlisting your own ingress layer or load balancer supports if you do expose it.

Is there rate limiting on the admin API? No built-in rate limiting today. If you need to throttle requests to the admin API, put a reverse proxy or API gateway in front of it.

Does every change require human review before it merges? Self-review and automated merging are both disabled by default, so in practice a human reviews and a human clicks merge. If Shipwright performs the merge itself (deploy enabled, or the merge command run directly), it bypasses branch protection’s required-approval rule by design. For a guaranteed, specific human approver, add Code Owners review as described above — that’s the one thing neither self-review nor another agent’s review can satisfy.