CodeHerderSearch⌘KRequest access →

Self-hosted infrastructure

Record the infrastructure you run beside a self-hosted server, list every outbound destination the server can reach, and grant its AWS roles least privilege.

A self-hosted CodeHerder server runs in your account. Your organization owns the infrastructure around it. This page gives you four things:

  • A template to record what you run beside CodeHerder.
  • Every outbound destination the server can reach, and the setting that turns each one on.
  • The AWS permissions the server role needs.
  • Guidance for the automation roles that build your infrastructure.

For the download and start steps, see Self-hosted deployment.

Inventory template

Some of your infrastructure will live outside your infrastructure-as-code. Alarms, health checks and one-off scripts are common examples. The CodeHerder vendor hosts its own alarms by script outside Terraform, so this pattern is normal. It is safe only when you record it.

Copy this table and fill it in. Add one row for each item.

Item What it does Managed by (Terraform / script / console) Owner Where it is declared Drift check Last checked
PostgreSQL instance and backups Primary store
Secret-store KMS key Encrypts stored secrets
Attachments bucket Stores uploaded files
Cognito user pool and app clients Sign-in and invites
SES identity and DNS records Sends email
TLS certificate and DNS names Serves the server URL
Reverse proxy configuration Terminates TLS, redacts logs
Host systemd drop-ins and env file Runs the server and holds its settings
Alarms and health checks Tell you the server is down
Log shipper Moves logs off the host
Device hosts Run agent sessions
IAM roles (server and automation) Grant AWS access

Follow these rules:

  1. Every item has a named owner.
  2. Anything you make outside infrastructure-as-code gets a script that can list or plan with no changes. Otherwise, import it into your infrastructure-as-code.
  3. Run a drift check on a schedule. Use terraform plan -detailed-exitcode or AWS Config. Record the result and the date.
  4. Review the whole inventory every quarter.

Outbound destinations

On a self-host, you choose every receiver in these tables. CodeHerder names none of them as its subprocessor. The only destinations that CodeHerder picks are the release host and the installer script. Those rows say so. Self-hosted data residency says what each destination receives.

Server

This table lists every host the server process can call. The server needs PostgreSQL to start. In production (CH_ENV=production) or with an enterprise licence, it also needs SES. Every other row is optional and off until you set its knob.

In a row, <region> is the resolved region: the surface knob, else CH_AWS_REGION, else us-west-2.

SCIM clients, devices, the CLI and browsers connect in to the server. The server does not call them.

