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:
- Every item has a named owner.
- 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.
- Run a drift check on a schedule. Use
terraform plan -detailed-exitcodeor AWS Config. Record the result and the date. - 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_LICENSEis 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:Encryptonly fromcodeherder 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:ListBucketis not needed. Thes3:PutObjectaction signs the presigned upload URL. If the bucket uses SSE-KMS, the role also needskms:GenerateDataKeyandkms:Decrypton the bucket key. Add a bucket policy that denies requests withoutaws: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_REPOat 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:PermissionsBoundarycondition). It stops a bounded role from removing or rewriting its own boundary. It also deniesorganizations:*,account:*and any change to CloudTrail. - Deny
kms:ScheduleKeyDeletionandkms:PutKeyPolicyon 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
AssumeRoleinto 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