Skip to content

View or edit on GitHub

This page is synchronized from doc/Cloudflare.md. Last modified on 2026-09-20 13:50 CEST by Nicolas Martin. Please view or edit the original file there; changes should be reflected here after a midnight build (CET time), or manually triggering it with a GitHub action (link).

Cloudflare

Cloudflare is our domain registrar and is used in a few distinct ways across the repository:

Pages

Two internal sites are hosted as Cloudflare Pages static websites, deployed via cloudflare/wrangler-action in .github/workflows/docs.yml:

Both are protected by Cloudflare Zero Trust access control (Google Workspace OAuth + one-time PIN).

Note that the other static sites in the repo (wtf, what, docs.deforestationfree.com) are not on Cloudflare Pages — they use S3 + Cloudfront instead, see Static-websites-on-AWS-S3.md.

R2 (object storage)

The dbt Elementary data-quality report is uploaded to a Cloudflare R2 bucket (trase-r2 — named for the storage service, distinct from the default trase-storage S3 bucket) rather than bundled into the wiki's Pages deploy, because Cloudflare Pages rejects any single deployed file over 25 MB and the report keeps growing with the test suite. See:

The R2 bucket has its own public custom domain (files.deforestationfree.com — kept generic since the bucket may host other large assets beyond reports), protected by the same Zero Trust policy as the wiki so authenticated users aren't prompted twice.

R2 objects are reachable through more than one API, but which one actually works depends on the token's permission tier — this tripped us up once already (see "Credentials"):

  • S3-compatible APIs3://trase-r2/... via boto3/aws-cli/s3fs, pointed at the R2 endpoint with an explicit --endpoint-url/endpoint_url=. The only one that works for object get/put with our bucket-scoped "Object Read & Write" tokens. Used for all actual object reads/writes in this repo — CI's upload, and pandas/polars/boto3 locally.
  • DuckDB's native R2 support — a TYPE r2 secret, addressing objects directly as r2://trase-r2/... (no explicit endpoint needed; DuckDB derives it from ACCOUNT_ID). Also S3-compatible under the hood, so it works fine with our tokens.
  • Cloudflare's own REST API — what wrangler r2 object ... and the official cloudflare Python package speak. 403s ("Authentication error") for object writes on anything less than an Admin-tier token (https://github.com/cloudflare/workers-sdk/issues/9235) — confirmed live in docs.yml's upload step before reverting it to aws s3 cp. Our tokens are deliberately bucket-scoped, not Admin, so this API is only relevant if a separate Admin-tier token is created later for bucket-level admin (CORS, lifecycle, custom domains) — not for routine object access.

Credentials

Cloudflare has no OIDC/IAM-role federation for R2, so access goes through R2 API tokens, all scoped to just the trase-r2 bucket. Three exist, each vaulted as its own AWS Secrets Manager secret rather than a GitHub secret or anything held locally — rotation is then a single put-secret-value, and a credential never has two places it needs updating. Each secret carries {"access_key_id": "...", "secret_access_key": "...", "api_token": "cfut_..."} — the api_token field is captured for potential future Admin-tier use but isn't usable for object access with these bucket-scoped tokens (see above):

Token (Cloudflare name) Permission Rotation Used by
trase-r2-ci-write Object Read & Write ~yearly docs.yml's upload step — S3 keypair fetched from Secrets Manager at runtime using the job's own AWS credentials, ARN in the (non-secret) repo variable R2_CI_WRITE_SECRET_ARN
trase-r2-shared-read Object Read only ~yearly Local Python scripts, via TRASE_R2__RO_SECRET_ARN
trase-r2-shared-write Object Read & Write ~yearly Local Python scripts doing ad hoc writes, via TRASE_R2__RW_SECRET_ARN

AWS-side access to these secrets is IAM-gated via two custom policies attached to two dedicated groups (kept separate from the existing sei-s3-read-* groups, since who needs R2 access won't generally overlap with who needs default-S3 access):

  • AccessTraseR2ReadOnlySecret (secretsmanager:GetSecretValue/DescribeSecret on trase-r2-shared-read only) — attached to both r2-read-only and r2-read-write.
  • AccessTraseR2ReadWriteSecret (same actions, on trase-r2-shared-write + trase-r2-ci-write) — attached to r2-read-write only.

r2-read-write gets both policies (not just the write one) because trase.tools.r2.connection's default role is "ro" — without read access too, that default would fail for anyone in the write group. The GitHub (this repo's CI identity) and jupyter IAM users are members of r2-read-write, among others — that's what gives docs.yml access to ci-write.

For reads by humans (not scripts), no R2 credential is needed at all — browsing files.deforestationfree.com is already authenticated by the Zero Trust policy above, using the same @trase.earth Google identity as the wiki.

For Python, use trase/tools/r2/connection.py rather than reading these secrets directly — it fetches and caches credentials the same way trase/tools/ducklake/connection.py does for the DuckLake Postgres password (RO cached on disk for 12h under tight permissions; RW never cached). Use storage_options() / attach_duckdb_secret() / client() for actual object access; api_token() / wrangler_env() / cloudflare_client() exist but only work if the resolved token happens to be Admin-tier.

Tunnel

apps.deforestationfree.com (a Streamlit app) is exposed via a Cloudflare Tunnel rather than a public ingress — cloudflared runs as a connector on the hosting machine and is also secured by Cloudflare Access. See trase/products/apps_deforestationfree_com/README.md.

DNS

Cloudflare is our domain registrar, used for CNAME records (e.g. validating AWS ACM certificates, pointing subdomains at Cloudfront distributions) — see Static-websites-on-AWS-S3.md.

Secrets

Repo secrets used across the above: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID. The R2 API tokens are deliberately not GitHub secrets — see "Credentials" above.