Skip to content
Notifications
Clear all

Showcase: How we automated offboarding to instantly revoke SASE access.

1 Posts
1 Users
0 Reactions
0 Views
(@infra_skeptic_9)
Reputable Member
Joined: 5 months ago
Posts: 155
Topic starter   [#5513]

Another day, another “zero trust” platform promising to revolutionize secure access. Fortinet throws its hat in the ring with FortiSASE, and of course the sales deck is all about seamless onboarding, single pane of glass, and reduced complexity. What they gloss over, as per usual, is the operational tail—specifically, what happens when someone leaves your organization. That “seamless” onboarding often begets a manual, error-prone, and dangerously slow offboarding process.

So when our leadership decided to pilot FortiSASE, my first question wasn't about throughput or cool features. It was, “Show me the exact API call or Terraform resource to nuke a user's access the second HR files the paperwork.” Cue the crickets. The out-of-the-box workflow involved a human logging into the admin portal, which is a non-starter for any team pretending to care about security.

Thus began our little side project: automating instant revocation. The goal was to hook into our existing HR-driven identity deprovisioning system (Okta Workflows, but could be anything) and have it trigger the complete dismantling of a user's SASE footprint. Here’s the grim reality we had to engineer around.

FortiSASE’s API is… Fortinet. It exists, but it feels like an afterthought. For our purposes, we needed to:
1. Terminate any active FortiClient EMS/ZTNA sessions for the user.
2. Remove the user from all FortiSASE access policies (both ZTNA and VPN).
3. Delete the user from FortiSASE (or at a minimum, disable them).

The API documentation is a labyrinth, and the actual endpoints required some serious trial and error. Here’s the core of our revocation script that gets called by our orchestration layer. It's in Python because that's what our ops glue is written in, but it's just HTTP calls at the end of the day.

```python
import requests
import sys

# FortiSASE API base - using their North America instance
BASE_URL = "https://api.sase.fortinet.com"
# You'd want these from a vault, obviously.
TENANT_ID = "your-tenant-id"
CLIENT_ID = "your-service-account-client-id"
CLIENT_SECRET = "your-service-account-secret"

def get_bearer_token():
"""Fetch a short-lived OAuth2 token. Because of course it's another token to manage."""
url = f"{BASE_URL}/iam/v1/oauth/token"
payload = {
'grant_type': 'client_credentials',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
resp = requests.post(url, data=payload)
resp.raise_for_status()
return resp.json()['access_token']

def revoke_user(email):
token = get_bearer_token()
headers = {'Authorization': f'Bearer {token}'}

# 1. Find the user's internal ID
user_list_url = f"{BASE_URL}/iam/v1/tenants/{TENANT_ID}/users"
users = requests.get(user_list_url, headers=headers).json()
user_id = None
for u in users.get('data', []):
if u.get('email', '').lower() == email.lower():
user_id = u['id']
break

if not user_id:
print(f"User {email} not found in FortiSASE.")
return

# 2. Kill active sessions via EMS API (simplified - you may need to iterate)
sessions_url = f"{BASE_URL}/ems/api/v1/tenants/{TENANT_ID}/sessions"
sessions = requests.get(sessions_url, headers=headers).json()
for sess in sessions.get('data', []):
if sess.get('user', {}).get('email', '').lower() == email.lower():
kill_url = f"{sessions_url}/{sess['id']}/terminate"
requests.post(kill_url, headers=headers)

# 3. Remove user from all Access Policies (this is the painful part)
policies_url = f"{BASE_URL}/pm/config/v1/tenants/{TENANT_ID}/access-policies"
policies = requests.get(policies_url, headers=headers).json()
for policy in policies.get('data', []):
# This is a naive approach; you need to patch the policy's member list.
# The actual PATCH body construction is omitted for brevity but involves
# fetching the policy, removing the user from 'members', and updating.
print(f"Would remove user from policy: {policy['name']}")
# Implementation detail: Their PATCH API for policies is JSON Merge Patch.
# It's fragile. Test this extensively.

# 4. Disable/Delete the user
disable_url = f"{BASE_URL}/iam/v1/tenants/{TENANT_ID}/users/{user_id}"
# To disable: PATCH with {"status": "disabled"}
# To delete: DELETE call
requests.patch(disable_url, headers=headers, json={"status": "disabled"})
print(f"Disabled user {email} (ID: {user_id}) in FortiSASE.")

if __name__ == "__main__":
revoke_user(sys.argv[1])
```

The takeaway? The platform is built for the greenfield “add” use case. The “remove” use case is a bolt-on, requiring you to stitch together multiple API domains (IAM, EMS, Policy Management) with inconsistent patterns. The cost of this automation wasn't in the runtime, but in the dozens of hours spent deciphering which of the five different “tenant” IDs to use where and handling the silent failures.

If you're considering FortiSASE, factor in the engineering time to build this kind of lifecycle automation. The manual alternative is a ticking clock between an employee's departure and a potential breach. But hey, at least the dashboard looks shiny.

-- cynical ops


Your k8s cluster is 40% idle.


   
Quote