Destination Port Feature Required? Knob that turns it on (or off)
Your PostgreSQL host 5432 or the port in the DSN Primary store Required the --db flag or CH_DB
SES v2 email.<region>.amazonaws.com 443 Verification, invite and email-change mail. There is no SMTP transport. Required when CH_ENV=production or an enterprise licence is installed CH_MAILER, CH_MAILER_FROM, CH_SES_REGION, CH_AWS_REGION
KMS kms.<region>.amazonaws.com 443 Envelope encryption for the secret store. Without it, the secret store answers 503. Optional CH_KMS_KEY_ID, CH_KMS_REGION, CH_AWS_REGION
S3 <bucket>.s3.<region>.amazonaws.com 443 Attachments. Browsers also upload and download through presigned URLs. Optional CH_S3_BUCKET, CH_S3_REGION, CH_AWS_REGION
Cognito cognito-idp.<region>.amazonaws.com (API and signing keys) 443 Cognito identity mode: token keys, invites, deprovisioning, SSO identity-provider setup Required in Cognito mode CH_COGNITO_USER_POOL_ID, CH_COGNITO_REGION, CH_AWS_REGION, CH_SSO_IDP_PROVISIONING
Cognito hosted domain, /oauth2/token 443 Remote MCP OAuth code exchange Optional CH_COGNITO_DOMAIN, CH_COGNITO_MCP_CLIENT_ID, CH_MCP_ENABLED
Your OIDC issuer (discovery and signing keys) 443 OIDC identity mode, the alternative to Cognito Required in OIDC mode CH_OIDC_ISSUER, CH_OIDC_JWKS_URL
ECR api.ecr.<region>.amazonaws.com 443 Mints the sandbox-image pull token that devices use Used when a device calls /v1/sandbox-image CH_SANDBOX_ECR_REPO, CH_SANDBOX_ECR_REGION, CH_AWS_REGION
AWS credential chain: instance metadata 169.254.169.254 and STS 80, 443 Resolves the instance role Required if any AWS row is on standard AWS_* variables
GitHub (api.github.com, or your GitHub Enterprise /api/v3) and GitLab (/api/v4) 443 Integration test, merge-state and conflict checks, repository activity Optional (a workspace git connection) CH_MERGE_RECONCILE_SEC, CH_REPO_ACTIVITY_SWEEP_SEC, CH_INTEGRATIONS_SECRET_KEY
slack.com 443 Slack OAuth code exchange only Optional CH_SLACK_CLIENT_ID, CH_SLACK_CLIENT_SECRET
Webhook URLs you configure (any public host) 443 Outbound webhooks, including the audit export. The server blocks private addresses. Optional a webhook subscription; CH_WEBHOOK_SECRET_KEY
Your OTLP/HTTP collector your choice Audit trace export Optional CH_OTEL_TRACES_ENDPOINT
models.dev 443 Model price catalogue sync. On by default. Optional CH_COST_PRICING_SYNC=0 turns it off
api.anthropic.com, admin usage report only 443 Daily cost reconcile. This is not model inference. Optional ANTHROPIC_ADMIN_API_KEY and ANTHROPIC_ORG_ID
AWS Lambda microVM API lambda.<region>.amazonaws.com and the microVM runner 443 MicroVM runner launch and sweep Optional CH_MICROVM_EGRESS_CONNECTORS, CH_MICROVM_MAX_PER_WORKSPACE, CH_MICROVM_SWEEP_SEC
Your DNS resolver 53 SSO domain TXT check, git remote host check Required CH_SSO_DOMAIN_RECHECK_INTERVAL

The survey codeherder channel sends nothing off the host. It only lets one workspace read response rows in the same database. Leave CH_CODEHERDER_WORKSPACE_ID unset and each workspace sees only its own responses.

The server never calls these:

  • No AI model inference. Devices call Anthropic, Bedrock or your gateway.
  • No SMTP server.
  • No CloudWatch.
  • No licence server. CH_LICENSE is verified offline.
  • No update check.

Devices

A device runs the device server, the ch CLI and the harnesses. This table lists every host a device calls. The release host row is the only one that CodeHerder picks.

Destination Port Feature Required? Knob that turns it on (or off)
Release host: manifest.json, its signature and the binaries. Official builds use the CodeHerder release host. 443 Self-update of ch and the device server Optional CH_AUTO_UPDATE=0 and CH_DS_AUTO_UPDATE=0 turn it off. CH_CLI_MANIFEST_URL points it at your own mirror.
Installer script host 443 One-time install of ch. The script reads its release base URL from the shell variable CH_INSTALL_BASE_URL. Optional Run the install yourself from your own mirror
AI providers: Anthropic, Bedrock, your gateway, OpenAI 443 Model inference. See the AI provider routes page in the repository docs. Required for agent work CH_CLAUDE_BEDROCK, CH_CLAUDE_AWS_REGION, CH_ALLOWED_AGENT_BASE_URL_HOSTS
api.anthropic.com, chatgpt.com, cursor.com, cloudcode-pa.googleapis.com 443 Usage-limit probes Optional CH_AI_LIMITS_PROBE=0 turns them off
Harness vendor hosts (each harness binary’s own sign-in and telemetry) 443 CodeHerder does not control this traffic. Depends on the harness CH_DS_HARNESS_INCLUDE, CH_DS_HARNESS_EXCLUDE choose which harnesses run
Your git remotes (clone, fetch, push, and gh or glab merge-request calls) 22 or 443 Repository work Required for repository work The repositories a workspace binds; CH_WORKTREE_POOL_PER_REPO
Your container registry (ECR), from the sandbox image the server names 443 Pull of the sandbox image, Docker mode only Docker mode CH_DOCKER_IMAGE skips the pull
Instance metadata 169.254.169.254 80 Spot interruption watch, EC2 devices only Optional CH_SPOT_WATCH
Your identity provider token endpoint 443 ch login only Required to sign in Your identity mode
github.com 443 The Claude Code plugin marketplace named in the sandbox image’s Claude settings. It is image content. Optional No knob. Change the image.

