moonrepo remote cache on S3: example config + IAM policy, and how do we keep it safe in CI?
Developer Productivity Tooling

moonrepo remote cache on S3: example config + IAM policy, and how do we keep it safe in CI?

9 min read

Using moonrepo with a remote cache on Amazon S3 is a powerful way to speed up builds across developer machines and CI, but it raises a few practical questions: what does the S3 configuration look like, what IAM permissions are required, and how do you keep access safe in CI? This guide walks through a complete example: moonrepo config, S3 bucket setup, minimal IAM policy, and security patterns for CI systems.


How moonrepo remote caching works (quick overview)

Moonrepo’s remote cache stores build artifacts keyed by inputs (source, environment, dependencies). When remote caching is enabled:

  • Local runs can read from the cache (to reuse previous artifacts).
  • CI and other builders can write and read to share results.
  • The cache backend is pluggable: you can use S3, GCS, etc.

For S3 specifically, moonrepo:

  • Writes objects (artifacts) into a bucket, typically under a prefix like moon-cache/.
  • Reads objects by key when a matching cache entry exists.
  • Uses AWS credentials from the standard SDK chain (environment vars, role, profile, etc.).

Example moonrepo config for S3 remote cache

You configure remote caching in your Moon workspace configuration. Depending on your setup, this will usually be in .moon/workspace.yml or moon.yml.

Below is a realistic example for an S3-backed remote cache.

Basic S3 remote cache configuration

# .moon/workspace.yml
tasks:
  # Enable the global tasks cache
  cache:
    engine: "s3"
    options:
      # Required: name of your cache bucket
      bucket: "my-company-moon-cache-prod"

      # Optional: path prefix inside the bucket
      # (helps organize / segregate environments or monorepos)
      prefix: "moon-cache/main-repo"

      # Optional: region, if you want to override env / AWS default
      region: "us-east-1"

      # Optional: whether to compress artifacts before uploading
      compression: "gzip"

      # Optional: tune TTL behavior if moon supports your version
      # maxAge: 604800  # cache TTL in seconds (example, 7 days)

    # Control remote caching behavior
    remote:
      read: true   # allow reading from remote cache
      write: true  # allow writing to remote cache

# Optional: You can further override cache behavior per project if needed.

Key points:

  • engine: "s3" tells moonrepo to use S3 as the remote backend.
  • bucket is the S3 bucket with your cache objects.
  • prefix helps you separate environments, repos, or branches.
  • remote.read and remote.write can be toggled per environment (more on that below).

Example: restricting writes locally, but allowing read in CI

You might want local developers to read from CI’s cache but not write to the shared S3 cache (to avoid polluted cache entries).

You can do this by:

  • Defaulting to read: true, write: false in config.
  • Overriding with write: true in CI via env vars or a CI-specific config.

For example, using environment variable overrides (exact variable names may differ by moon version; adjust accordingly to official docs):

# In local dev, no env vars:
export MOON_CACHE_REMOTE_READ=true
export MOON_CACHE_REMOTE_WRITE=false

# In CI job config (GitHub Actions example):
env:
  MOON_CACHE_REMOTE_READ: "true"
  MOON_CACHE_REMOTE_WRITE: "true"

Or, you can keep your base workspace config read-only and use a CI-specific config file merged via your CI pipeline.


S3 bucket setup for moonrepo remote cache

Create an S3 bucket dedicated to moonrepo cache:

  • Name example: my-company-moon-cache-prod
  • Block all public access.
  • Enable default encryption (SSE-S3 or SSE-KMS).
  • Turn on lifecycle rules to expire old cache objects.

Example S3 bucket settings

  • Block Public Access: enabled (all four options).
  • Bucket Policy: no public Principal (*) allow statements.
  • Encryption:
    • SSE-S3 (AES-256) or SSE-KMS with a dedicated KMS key.
  • Lifecycle Rules:
    • Transition to cheaper storage after some days (optional).
    • Expire after 7–30 days depending on cache volume and costs.

