# Self-hosted infrastructure

Source: https://codeherder.com/docs/self-host-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](https://codeherder.com/docs/self-hosting/).

## 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](https://codeherder.com/docs/self-host-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](https://codeherder.com/docs/self-host-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](https://codeherder.com/docs/self-host-kms-and-attachments/) 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": "*"
    }
  ]
}
```
