wif.tax

Modal

D Modal (serverless GPU compute: Functions, Sandboxes, SDK and CLI)

CI jobs and services drive Modal with a static token id/secret pair. No OIDC or GitHub Actions federation exists into Modal. Service users (Team plan) are scoped per Environment, created and deleted through the SDK, and show a last-used time. They never expire unless you set an expiry, and Modal is not a GitHub secret-scanning partner. Outbound, Modal is an OIDC issuer: S3 mounts and ECR pulls are keyless, but GCS, Artifact Registry, Azure and R2 need pasted keys.

Docs: modal.com · Checked 2026-10-22 · source YAML · dispute via PR

Inbound Drequires Team (service users and RBAC)

How a workload running anywhere authenticates to this product.

Static Modal API token (MODAL_TOKEN_ID ak-... plus MODAL_TOKEN_SECRET as-...), owned either by a human member or by a service user, used by the Python/JS/Go SDKs and the modal CLI for every deploy, run and Function call. Third-party apps can instead hold a Modal-issued OAuth client secret plus refresh token. No OIDC, workload identity or token exchange path exists.

Modal's control plane and data plane are one API: the same token id/secret deploys Apps, calls Functions, creates Sandboxes and reads Volumes, Dicts and Queues, so there is no weaker side path to grade separately. The GitHub Actions guide stores MODAL_TOKEN_ID and MODAL_TOKEN_SECRET as repository secrets; nothing in the guide, the config reference or llms-full.txt describes exchanging a GitHub, cloud or other OIDC token for a Modal credential. The OAuth client path (oc-/ov- client id and secret plus a refresh token) is for platforms acting on behalf of Modal users and still rests on a static client secret, so it is C-shaped at best and does not apply to a customer's own workloads. MODAL_IDENTITY_TOKEN is a token Modal issues to its own containers for calling out (see outbound); code inside Modal reaching Modal is ambient identity, which does not earn an A. Proxy tokens (wk-/ws-, Environment-scoped under RBAC) are static keys for callers of a customer's own web endpoints, not for driving Modal. Graded at the service-user mechanism (Team plan); on plans without service users a workload holds a member token that carries that human's full access, which fails scoping and would make this F. Moves to B or A if Modal accepts federated tokens for service users.

Outbound B

How this product reaches into your cloud, repos, or other SaaS.

Modal is an OIDC issuer (https://oidc.modal.com) and injects a short-lived MODAL_IDENTITY_TOKEN into every Function. S3 CloudBucketMounts and ECR image pulls can assume an AWS IAM role through it. GCS mounts, R2 mounts, GCP Artifact Registry, Azure Container Registry, Docker Hub and OpenTelemetry export take static keys stored as Modal Secrets.

Checked: CloudBucketMount (S3, R2, GCS), private registries (ECR, GCP Artifact Registry and Container Registry, Azure Container Registry, Docker Hub), the Datadog and OpenTelemetry log/metric exports, and the OIDC guide. AWS is keyless on both platform paths: you register oidc.modal.com as an IAM OIDC provider and trust a role on the sub claim (workspace, Environment, App or Function), and Modal assumes it for the S3 mount (oidc_auth_role_arn) or the ECR pull (AWS_ROLE_ARN in a Secret). GCP has no platform path: GCS mounts take an HMAC key and Artifact Registry a service-account JSON key, even though the discovery document would let your own Function code federate into GCP or Azure by hand (the guide says so, without a walkthrough). Azure ACR needs long-lived repository tokens, R2 an API token, Docker Hub a read-only PAT. The Datadog integration is an authorize-in-the-browser flow that mints the Datadog API key for you (consent-style, counted as keyless); OpenTelemetry takes a pasted API key header. Hugging Face, W&B and similar tokens in the examples are pasted into Secrets by your own code, not Modal reaching in. Keyless on AWS with static keys elsewhere is B; a GCP federation path for mounts and Artifact Registry would make it A.

Hygiene for any static credential the product still issues

✗ Expiry enforceable
Service user tokens, the credential this card grades, take an optional expiry (--expires-in, expires_in) and default to never; no workspace policy that forces one on them is documented. Member tokens are different: a workspace can set a maximum member API token lifetime, and modal token new defaults to it. That cap is documented only in the CLI help and the audit-log event (it is unset, "no cap", by default) and covers member tokens. Per the precedent that a changeable default is not an enforced expiry, this fails. Whether the cap also binds service users is not publicly verifiable (the settings page is behind a login). It would not change the headline: inbound is D either way.
  • modal.com checked 2026-10-22
    “the newly created Service User will never expire.”
  • modal.com checked 2026-10-22
    “Lifetime of the new token, e.g. 12h, 7d, or 90d. Defaults to the workspace's maximum allowed lifetime.”
  • modal.com checked 2026-10-22
    “A workspace's maximum member API token lifetime was updated. Before/after values in seconds are recorded in the event metadata; an unset value means no cap.”
✓ Rotation via API
modal.Workspace.service_users.create/delete and the matching modal workspace service-users CLI commands need no browser, so a job can mint a new service user, swap it in and delete the old one. A service user holds a single token, so rotating means replacing the service user and re-granting its Environment roles. Member tokens come only from a web session (modal token new) or the dashboard.
  • modal.com checked 2026-10-22
    “Finally, you can also create a new Service User directly from Python:”
  • modal.com checked 2026-10-22
    “Deleting a Service User will immediately revoke the credentials associated with that Service User.”
✓ Scoped keys
Service users are a purpose-built machine principal that starts with No Access and gets Viewer or Contributor on each Environment it needs, which the rubric counts. The roles are coarse (Viewer or Contributor per Environment, no per-App or per-action grants). Member tokens inherit the human's workspace role. Service users and RBAC require the Team plan.
  • modal.com checked 2026-10-22
    “For enhanced security, by default a Service User has no access to any of the Environments in the Workspace.”
  • modal.com checked 2026-10-22
    “Unlike human users, service users do not have a Workspace-level role; their access is controlled entirely through Environment Roles.”
✓ Last-used visible
ws.service_users.list() and .info() return ServiceUserInfo, which carries last_used_at next to created_at and expires_at, so idle service users can be found and deleted from a script. Passed on the SDK type alone; no guide text describes it. A last-used view for member tokens is not documented publicly.
  • modal.com checked 2026-10-22
    “A lightweight dataclass containing metadata about a Modal Service User.”
  • modal.com checked 2026-10-22
    “name="last_used_at"”
  • modal.com checked 2026-10-22
    “A list of `ServiceUserInfo` dataclasses.”
✗ Leak revocation
Modal does not appear in GitHub's supported secret-scanning patterns table, partner or otherwise, even though its tokens carry greppable ak-/as- (and wk-/ws- proxy) prefixes.

Notes

One card for Modal: Functions, Sandboxes, Volumes and the rest sit behind one API and one token type. Three of five hygiene checks pass, all on Team-plan service users. Without the Team plan a workload uses a member token with its owner's full access, which would be F. Fixes, in order: let service users accept a GitHub Actions or any-issuer OIDC token (Modal already runs an OIDC issuer outbound), enforce a maximum lifetime on service users, join GitHub secret scanning, and add GCP federation for GCS mounts and Artifact Registry.

Badge

wif.tax grade D

[![wif.tax grade](https://wif.tax/badge/modal.svg)](https://wif.tax/p/modal/)