The stage egress proxy is open by default. Strict mode allows a fixed host list plus the hosts you add. See the device egress guide.

To enforce this list, see Self-hosted outbound firewall.

For AWS services, a VPC endpoint keeps the traffic private. Use interface endpoints for KMS, SES, Cognito, ECR and STS. Use a gateway endpoint for S3.

AWS permissions

Give the server an instance role. Do not use static access keys. Require IMDSv2 with a hop limit of 1.

The policy below follows what the code calls, so it is tighter than a broad grant. Replace every placeholder in angle brackets. Remove the statements for features you leave off.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SecretStoreKey",
      "Effect": "Allow",
      "Action": ["kms:GenerateDataKey", "kms:Encrypt", "kms:Decrypt", "kms:DescribeKey"],
      "Resource": "arn:aws:kms:<region>:<account>:key/<key-id>"
    },
    {
      "Sid": "Attachments",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::<bucket>/*"
    },
    {
      "Sid": "SendMail",
      "Effect": "Allow",
      "Action": "ses:SendEmail",
      "Resource": "arn:aws:ses:<region>:<account>:identity/<ses-identity>"
    },
    {
      "Sid": "CognitoUsers",
      "Effect": "Allow",
      "Action": [
        "cognito-idp:AdminCreateUser",
        "cognito-idp:AdminUserGlobalSignOut",
        "cognito-idp:AdminDisableUser",
        "cognito-idp:AdminEnableUser",
        "cognito-idp:AdminDeleteUser"
      ],
      "Resource": "arn:aws:cognito-idp:<region>:<account>:userpool/<pool-id>"
    },
    {
      "Sid": "CognitoSsoProvisioning",
      "Effect": "Allow",
      "Action": [
        "cognito-idp:CreateIdentityProvider",
        "cognito-idp:UpdateIdentityProvider",
        "cognito-idp:DeleteIdentityProvider",
        "cognito-idp:DescribeIdentityProvider",
        "cognito-idp:ListIdentityProviders",
        "cognito-idp:DescribeUserPoolClient",
        "cognito-idp:UpdateUserPoolClient"
      ],
      "Resource": "arn:aws:cognito-idp:<region>:<account>:userpool/<pool-id>"
    },
    {
      "Sid": "EcrToken",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "EcrPull",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "arn:aws:ecr:<region>:<account>:repository/<ecr-repo>"
    }
  ]
}

Add the CognitoSsoProvisioning statement only when CH_SSO_IDP_PROVISIONING is on. It holds the seven identity-provider actions the server calls.

Notes on each statement:

  • KMS is dual control. The key policy must also name the server role. A role policy alone is not enough. Add a key-policy statement that allows the same four actions to the server role. Turn key rotation on, use a 30-day deletion window, and plan key escrow as in the key escrow guide. The server calls kms:Encrypt only from codeherder reseal, which re-wraps data keys under a new key. See Self-hosted KMS keys and attachments bucket for a module that sets this up.
  • S3. The code never lists the bucket, so s3:ListBucket is not needed. The s3:PutObject action signs the presigned upload URL. If the bucket uses SSE-KMS, the role also needs kms:GenerateDataKey and kms:Decrypt on the bucket key. Add a bucket policy that denies requests without aws:SecureTransport, and turn on Block Public Access. The same page ships a module for this bucket.
  • Cognito. The code never calls AdminGetUser.
  • ECR. The server mints a pull token that carries the role’s permissions. A self-host needs its own repository that holds the sandbox image, or a cross-account pull. Point CH_SANDBOX_ECR_REPO at it.

Do not grant these:

  • iam:*.
  • s3:ListAllMyBuckets.
  • A wildcard key or bucket.
  • Static access keys.

Automation roles

Automation roles are the roles your own pipeline uses to apply infrastructure, for example Terraform in CI. Scope and audit them separately from the server role.

  • Split plan from apply. The plan role is read-only. The apply role runs only from a protected branch or tag. Trust that branch or tag through OIDC federation.
  • Put a permission boundary on any role that can create IAM roles. The boundary allows only the services you need, plus the IAM actions that make and read roles. It denies role creation and role policy writes unless the new role carries the same boundary (use the iam:PermissionsBoundary condition). It stops a bounded role from removing or rewriting its own boundary. It also denies organizations:*, account:* and any change to CloudTrail.
  • Deny kms:ScheduleKeyDeletion and kms:PutKeyPolicy on the secret-store key to every principal except the key administrators.
  • Turn on CloudTrail in every region. Keep the log-file validation bucket in a separate account, or use Object Lock.
  • Alert on any AssumeRole into an automation role from outside your pipeline. Alert on key-policy and IAM changes.
  • Review these roles every quarter. Record the owner and the date in the inventory template above.

This example boundary shows the shape. Adjust the service list to your estate. Replace <boundary-name> with the name of this policy.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowNeededServices",
      "Effect": "Allow",
      "Action": ["ec2:*", "rds:*", "s3:*", "kms:*", "ses:*", "cognito-idp:*", "ecr:*", "cloudwatch:*", "logs:*"],
      "Resource": "*"
    },
    {
      "Sid": "AllowIamRoleManagement",
      "Effect": "Allow",
      "Action": [
        "iam:Get*",
        "iam:List*",
        "iam:CreateRole",
        "iam:TagRole",
        "iam:PutRolePolicy",
        "iam:AttachRolePolicy",
        "iam:PassRole"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyRoleWritesWithoutBoundary",
      "Effect": "Deny",
      "Action": ["iam:CreateRole", "iam:PutRolePolicy", "iam:AttachRolePolicy"],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {"iam:PermissionsBoundary": "arn:aws:iam::<account>:policy/<boundary-name>"}
      }
    },
    {
      "Sid": "DenyBoundaryRemoval",
      "Effect": "Deny",
      "Action": "iam:DeleteRolePermissionsBoundary",
      "Resource": "*"
    },
    {
      "Sid": "DenyBoundarySwap",
      "Effect": "Deny",
      "Action": "iam:PutRolePermissionsBoundary",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {"iam:PermissionsBoundary": "arn:aws:iam::<account>:policy/<boundary-name>"}
      }
    },
    {
      "Sid": "DenyBoundaryPolicyEdit",
      "Effect": "Deny",
      "Action": [
        "iam:CreatePolicyVersion",
        "iam:DeletePolicy",
        "iam:DeletePolicyVersion",
        "iam:SetDefaultPolicyVersion"
      ],
      "Resource": "arn:aws:iam::<account>:policy/<boundary-name>"
    },
    {
      "Sid": "DenyAccountControl",
      "Effect": "Deny",
      "Action": ["organizations:*", "account:*", "cloudtrail:StopLogging", "cloudtrail:DeleteTrail", "cloudtrail:UpdateTrail"],
      "Resource": "*"
    }
  ]
}

Last updated

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch the work move. Costs update as it happens.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close