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=openperforms 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:
- Admin API key — a bearer token matching an entry in
SHIPWRIGHT_ADMIN_API_KEYS(comma-separatedname:token:scopetuples). Full admin access. - Per-agent bearer token — a bearer token scoped to one agent id. Cross-agent access returns
403. - Session cookie — the same
admin_sessioncookie 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.