We all know MFA is non-negotiable for human users. But what about service accounts used by CI/CD pipelines, automation, or internal services hitting your APIs behind Cloudflare Access? Forcing MFA on them breaks everything.
Here's how to set up a policy that allows specific service accounts to bypass MFA, while keeping it locked down for everyone else. This uses Access Groups and Service Auth.
**First, create a dedicated Service Auth application in Zero Trust.**
1. In Zero Trust, go to **Access > Applications**.
2. Click **Add an application** and select **Service Auth**.
3. Give it a clear name (e.g., "Pipeline Service Accounts").
4. Note the generated **Client ID** and **Client Secret**. You'll need these for your service to authenticate.
**Next, build an Access Group for the exception.**
You need a group that includes these service tokens. Go to **Access > Access Groups > Create Group**.
* **Group Name:** `bypass-mfa-service-accounts`
* **Include:** Set the rule to `Service Token` and select the Service Auth application you just created.
```sql
(any(service_token.id[*]) in {""})
```
**Finally, apply it in your Application's policy.**
Edit the policy for the application your service needs to reach. You'll have two rules:
1. **For the service accounts:** Use the `bypass-mfa-service-accounts` group as a **Require** rule, with no additional MFA requirement.
2. **For everyone else (humans):** Add a second **Require** rule for your usual user group (e.g., `your-team@domain.com`) and set **MFA** to `Required`.
The policy evaluator processes these in order. The service account matches the first rule and gets in without MFA. Human users skip the first rule (they're not a service token) and hit the second rule, which demands MFA.
**Key Pitfall:** This only works if the service account is **ONLY** used for machine-to-machine. Never use those credentials in a browser session. The security model relies on the secrecy of the Client Secret. Rotate it like any other credential.
Integration is not a project, it's a lifestyle.
That's a solid foundation for setting up the exception. The critical piece many teams miss is the policy logic on the application itself.
You've created the group `bypass-mfa-service-accounts`. When you edit the Access policy for the application, you need to add a *second* rule. The first rule likely requires MFA for everyone. You then add a rule above it with the action `Allow` and a condition like `Access Group > `bypass-mfa-service-accounts`. Crucially, you must set `Require` to "No MFA" for that specific rule.
Otherwise, you've just allowed them in but they'll still hit the MFA prompt. The order of rules matters here, as the first matching rule applies.
Good catch on the rule order, that's a classic setup trap. Just to add, you should also watch out for any global "Require MFA" policies in the organization's login methods. Those can sometimes override the app-specific rule, so it's worth a quick check in the authentication settings to make sure the exception path is clear.
automate everything
Thanks for laying this out, that's really clear. Quick question on the Service Auth app creation - what happens if you have multiple CI/CD systems? Would you create one app per system and add all their tokens to the same group, or is it better to have separate groups? Just thinking about audit trails later.
Still learning
Great question on audit trails. I'd lean towards separate Service Auth apps per major system (Jenkins, GitHub Actions, etc.), but still use that single, locked-down `bypass-mfa-service-accounts` group for the MFA rule.
That way, if a token from `ci-jenkins-prod` shows up in logs, you know exactly which pipeline it was. You can still manage the access policy in one place via the group. It's a bit more setup upfront, but makes tracing way cleaner if you ever need to rotate or revoke.
Prompt engineering is the new debugging
Great start, user278. To finish that thought on the application policy: you'll edit your main app's policy, add a new rule at the *top* with the action set to "Allow," and set the requirement to "No MFA." Then, choose the `bypass-mfa-service-accounts` group in the "Include" condition. The existing rule for "Everyone" with MFA required stays below it.
One practical tip: treat the Client ID and Secret like you would any other secret - inject them via environment variables or a secrets manager in your pipeline. Hardcoding them is a common slip that undermines the whole setup.
Integrate or die
Solid steps. But you're forgetting the cost hit.
That's not a "Service Auth application" - it's a *Service Token application*. Every service token created under it is a billable seat. Those "non-human user" licenses add up fast and they don't advertise that until the invoice lands. Check your Zero Trust plan's quota.
Read the contract
You've laid out the core flow nicely. One thing I'd add to your procedure is to create a separate Access Group per environment (dev, staging, prod) rather than one monolithic `bypass-mfa-service-accounts` group. That way, if a token from `ci-jenkins-dev` somehow gets reused in a production pipeline, the policy won't match and the request will fail at the MFA step. It's a small extra group per environment, but it gives you a tighter blast radius.
I also like to keep a spreadsheet (or a YAML file in a repo) that maps each Service Token's Client ID to the pipeline name, responsible team, and rotation date. The Access Group selector in Cloudflare only shows the token name, not the ID, so having that cross-reference saves time when you're auditing logs or rotating a compromised token.
Do you typically use a single Client Secret per pipeline, or do you rotate secrets on a schedule and update the application each time?
Method over hype