
moonrepo remote cache on S3: example config + IAM policy, and how do we keep it safe in CI?
If you’re adopting moonrepo’s remote caching, pushing cache artifacts to Amazon S3 is one of the most convenient ways to speed up CI pipelines. The challenge is doing it safely: configuring the S3 bucket, IAM policies, and CI credentials so that cache reads/writes are fast, reliable, and locked down.
This guide walks through:
- A complete example moonrepo remote cache configuration for S3
- A minimal IAM policy (with optional stricter variants)
- How to keep your moonrepo remote cache on S3 safe in CI, including GitHub Actions examples
- Common pitfalls and best practices
How moonrepo remote cache on S3 works
At a high level:
- Moon runs tasks and generates cache artifacts (build outputs, test results, etc.).
- When remote caching is enabled, Moon uploads these artifacts to a remote store—in this case, S3.
- Other developers or CI jobs with access to that S3 bucket can download the same artifacts instead of recomputing them.
Key security objectives:
- Only your CI and trusted developers can read/write the cache.
- Public internet users should not be able to access the artifacts.
- CI credentials are short-lived and scoped down to cache operations.
Example moonrepo remote cache config for S3
moonrepo is configured via moon.yml (or moon.json / moon.toml). For an S3 remote cache, the config typically goes under the remoteCache section.
Here’s a realistic YAML example:
# moon.yml
remoteCache:
enabled: true
# Type of remote cache backend
# (The exact field name may differ depending on your moonrepo version;
# consult your current docs, but the structure is generally similar.)
provider: "s3"
s3:
# AWS region where the bucket lives (e.g., us-east-1, eu-west-1)
region: "us-east-1"
# Name of the S3 bucket
bucket: "my-moonrepo-cache-bucket"
# Optional prefix/key prefix in the bucket to avoid clashing with other data
# e.g., projects/my-monorepo-cache/
prefix: "moon-cache/"
# Optional: Configure S3 endpoint for self-hosted / minio-style setups
# endpoint: "https://s3.amazonaws.com"
# Optional: how long to cache artifacts locally (in seconds) before re-checking remote
#ttl: 3600
# Optional: set to true if using path-style URLs (for some S3-compatible services)
#forcePathStyle: false
# Optional: limit cache uploads from local dev machines and only upload from CI
# (Useful to prevent accidental leaking of sensitive artifacts)
uploadFromLocal: false
Moon will use standard AWS SDK credential resolution. That means it will pick credentials from:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,AWS_REGION) - AWS CLI profiles / shared config files
- EC2/ECS/role-based credentials (if running in AWS)
In CI, you typically rely on:
- OIDC +
aws-actions/configure-aws-credentials(GitHub Actions) - Native CI IAM roles (e.g., GitHub-hosted runners with AWS roles, GitLab AWS integration, etc.)
Creating a secure S3 bucket for moonrepo remote cache
Before we define IAM, create and lock down the bucket that will store the moonrepo remote cache on S3.
1. Create the bucket
Use the AWS console or CLI:
aws s3api create-bucket \
--bucket my-moonrepo-cache-bucket \
--region us-east-1 \
--create-bucket-configuration LocationConstraint=us-east-1
Pick a region close to your CI runners to minimize latency.
2. Block public access
Explicitly block public access at the bucket level:
aws s3api put-public-access-block \
--bucket my-moonrepo-cache-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
This ensures that even if someone tries to add a public ACL or bucket policy, it’s blocked at the control plane.
3. Optional: server-side encryption
For most teams, SSE-S3 is enough; some require SSE-KMS.
SSE-S3 (simple, no key management overhead)
aws s3api put-bucket-encryption \
--bucket my-moonrepo-cache-bucket \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}'
SSE-KMS (for stricter compliance)
Create or reuse a KMS key and enforce it:
aws s3api put-bucket-encryption \
--bucket my-moonrepo-cache-bucket \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
}
}
]
}'
Then ensure your CI IAM role has permission to use that KMS key (more on that later).
Minimal IAM policy for moonrepo remote cache on S3
You want the principle of least privilege: the role or user used in CI should only access this cache bucket (and optionally, only a specific prefix).
Below is a minimal policy allowing read/write to everything under s3://my-moonrepo-cache-bucket/moon-cache/.
Example IAM policy document
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MoonRepoRemoteCacheBucketAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-moonrepo-cache-bucket",
"arn:aws:s3:::my-moonrepo-cache-bucket/moon-cache/*"
]
}
]
}
Explanation:
s3:GetObject,s3:PutObject: needed to read/write cache artifacts.s3:DeleteObject: optional; needed if you or Moon will prune cache by deleting objects.s3:ListBucket: required if Moon lists objects or checks for existence via listing.- Restricts access to only this bucket and the chosen prefix
moon-cache/.
If you want to disallow deletes, simply remove "s3:DeleteObject" from the policy. Your cache will grow without deletions, but it may be acceptable if lifecycle rules prune old objects.
Optional: even tighter policy with conditions
You can further lock your moonrepo remote cache on S3 using IAM conditions.
Restrict to a specific prefix
If multiple projects share the same bucket, you can scope CI to a single prefix (e.g., moon-cache/project-a/):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MoonRepoRemoteCachePrefixAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-moonrepo-cache-bucket/moon-cache/project-a/*"
},
{
"Sid": "MoonRepoRemoteCacheListPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-moonrepo-cache-bucket",
"Condition": {
"StringLike": {
"s3:prefix": "moon-cache/project-a/*"
}
}
}
]
}
Now, this role cannot interact with cache data outside moon-cache/project-a.
Restrict encryption key (if using KMS)
If you use SSE-KMS, add a statement allowing KMS operations for your specific key:
{
"Sid": "AllowKMSForMoonRepoCache",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
}
Attach this to the same IAM principal as the S3 policy.
How to keep moonrepo remote cache on S3 safe in CI
Running CI with access to an S3 cache always raises a key question: how do we keep it safe while still getting performance gains?
The main principles:
- Use short-lived credentials (OIDC + role assumption, not long-lived keys).
- Limit what CI can do (least-privilege IAM policies as shown above).
- Prevent secrets from leaking in logs (no printing AWS keys, no verbose S3 endpoints).
- Don’t allow untrusted PRs full write access to your cache.
Below are concrete approaches.
GitHub Actions: secure configuration example
GitHub Actions is a common CI choice for moonrepo remote cache on S3. Modern best practice is using OIDC to assume an AWS IAM role at workflow runtime—no static keys in secrets.
1. Create an IAM role for GitHub Actions
In AWS, create an IAM role with a web identity trust for GitHub OIDC. The trust policy might look like this (simplified):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
}
}
}
]
}
- Replace
123456789012with your AWS account ID. - Replace
my-org/my-repowith your GitHub org/repo name. - Tighten further by limiting
subto certain branches or environments if desired.
Attach the S3 (and optional KMS) policies discussed earlier to this role.
2. Configure the GitHub Action workflow
A typical workflow using moonrepo remote cache on S3 might look like:
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
permissions:
id-token: write # required for OIDC
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-moonrepo-cache-role
aws-region: us-east-1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run moonrepo tasks with remote cache
env:
# AWS creds are automatically exported by configure-aws-credentials
# Optionally set MOON_* env vars if needed by your version of moonrepo:
# MOON_CACHE_REMOTE_ENABLED: "true"
# MOON_CACHE_REMOTE_PROVIDER: "s3"
# MOON_S3_BUCKET: "my-moonrepo-cache-bucket"
# MOON_S3_REGION: "us-east-1"
run: |
npx moon run :build
npx moon run :test
Notes:
aws-actions/configure-aws-credentialsobtains short-lived credentials via OIDC and configures them as environment variables.- There are no static
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYstored in GitHub secrets. - Permissions can be limited to certain branches via your IAM role’s trust policy or by separate roles/workflows.
Distinguishing cache access for PRs vs main branch
If you allow forks or untrusted contributors to run CI, you may not want them to write to your moonrepo remote cache on S3.
Common patterns:
-
Read-only access on PRs, read/write on main
- Create two IAM roles:
github-moonrepo-cache-readwritegithub-moonrepo-cache-readonly
- Read-only role policy only has
s3:GetObjectands3:ListBucket. - Use workflow conditions:
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure AWS credentials (read/write on main) if: github.ref == 'refs/heads/main' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-moonrepo-cache-readwrite aws-region: us-east-1 - name: Configure AWS credentials (read-only on PRs) if: github.event_name == 'pull_request' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/github-moonrepo-cache-readonly aws-region: us-east-1 - name: Run moonrepo tasks run: npx moon run :build - Create two IAM roles:
-
Disable remote uploads for PRs from forks
- In your
moon.yml, ensureuploadFromLocal: falseso that only CI can upload. - In CI, set an env var or CLI flag to enable uploads only in trusted contexts.
For example, only enable remote uploads on main:
- name: Run moonrepo tasks env: MOON_CACHE_REMOTE_UPLOAD: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }} run: npx moon run :buildThis depends on how your moonrepo version expects environment flags; adjust naming accordingly.
- In your
Keeping remote cache secrets out of logs
Even though OIDC roles avoid static secrets, you still want to ensure:
- You never echo AWS credentials in scripts or debugging output.
- You avoid printing full S3 URLs with query parameters (signed URLs) if you ever generate them.
- Shell traces like
set -xdo not inadvertently log secrets.
Checklist:
- Do not run
envin logs for debugging unless in a secure internal environment. - If you must debug, scrub sensitive variables before printing.
- Return to normal logging after you’ve validated the configuration.
Lifecycle policies: managing cost and retention
A moonrepo remote cache on S3 can grow quickly if you don’t purge old artifacts. Instead of giving the CI role delete powers, you can delegate cleanup to S3’s lifecycle policies:
aws s3api put-bucket-lifecycle-configuration \
--bucket my-moonrepo-cache-bucket \
--lifecycle-configuration '{
"Rules": [
{
"ID": "ExpireOldMoonRepoCache",
"Prefix": "moon-cache/",
"Status": "Enabled",
"Expiration": {
"Days": 30
}
}
]
}'
This automatically deletes cache objects older than 30 days with no extra logic in moonrepo or CI.
Local development vs CI: sharing the same remote cache
You can choose whether local developers also use the moonrepo remote cache on S3 or if it should be CI-only.
Option 1: CI-only remote cache
Pros: simpler security, no local AWS credentials needed.
- Set
uploadFromLocal: falseinmoon.yml. - Developers rely on local cache only.
- CI both reads and writes to remote cache; devs benefit indirectly because CI builds are faster.
Option 2: CI + developer remote cache
Pros: fastest possible experience; devs pull artifacts from CI and from each other.
- Developers must authenticate with AWS (short-lived credentials via AWS SSO recommended).
- Use IAM groups/roles to grant them access only to the cache bucket.
- Consider read-only access for most devs; write access only from CI and a few trusted users.
Example for local dev using AWS SSO:
aws sso login --profile dev-moonrepo
export AWS_PROFILE=dev-moonrepo
npx moon run :build
Moon uses the profile credentials, and your remote cache remains consistent across local envs and CI.
Common pitfalls and how to avoid them
-
Bucket accidentally public
- Always enable “Block public access” at the account and bucket levels.
- Avoid bucket policies that use
Principal: "*", especially withGetObject.
-
Overly broad IAM access
- Don’t use
s3:*onarn:aws:s3:::*just to “get it working.” - Start with the minimal policy provided and expand only when necessary.
- Don’t use
-
Static long-lived access keys in CI
- Replace them with OIDC and IAM roles as soon as possible.
- Rotate any existing keys and remove them from your secrets.
-
Performance issues due to distant regions or poor prefixing
- Place the S3 bucket in the same region as your runners when possible.
- Use a prefix to avoid collisions and optionally segment by project.
-
Leaking credentials or sensitive outputs in logs
- Avoid printing
AWS_*env vars. - Watch for third-party actions that might echo environment configurations.
- Avoid printing
Summary
To safely run a moonrepo remote cache on S3 in CI:
- Configure Moon to use an S3-based remote cache with a dedicated bucket and prefix.
- Lock down the bucket: block public access, enable server-side encryption, and (optionally) lifecycle rules.
- Attach a least-privilege IAM policy that allows only the needed S3 (and KMS) actions on that bucket/prefix.
- Use OIDC-based short-lived credentials in CI (e.g., GitHub Actions +
configure-aws-credentials). - Distinguish read vs write access between trusted branches (main) and untrusted PRs.
- Avoid logging secrets and keep the blast radius small with tightly scoped roles.
With this setup, you get the full speed benefits of moonrepo’s remote cache on S3 while keeping your CI and infrastructure safe and maintainable.