Example lifecycle rule (conceptual):

  • Rule name: moon-cache-expire
  • Filter: prefix moon-cache/
  • Expiration: 14 days after creation.

Minimal IAM policy for moonrepo remote cache on S3

To keep the cache safe, you want the principle of least privilege:

  • One IAM role or user per environment (e.g., moon-cache-ci, moon-cache-dev).
  • Each role has access only to:
    • The cache bucket you specify.
    • A specific prefix if you need separation (e.g., moon-cache/{env}/).

Basic S3 cache IAM policy (read & write)

This example grants read/write access only to objects under a specific prefix. Adjust ACCOUNT_ID, REGION, BUCKET_NAME, and PREFIX for your environment.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucketPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::BUCKET_NAME",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "moon-cache/main-repo/*"
          ]
        }
      }
    },
    {
      "Sid": "ReadWriteCacheObjects",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::BUCKET_NAME/moon-cache/main-repo/*"
    }
  ]
}

Notes:

  • ListBucket is restricted to objects with prefix moon-cache/main-repo/.
  • Only GetObject, PutObject, and DeleteObject are allowed.
  • No permission to change bucket policy, ACLs, or other buckets.

If you want read-only access (for some environments), remove s3:PutObject and s3:DeleteObject.

Optional: KMS key policy for SSE-KMS

If you use SSE-KMS (recommended for sensitive repos), your builders must have permissions on the KMS key.

Attach a KMS key policy or IAM policy like:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowUseOfKeyForMoonCache",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_ID:role/moon-cache-ci"
      },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey*",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    }
  ]
}

Then configure your bucket’s default encryption to use this KMS key.


How to authenticate moonrepo to S3

Moonrepo depends on the standard AWS credentials chain. You don’t configure keys inside moon itself; instead, you configure the environment:

  • AWS access key pair:
    • AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    • Optionally AWS_SESSION_TOKEN if using temporary credentials.
  • IAM role:
    • For EC2, ECS, Lambda, or CI via OIDC.
  • AWS profile:
    • AWS_PROFILE pointing to ~/.aws/credentials.

Prefer roles and short-lived credentials over long-lived access keys, especially for CI.

Local dev authentication

Options:

  1. AWS SSO / IAM Identity Center:
    • Use aws configure sso and AWS_PROFILE=moon-dev when running moon.
  2. Named profile with access keys:
    • Store in ~/.aws/credentials.
    • Export AWS_PROFILE=moon-dev.
  3. Env vars:
    • Export access keys locally only for test environments.

Keep production cache credentials separate from dev.

CI authentication

How you authenticate depends on your CI provider:

  • GitHub Actions:
    • Use OIDC + aws-actions/configure-aws-credentials.
  • GitLab CI:
    • Use an IAM role with web identity or access keys in CI variables.
  • CircleCI, Buildkite, etc.:
    • Typically: attach an IAM role to runners or inject short-lived credentials.

Example for GitHub Actions is below.


Secure CI setup for moonrepo S3 cache

The main concerns for “how do we keep it safe in CI?”:

  1. Avoid hard-coded AWS keys in CI config.
  2. Limit privilege to only the cache bucket/prefix.
  3. Use short-lived credentials.
  4. Prevent unauthorized jobs/environments from hitting the same cache.
  5. Avoid leaking cache contents across forks and pull requests.

Example: GitHub Actions + S3 remote cache with OIDC

Step 1: Create IAM role for GitHub Actions

Create a role like my-company-moon-cache-ci that the GitHub OIDC provider can assume.

Example trust policy (simplified; restrict to your repo + branch):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID: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:ORG/REPO:*"
        }
      }
    }
  ]
}

Attach the S3 cache IAM policy discussed earlier to this role.

Step 2: Configure GitHub Actions workflow

name: CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # required for OIDC
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials for moon cache
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/my-company-moon-cache-ci
          aws-region: us-east-1

      - name: Setup Node (if needed)
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Enable moon remote cache write in CI
        run: |
          echo "MOON_CACHE_REMOTE_READ=true" >> $GITHUB_ENV
          echo "MOON_CACHE_REMOTE_WRITE=true" >> $GITHUB_ENV

      - name: Run moon tasks
        run: |
          npx moon run :test
          npx moon run :build

Highlights:

  • Credentials are short-lived, issued per job through OIDC.
  • The role scopes access to the cache bucket/prefix only.
  • Environment variables toggle moon remote read/write behavior.

Handling pull requests securely

Consider the following:

  • Incoming PRs from forks:
    • Often you should not give them access to prod cache or AWS at all.
    • Use conditional steps:
      • For pull_request from forks: disable remote cache or use a separate low-privilege cache.
  • Branch-specific cache namespaces:
    • Use S3 prefix per branch or per environment:
      • moon-cache/main/
      • moon-cache/feature/*
    • Or let moon incorporate branch in cache key if supported.

Example GitHub Actions conditional:

      - name: Configure AWS credentials
        if: github.event.pull_request.head.repo.fork == false
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/my-company-moon-cache-ci
          aws-region: us-east-1

For forked PRs, skip this step and rely on local cache only.


Additional hardening tips for S3 remote caches

To keep your moonrepo remote cache on S3 as safe as possible:

1. Use private buckets only

  • Block public access at both the S3 account and bucket level.
  • Do not use bucket policies that allow Principal: "*".
  • Review the bucket policy regularly.

2. Use encryption everywhere

  • Default S3 encryption:
    • SSE-S3 is acceptable for most builds.
    • Use SSE-KMS for extra control/auditing or stricter compliance.
  • Ensure your IAM roles have access to the KMS key if you use SSE-KMS.

3. Lifecycle policies to reduce data exposure

  • Old cache entries are not usually needed forever.
  • Expire them after 7–30 days to reduce:
    • Cost.
    • Risk of very old artifacts lingering in storage.

4. Separate dev, staging, and production caches

Use either:

  • Separate buckets: moon-cache-dev, moon-cache-staging, moon-cache-prod.
  • Or separate prefixes and IAM roles with limited access.

Example prefixes:

  • moon-cache/dev/main-repo/
  • moon-cache/staging/main-repo/
  • moon-cache/prod/main-repo/

Your CI role for prod should not have access to dev, and vice versa.

5. Avoid embedding secrets in cache contents

Moon artifacts should not contain credentials or secrets. Double-check:

  • Build output does not package .env files with secrets.
  • Docker images are built separately, not stored raw in cache.
  • Secrets are injected at runtime, not baked into cacheable artifacts.

6. Limit IAM roles to CI only

  • For GitHub Actions, use OIDC roles that are only assumable by your production repo.
  • For self-hosted runners or other CI, ensure the machine role is limited and not reused for unrelated tasks.

Putting it all together

To set up moonrepo remote cache on S3 and keep it safe in CI:

  1. Create a private S3 bucket for the cache (e.g., my-company-moon-cache-prod) with encryption and lifecycle rules.
  2. Define a minimal IAM policy that:
    • Grants ListBucket on the cache prefix.
    • Grants GetObject / PutObject / DeleteObject only on the cache prefix.
  3. Create an IAM role per environment (e.g., moon-cache-ci) and attach the policy.
  4. Use short-lived credentials:
    • OIDC-based role assumption in GitHub Actions or other CI.
    • Local dev via AWS SSO or a separate profile.
  5. Configure moonrepo:
    • Set engine: "s3", bucket, and prefix in .moon/workspace.yml.
    • Use remote.read / remote.write flags and env overrides for CI vs local.
  6. Harden the setup:
    • Keep buckets private.
    • Separate environments via buckets or prefixes.
    • Disable remote cache for untrusted PRs.
    • Avoid secrets in build artifacts.

With this pattern, you get fast, shared builds via moonrepo’s remote cache on S3, while keeping your cloud resources and build artifacts locked down appropriately for CI and production use.