Skip to content

Commit 2492a30

Browse files
committed
feat(webapp): apply default repository policy on ECR repo creation
Self-hosters that run the webapp's ECR account separately from their EKS worker account hit a 403 Forbidden on every new project's first run: `ensureEcrRepositoryExists` calls CreateRepository but never sets a repository policy, so kubelet can't pull the runner image cross-account. Add an optional `DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY` env var (raw IAM policy JSON, V4 mirror as well). When set, the webapp calls SetRepositoryPolicy after CreateRepository, baking the operator's cross-account pull rule into every new repo automatically. Existing repos are unaffected — they keep their current policy. Cloud is unaffected — the env var is optional and unset by default. Verified locally against a self-host on EKS with cross-account ECR: without the policy, runners stayed in ImagePullBackOff with 403; with it, the same flow completes a hello-world run end-to-end in ~5s.
1 parent 1a7943c commit 2492a30

4 files changed

Lines changed: 37 additions & 1 deletion

File tree

apps/webapp/app/env.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ const EnvironmentSchema = z
300300
DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
301301
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(),
302302
DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(),
303+
DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z.string().optional(), // raw IAM policy JSON applied to every repo created by the webapp
303304

304305
// Deployment registry (v4) - falls back to v3 registry if not specified
305306
V4_DEPLOY_REGISTRY_HOST: z
@@ -332,6 +333,10 @@ const EnvironmentSchema = z
332333
.string()
333334
.optional()
334335
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID),
336+
V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z
337+
.string()
338+
.optional()
339+
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY),
335340

336341
// Compute gateway (template creation during deploy finalize)
337342
COMPUTE_GATEWAY_URL: z.string().optional(),

apps/webapp/app/v3/getDeploymentImageRef.server.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
GetAuthorizationTokenCommand,
99
PutLifecyclePolicyCommand,
1010
PutImageTagMutabilityCommand,
11+
SetRepositoryPolicyCommand,
1112
} from "@aws-sdk/client-ecr";
1213
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
1314
import { tryCatch } from "@trigger.dev/core";
@@ -138,6 +139,7 @@ export async function getDeploymentImageRef({
138139
roleArn: registry.ecrAssumeRoleArn,
139140
externalId: registry.ecrAssumeRoleExternalId,
140141
},
142+
defaultRepositoryPolicy: registry.ecrDefaultRepositoryPolicy,
141143
})
142144
);
143145

@@ -219,12 +221,14 @@ async function createEcrRepository({
219221
accountId,
220222
registryTags,
221223
assumeRole,
224+
defaultRepositoryPolicy,
222225
}: {
223226
repositoryName: string;
224227
region: string;
225228
accountId?: string;
226229
registryTags?: string;
227230
assumeRole?: AssumeRoleConfig;
231+
defaultRepositoryPolicy?: string;
228232
}): Promise<Repository> {
229233
const ecr = await createEcrClient({ region, assumeRole });
230234

@@ -262,6 +266,20 @@ async function createEcrRepository({
262266
})
263267
);
264268

269+
// Apply an operator-provided IAM policy to the new repository. Useful for
270+
// self-hosters whose ECR account is separate from the account running the
271+
// EKS workers — without this the workers get 403 Forbidden when pulling the
272+
// task image (default ECR policy only grants access to the registry owner).
273+
if (defaultRepositoryPolicy) {
274+
await ecr.send(
275+
new SetRepositoryPolicyCommand({
276+
repositoryName: result.repository.repositoryName,
277+
registryId: result.repository.registryId,
278+
policyText: defaultRepositoryPolicy,
279+
})
280+
);
281+
}
282+
265283
return result.repository;
266284
}
267285

@@ -386,11 +404,13 @@ async function ensureEcrRepositoryExists({
386404
registryHost,
387405
registryTags,
388406
assumeRole,
407+
defaultRepositoryPolicy,
389408
}: {
390409
repositoryName: string;
391410
registryHost: string;
392411
registryTags?: string;
393412
assumeRole?: AssumeRoleConfig;
413+
defaultRepositoryPolicy?: string;
394414
}): Promise<{ repo: Repository; repoCreated: boolean }> {
395415
const { region, accountId } = parseEcrRegistryDomain(registryHost);
396416

@@ -428,7 +448,14 @@ async function ensureEcrRepositoryExists({
428448
}
429449

430450
const [createRepoError, newRepo] = await tryCatch(
431-
createEcrRepository({ repositoryName, region, accountId, registryTags, assumeRole })
451+
createEcrRepository({
452+
repositoryName,
453+
region,
454+
accountId,
455+
registryTags,
456+
assumeRole,
457+
defaultRepositoryPolicy,
458+
})
432459
);
433460

434461
if (createRepoError) {

apps/webapp/app/v3/registryConfig.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type RegistryConfig = {
88
ecrTags?: string;
99
ecrAssumeRoleArn?: string;
1010
ecrAssumeRoleExternalId?: string;
11+
ecrDefaultRepositoryPolicy?: string;
1112
};
1213

1314
export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
@@ -20,6 +21,7 @@ export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
2021
ecrTags: env.V4_DEPLOY_REGISTRY_ECR_TAGS,
2122
ecrAssumeRoleArn: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
2223
ecrAssumeRoleExternalId: env.V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
24+
ecrDefaultRepositoryPolicy: env.V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY,
2325
};
2426
}
2527

@@ -31,5 +33,6 @@ export function getRegistryConfig(isV4Deployment: boolean): RegistryConfig {
3133
ecrTags: env.DEPLOY_REGISTRY_ECR_TAGS,
3234
ecrAssumeRoleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN,
3335
ecrAssumeRoleExternalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID,
36+
ecrDefaultRepositoryPolicy: env.DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY,
3437
};
3538
}

docs/self-hosting/env/webapp.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ mode: "wide"
7676
| `DEPLOY_REGISTRY_USERNAME` | No || Deploy registry username. |
7777
| `DEPLOY_REGISTRY_PASSWORD` | No || Deploy registry password. |
7878
| `DEPLOY_REGISTRY_NAMESPACE` | No | trigger | Deploy registry namespace. |
79+
| `DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY` | No || Raw IAM policy JSON applied via SetRepositoryPolicy to every ECR repo created by the webapp. Use to grant cross-account pull access to EKS workers when the ECR account is separate from the cluster account. |
7980
| `DEPLOY_IMAGE_PLATFORM` | No | linux/amd64 | Deploy image platform, same values as docker `--platform` flag. |
8081
| `DEPLOY_TIMEOUT_MS` | No | 480000 (8m) | Deploy timeout (ms). |
8182
| **Object store (S3)** | | | |

0 commit comments

Comments
 (0)