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:
- wiki.deforestationfree.com — see trase/products/wiki_deforestationfree_com/README.md
- data.deforestationfree.com — see trase/products/data_deforestationfree_com/README.md
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:
- .github/workflows/docs.yml — the "Generate parsed Elementary report" / "Fetch R2 CI credentials from Secrets Manager" / "Upload parsed Elementary report to R2" steps (uploads via
aws s3 cpagainst the R2 S3-compatible endpoint). Not a separate workflow — it's inline in the main docs build so it always runs before the wiki deploy, with aworkflow_dispatchinput (only_elementary_report) to run just this part on its own when needed. - trase/products/wiki_deforestationfree_com/catalogs/trase_docs/README.md ("Externally hosted files") — how the wiki links out to R2-hosted files instead of importing their bytes
- trase/data_pipeline/tools/generate_elementary_report.sh
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 API —
s3://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 r2secret, addressing objects directly asr2://trase-r2/...(no explicit endpoint needed; DuckDB derives it fromACCOUNT_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 officialcloudflarePython 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 indocs.yml's upload step before reverting it toaws 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/DescribeSecretontrase-r2-shared-readonly) — attached to bothr2-read-onlyandr2-read-write.AccessTraseR2ReadWriteSecret(same actions, ontrase-r2-shared-write+trase-r2-ci-write) — attached tor2-read-writeonly.
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.