mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
870 lines
28 KiB
Markdown
870 lines
28 KiB
Markdown
---
|
|||
|
|
name: identity-access-management
|
||
|
|
description: Set up and manage SSO, SCIM provisioning, and MFA for startup teams using Google Workspace, Okta, or Azure AD. Use when centralizing authentication, onboarding SSO, or meeting compliance requirements.
|
||
|
|
license: MIT
|
||
|
|
metadata:
|
||
|
|
author: devops-skills
|
||
|
|
version: "1.0"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Identity & Access Management for Startups
|
||
|
|
|
||
|
|
Centralized identity management is not optional once your team exceeds a handful of people. This skill covers practical, production-ready configurations for SSO, SCIM provisioning, MFA enforcement, and access governance using the three most common identity providers for startups: Google Workspace, Okta, and Azure AD (Entra ID).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. When to Use This Skill
|
||
|
|
|
||
|
|
Reach for this skill when:
|
||
|
|
|
||
|
|
- **First SSO setup** -- You are moving from individual app logins to centralized authentication.
|
||
|
|
- **Compliance audit preparation** -- SOC 2, ISO 27001, or HIPAA requires documented access controls, MFA enforcement, and audit logs.
|
||
|
|
- **Team growth inflection** -- You are crossing 15-20 employees and manual onboarding/offboarding is becoming error-prone.
|
||
|
|
- **Vendor security questionnaires** -- Customers are asking about your identity posture and you need to demonstrate controls.
|
||
|
|
- **Incident response** -- You need to revoke access quickly across all systems for a departing or compromised user.
|
||
|
|
|
||
|
|
Signs you are overdue:
|
||
|
|
|
||
|
|
- Shared passwords in a spreadsheet or chat channel.
|
||
|
|
- No central audit log of who accessed what and when.
|
||
|
|
- Offboarding takes more than one business day.
|
||
|
|
- Developers have standing admin access to production.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Google Workspace as Identity Provider
|
||
|
|
|
||
|
|
Google Workspace is the most common starting IdP for startups. Combined with the GAM CLI tool, it provides powerful automation.
|
||
|
|
|
||
|
|
### Install GAM (Google Apps Manager)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Install GAM on Linux/macOS
|
||
|
|
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install)
|
||
|
|
|
||
|
|
# Authorize GAM with your Workspace domain
|
||
|
|
gam oauth create
|
||
|
|
|
||
|
|
# Verify connection
|
||
|
|
gam info domain
|
||
|
|
```
|
||
|
|
|
||
|
|
### Create Organizational Units
|
||
|
|
|
||
|
|
Organizational units (OUs) control policy inheritance and app access.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create OUs for team structure
|
||
|
|
gam create org "Engineering"
|
||
|
|
gam create org "Engineering/Backend"
|
||
|
|
gam create org "Engineering/Frontend"
|
||
|
|
gam create org "Operations"
|
||
|
|
gam create org "Operations/IT"
|
||
|
|
gam create org "Finance"
|
||
|
|
gam create org "Contractors"
|
||
|
|
|
||
|
|
# Move a user into an OU
|
||
|
|
gam update user alice@company.com org "Engineering/Backend"
|
||
|
|
|
||
|
|
# List all OUs
|
||
|
|
gam print orgs
|
||
|
|
```
|
||
|
|
|
||
|
|
### Configure a SAML App in Google Workspace
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Export the Google IdP metadata (download from Admin Console or use GAM)
|
||
|
|
# Admin Console: Apps > Web and mobile apps > Add app > Search for app > Download IdP metadata
|
||
|
|
|
||
|
|
# For a custom SAML app, you need:
|
||
|
|
# 1. ACS URL (from the service provider)
|
||
|
|
# 2. Entity ID (from the service provider)
|
||
|
|
# 3. Name ID format (usually EMAIL)
|
||
|
|
|
||
|
|
# Example: Add a custom SAML app via Admin Console API
|
||
|
|
gam create samlapp "Internal Dashboard" \
|
||
|
|
acs_url "https://dashboard.company.com/saml/acs" \
|
||
|
|
entity_id "https://dashboard.company.com" \
|
||
|
|
name_id_format "EMAIL" \
|
||
|
|
name_id "user.primaryEmail"
|
||
|
|
|
||
|
|
# Assign the app to an OU
|
||
|
|
gam update samlapp "Internal Dashboard" org "Engineering" enabled on
|
||
|
|
|
||
|
|
# Verify SAML app status
|
||
|
|
gam print samlappinfo "Internal Dashboard"
|
||
|
|
```
|
||
|
|
|
||
|
|
### SCIM Provisioning with Google Workspace
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Enable auto-provisioning for supported apps
|
||
|
|
# Google Workspace supports automatic user provisioning for apps like:
|
||
|
|
# Slack, Zoom, Box, Dropbox, Asana, GitHub Enterprise
|
||
|
|
|
||
|
|
# List provisioned apps
|
||
|
|
gam print tokens
|
||
|
|
|
||
|
|
# Force sync provisioning for an app
|
||
|
|
gam sync samlapp "Slack" users
|
||
|
|
|
||
|
|
# Bulk create users from CSV
|
||
|
|
# users.csv format: firstname,lastname,email,org,password
|
||
|
|
gam csv users.csv gam create user ~email \
|
||
|
|
firstname ~firstname lastname ~lastname \
|
||
|
|
password ~password org ~org \
|
||
|
|
changepassword on
|
||
|
|
```
|
||
|
|
|
||
|
|
### Enforce MFA at the Workspace Level
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Enforce 2-step verification for the entire domain
|
||
|
|
gam update org "/" 2sv enforced
|
||
|
|
|
||
|
|
# Enforce 2SV for a specific OU
|
||
|
|
gam update org "Engineering" 2sv enforced
|
||
|
|
|
||
|
|
# Set enforcement date (give users time to enroll)
|
||
|
|
gam update org "/" 2sv enforced enforceddate 2026-04-15
|
||
|
|
|
||
|
|
# Check 2SV enrollment status for all users
|
||
|
|
gam print users fields isEnforcedIn2Sv,isEnrolledIn2Sv
|
||
|
|
|
||
|
|
# Find users who have NOT enrolled in 2SV
|
||
|
|
gam print users query "isEnrolledIn2Sv=false" fields primaryEmail,name
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Okta Setup
|
||
|
|
|
||
|
|
Okta offers a free tier for startups (Okta for Startups program -- up to 100 users) making it an excellent choice for teams that need a dedicated IdP.
|
||
|
|
|
||
|
|
### Initial Okta Configuration via API
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Set your Okta domain and API token
|
||
|
|
export OKTA_ORG_URL="https://company.okta.com"
|
||
|
|
export OKTA_API_TOKEN="your-api-token"
|
||
|
|
|
||
|
|
# Verify connectivity
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/org" | jq '.companyName'
|
||
|
|
|
||
|
|
# Create a user
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users?activate=true" \
|
||
|
|
-d '{
|
||
|
|
"profile": {
|
||
|
|
"firstName": "Alice",
|
||
|
|
"lastName": "Engineer",
|
||
|
|
"email": "alice@company.com",
|
||
|
|
"login": "alice@company.com"
|
||
|
|
},
|
||
|
|
"credentials": {
|
||
|
|
"password": { "value": "TempP@ss123!" }
|
||
|
|
}
|
||
|
|
}' | jq '.id'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Create Groups for RBAC
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create groups
|
||
|
|
for group in "Engineering" "Operations" "Finance" "Contractors" "AdminAccess"; do
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/groups" \
|
||
|
|
-d "{\"profile\": {\"name\": \"${group}\", \"description\": \"${group} team group\"}}" \
|
||
|
|
| jq '{id: .id, name: .profile.name}'
|
||
|
|
done
|
||
|
|
|
||
|
|
# Add user to group
|
||
|
|
USER_ID="00u1abc123"
|
||
|
|
GROUP_ID="00g1def456"
|
||
|
|
curl -s -X PUT \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/groups/${GROUP_ID}/users/${USER_ID}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Add a SAML Application in Okta
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create a SAML 2.0 application
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/apps" \
|
||
|
|
-d '{
|
||
|
|
"name": "custom_saml_app",
|
||
|
|
"label": "Internal Dashboard",
|
||
|
|
"signOnMode": "SAML_2_0",
|
||
|
|
"settings": {
|
||
|
|
"signOn": {
|
||
|
|
"defaultRelayState": "",
|
||
|
|
"ssoAcsUrl": "https://dashboard.company.com/saml/acs",
|
||
|
|
"audience": "https://dashboard.company.com",
|
||
|
|
"recipient": "https://dashboard.company.com/saml/acs",
|
||
|
|
"destination": "https://dashboard.company.com/saml/acs",
|
||
|
|
"subjectNameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||
|
|
"attributeStatements": [
|
||
|
|
{
|
||
|
|
"type": "EXPRESSION",
|
||
|
|
"name": "email",
|
||
|
|
"namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
||
|
|
"values": ["user.email"]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"type": "EXPRESSION",
|
||
|
|
"name": "groups",
|
||
|
|
"namespace": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
||
|
|
"values": ["getFilteredGroups({\"00g1def456\"}, \"group.name\", 50)"]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}' | jq '{id: .id, label: .label, status: .status}'
|
||
|
|
|
||
|
|
# Assign group to application
|
||
|
|
APP_ID="0oa1xyz789"
|
||
|
|
curl -s -X PUT \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/apps/${APP_ID}/groups/${GROUP_ID}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Okta MFA Policy
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create an MFA enrollment policy requiring WebAuthn + TOTP
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/policies" \
|
||
|
|
-d '{
|
||
|
|
"type": "MFA_ENROLL",
|
||
|
|
"name": "Require Strong MFA",
|
||
|
|
"status": "ACTIVE",
|
||
|
|
"settings": {
|
||
|
|
"factors": {
|
||
|
|
"webauthn": { "enroll": { "self": "REQUIRED" } },
|
||
|
|
"google_otp": { "enroll": { "self": "OPTIONAL" } },
|
||
|
|
"okta_email": { "enroll": { "self": "NOT_ALLOWED" } },
|
||
|
|
"okta_sms": { "enroll": { "self": "NOT_ALLOWED" } }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}' | jq '{id: .id, name: .name, status: .status}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Azure AD / Entra ID
|
||
|
|
|
||
|
|
Azure AD (now Microsoft Entra ID) is common at startups using Microsoft 365 or Azure cloud.
|
||
|
|
|
||
|
|
### Azure CLI Setup
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Install Azure CLI and sign in
|
||
|
|
az login
|
||
|
|
|
||
|
|
# Set the default tenant
|
||
|
|
az account set --subscription "your-subscription-id"
|
||
|
|
|
||
|
|
# Verify tenant
|
||
|
|
az ad signed-in-user show --query '{name:displayName, email:userPrincipalName}'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Create Users and Groups
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create a user
|
||
|
|
az ad user create \
|
||
|
|
--display-name "Alice Engineer" \
|
||
|
|
--user-principal-name "alice@company.onmicrosoft.com" \
|
||
|
|
--password "TempP@ss123!" \
|
||
|
|
--force-change-password-next-sign-in true
|
||
|
|
|
||
|
|
# Create security groups
|
||
|
|
for group in "SG-Engineering" "SG-Operations" "SG-Finance" "SG-Admins"; do
|
||
|
|
az ad group create --display-name "$group" --mail-nickname "$group"
|
||
|
|
done
|
||
|
|
|
||
|
|
# Add user to group
|
||
|
|
USER_OID=$(az ad user show --id "alice@company.onmicrosoft.com" --query id -o tsv)
|
||
|
|
GROUP_OID=$(az ad group show --group "SG-Engineering" --query id -o tsv)
|
||
|
|
az ad group member add --group "$GROUP_OID" --member-id "$USER_OID"
|
||
|
|
|
||
|
|
# List group members
|
||
|
|
az ad group member list --group "SG-Engineering" --query '[].{name:displayName, email:userPrincipalName}' -o table
|
||
|
|
```
|
||
|
|
|
||
|
|
### Conditional Access Policies via Graph API
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Require MFA for all users accessing cloud apps
|
||
|
|
# Uses Microsoft Graph API
|
||
|
|
ACCESS_TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)
|
||
|
|
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
|
||
|
|
-d '{
|
||
|
|
"displayName": "Require MFA for all users",
|
||
|
|
"state": "enabledForReportingButNotEnforced",
|
||
|
|
"conditions": {
|
||
|
|
"users": {
|
||
|
|
"includeUsers": ["All"],
|
||
|
|
"excludeGroups": ["'${BREAKGLASS_GROUP_OID}'"]
|
||
|
|
},
|
||
|
|
"applications": {
|
||
|
|
"includeApplications": ["All"]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"grantControls": {
|
||
|
|
"operator": "OR",
|
||
|
|
"builtInControls": ["mfa"]
|
||
|
|
}
|
||
|
|
}'
|
||
|
|
|
||
|
|
# Block legacy authentication (critical for security)
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
|
||
|
|
-d '{
|
||
|
|
"displayName": "Block legacy authentication",
|
||
|
|
"state": "enabled",
|
||
|
|
"conditions": {
|
||
|
|
"users": { "includeUsers": ["All"] },
|
||
|
|
"applications": { "includeApplications": ["All"] },
|
||
|
|
"clientAppTypes": ["exchangeActiveSync", "other"]
|
||
|
|
},
|
||
|
|
"grantControls": {
|
||
|
|
"operator": "OR",
|
||
|
|
"builtInControls": ["block"]
|
||
|
|
}
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. SSO Integration Patterns
|
||
|
|
|
||
|
|
### SAML vs OIDC Decision Guide
|
||
|
|
|
||
|
|
| Factor | SAML 2.0 | OIDC / OAuth 2.0 |
|
||
|
|
|---|---|---|
|
||
|
|
| Best for | Enterprise SaaS apps | SPAs, mobile apps, APIs |
|
||
|
|
| Token format | XML assertions | JWT tokens |
|
||
|
|
| Setup complexity | Higher (certificates, metadata XML) | Lower (client ID + secret) |
|
||
|
|
| Logout | Inconsistent (SLO is poorly supported) | Token expiry + revocation |
|
||
|
|
| Use when | App only supports SAML | You have a choice, or need API auth |
|
||
|
|
|
||
|
|
**Rule of thumb**: If the SaaS vendor supports OIDC, prefer it. If they only support SAML, use SAML. Never use LDAP-over-internet.
|
||
|
|
|
||
|
|
### Integrating Common SaaS Apps
|
||
|
|
|
||
|
|
#### Slack Enterprise SSO
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Okta OIDC integration for Slack
|
||
|
|
# 1. In Okta: Applications > Browse App Catalog > Slack
|
||
|
|
# 2. Configure with your Slack workspace URL
|
||
|
|
# 3. Enable SCIM provisioning
|
||
|
|
|
||
|
|
# Verify Slack SCIM connection
|
||
|
|
curl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
|
||
|
|
"https://api.slack.com/scim/v2/Users?count=5" | jq '.Resources[].userName'
|
||
|
|
```
|
||
|
|
|
||
|
|
#### GitHub Organization SSO
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Configure SAML for GitHub Org (requires GitHub Enterprise Cloud)
|
||
|
|
# 1. GitHub Org Settings > Authentication security > Enable SAML
|
||
|
|
# 2. Provide IdP SSO URL, IdP issuer, public certificate from your IdP
|
||
|
|
|
||
|
|
# Use GitHub CLI to verify SSO status
|
||
|
|
gh api orgs/company/credential-authorizations --paginate \
|
||
|
|
| jq '.[] | {login: .login, credential_type: .credential_type, authorized_at: .authorized_credential_note}'
|
||
|
|
|
||
|
|
# Require SAML SSO for all org members
|
||
|
|
gh api -X PATCH orgs/company \
|
||
|
|
-f saml_enforced=true
|
||
|
|
```
|
||
|
|
|
||
|
|
#### AWS SSO (IAM Identity Center)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Configure AWS IAM Identity Center with external IdP
|
||
|
|
aws sso-admin list-instances --query 'Instances[0].InstanceArn' --output text
|
||
|
|
|
||
|
|
INSTANCE_ARN="arn:aws:sso:::instance/ssoins-1234567890"
|
||
|
|
IDENTITY_STORE_ID="d-1234567890"
|
||
|
|
|
||
|
|
# Create a permission set
|
||
|
|
aws sso-admin create-permission-set \
|
||
|
|
--instance-arn "$INSTANCE_ARN" \
|
||
|
|
--name "DeveloperAccess" \
|
||
|
|
--description "Read-only + deploy access for engineers" \
|
||
|
|
--session-duration "PT8H"
|
||
|
|
|
||
|
|
# Attach AWS managed policy to permission set
|
||
|
|
PERMISSION_SET_ARN="arn:aws:sso:::permissionSet/ssoins-1234567890/ps-abc123"
|
||
|
|
aws sso-admin attach-managed-policy-to-permission-set \
|
||
|
|
--instance-arn "$INSTANCE_ARN" \
|
||
|
|
--permission-set-arn "$PERMISSION_SET_ARN" \
|
||
|
|
--managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"
|
||
|
|
|
||
|
|
# Assign group to AWS account with permission set
|
||
|
|
aws sso-admin create-account-assignment \
|
||
|
|
--instance-arn "$INSTANCE_ARN" \
|
||
|
|
--target-id "123456789012" \
|
||
|
|
--target-type AWS_ACCOUNT \
|
||
|
|
--permission-set-arn "$PERMISSION_SET_ARN" \
|
||
|
|
--principal-type GROUP \
|
||
|
|
--principal-id "a1b2c3d4-5678-90ab-cdef-GROUP001"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. SCIM Provisioning
|
||
|
|
|
||
|
|
SCIM (System for Cross-domain Identity Management) automates user lifecycle across SaaS apps.
|
||
|
|
|
||
|
|
### SCIM API Examples
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Standard SCIM 2.0 endpoints (most IdPs and SaaS apps follow this)
|
||
|
|
SCIM_BASE="https://app.example.com/scim/v2"
|
||
|
|
SCIM_TOKEN="your-scim-bearer-token"
|
||
|
|
|
||
|
|
# List users
|
||
|
|
curl -s -H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
"${SCIM_BASE}/Users?count=10&startIndex=1" | jq '.Resources[] | {id, userName, active}'
|
||
|
|
|
||
|
|
# Create a user via SCIM
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
-H "Content-Type: application/scim+json" \
|
||
|
|
"${SCIM_BASE}/Users" \
|
||
|
|
-d '{
|
||
|
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||
|
|
"userName": "alice@company.com",
|
||
|
|
"name": { "givenName": "Alice", "familyName": "Engineer" },
|
||
|
|
"emails": [{ "primary": true, "value": "alice@company.com", "type": "work" }],
|
||
|
|
"active": true,
|
||
|
|
"groups": []
|
||
|
|
}' | jq '{id, userName, active}'
|
||
|
|
|
||
|
|
# Deactivate a user via SCIM (PATCH is the standard for partial updates)
|
||
|
|
USER_SCIM_ID="abc-123-def"
|
||
|
|
curl -s -X PATCH \
|
||
|
|
-H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
-H "Content-Type: application/scim+json" \
|
||
|
|
"${SCIM_BASE}/Users/${USER_SCIM_ID}" \
|
||
|
|
-d '{
|
||
|
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||
|
|
"Operations": [{ "op": "replace", "value": { "active": false } }]
|
||
|
|
}' | jq '{id, userName, active}'
|
||
|
|
|
||
|
|
# Delete a user permanently via SCIM
|
||
|
|
curl -s -X DELETE \
|
||
|
|
-H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
"${SCIM_BASE}/Users/${USER_SCIM_ID}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### SCIM Group Management
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create a group via SCIM
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
-H "Content-Type: application/scim+json" \
|
||
|
|
"${SCIM_BASE}/Groups" \
|
||
|
|
-d '{
|
||
|
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||
|
|
"displayName": "Engineering",
|
||
|
|
"members": [
|
||
|
|
{ "value": "user-id-001", "display": "alice@company.com" },
|
||
|
|
{ "value": "user-id-002", "display": "bob@company.com" }
|
||
|
|
]
|
||
|
|
}' | jq '{id, displayName}'
|
||
|
|
|
||
|
|
# Add a member to an existing group
|
||
|
|
GROUP_SCIM_ID="grp-456"
|
||
|
|
curl -s -X PATCH \
|
||
|
|
-H "Authorization: Bearer ${SCIM_TOKEN}" \
|
||
|
|
-H "Content-Type: application/scim+json" \
|
||
|
|
"${SCIM_BASE}/Groups/${GROUP_SCIM_ID}" \
|
||
|
|
-d '{
|
||
|
|
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||
|
|
"Operations": [{
|
||
|
|
"op": "add",
|
||
|
|
"path": "members",
|
||
|
|
"value": [{ "value": "user-id-003" }]
|
||
|
|
}]
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. MFA Enforcement
|
||
|
|
|
||
|
|
### WebAuthn / Passkeys (Strongest)
|
||
|
|
|
||
|
|
WebAuthn (FIDO2) hardware keys and passkeys are phishing-resistant and should be the primary MFA factor.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Okta: Enforce WebAuthn as primary factor
|
||
|
|
curl -s -X PUT \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/org/factors/webauthn" \
|
||
|
|
-d '{ "status": "ACTIVE" }'
|
||
|
|
|
||
|
|
# Google Workspace: Enforce security keys only (disable SMS/voice)
|
||
|
|
gam update org "/" 2sv enforced allowedmethods security_key
|
||
|
|
|
||
|
|
# Azure AD: Require phishing-resistant MFA via conditional access
|
||
|
|
# (use the Graph API conditional access endpoint with authenticationStrengths)
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
|
||
|
|
-d '{
|
||
|
|
"displayName": "Require phishing-resistant MFA for admins",
|
||
|
|
"state": "enabled",
|
||
|
|
"conditions": {
|
||
|
|
"users": { "includeRoles": ["62e90394-69f5-4237-9190-012177145e10"] },
|
||
|
|
"applications": { "includeApplications": ["All"] }
|
||
|
|
},
|
||
|
|
"grantControls": {
|
||
|
|
"operator": "OR",
|
||
|
|
"authenticationStrength": {
|
||
|
|
"id": "00000000-0000-0000-0000-000000000004"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
### TOTP Backup Configuration
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Generate backup codes for users (Okta)
|
||
|
|
USER_ID="00u1abc123"
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" \
|
||
|
|
-d '{
|
||
|
|
"factorType": "token:software:totp",
|
||
|
|
"provider": "GOOGLE"
|
||
|
|
}' | jq '{id: .id, status: .status}'
|
||
|
|
```
|
||
|
|
|
||
|
|
### MFA Bypass Procedure (Emergency)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Okta: Reset MFA for a locked-out user
|
||
|
|
USER_ID="00u1abc123"
|
||
|
|
# List enrolled factors
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors" | jq '.[].factorType'
|
||
|
|
|
||
|
|
# Delete a specific factor to allow re-enrollment
|
||
|
|
FACTOR_ID="fct1abc123"
|
||
|
|
curl -s -X DELETE \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}"
|
||
|
|
|
||
|
|
# Google Workspace: Generate backup verification codes
|
||
|
|
gam user alice@company.com update backupcodes
|
||
|
|
|
||
|
|
# Azure AD: Require re-registration of MFA methods
|
||
|
|
az rest --method DELETE \
|
||
|
|
--url "https://graph.microsoft.com/v1.0/users/${USER_OID}/authentication/phoneMethods/3179e48a-750b-4051-897c-87b9720928f7"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. Role-Based Access Control
|
||
|
|
|
||
|
|
### Group-Based Access Patterns
|
||
|
|
|
||
|
|
Map every application permission to a group, never to an individual user.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Naming convention: APP-ROLE
|
||
|
|
# Examples:
|
||
|
|
# aws-developer -> AWS ReadOnly + deploy
|
||
|
|
# aws-admin -> AWS AdministratorAccess
|
||
|
|
# github-engineer -> GitHub write access
|
||
|
|
# github-admin -> GitHub admin access
|
||
|
|
# slack-member -> Slack standard member
|
||
|
|
# pagerduty-oncall -> PagerDuty responder role
|
||
|
|
|
||
|
|
# Okta: Create group rules for automatic assignment based on department
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/groups/rules" \
|
||
|
|
-d '{
|
||
|
|
"type": "group_rule",
|
||
|
|
"name": "Auto-assign engineers to GitHub",
|
||
|
|
"conditions": {
|
||
|
|
"expression": {
|
||
|
|
"value": "user.department == \"Engineering\"",
|
||
|
|
"type": "urn:okta:expression:1.0"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"actions": {
|
||
|
|
"assignUserToGroups": { "groupIds": ["GITHUB_ENGINEERS_GROUP_ID"] }
|
||
|
|
}
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Just-in-Time (JIT) Access
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# AWS: Grant temporary elevated access using STS assume-role
|
||
|
|
# The user assumes a role that expires after a set duration
|
||
|
|
aws sts assume-role \
|
||
|
|
--role-arn "arn:aws:iam::123456789012:role/EmergencyAdmin" \
|
||
|
|
--role-session-name "alice-incident-2026-03-24" \
|
||
|
|
--duration-seconds 3600 \
|
||
|
|
| jq '{AccessKeyId: .Credentials.AccessKeyId, Expiration: .Credentials.Expiration}'
|
||
|
|
|
||
|
|
# Okta: Create a time-limited group membership (via API scheduled task)
|
||
|
|
# Add user to admin group
|
||
|
|
curl -s -X PUT \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}"
|
||
|
|
|
||
|
|
# Schedule removal after 4 hours (use a cron job or automation tool)
|
||
|
|
echo "0 */4 * * * curl -s -X DELETE -H 'Authorization: SSWS ${OKTA_API_TOKEN}' \
|
||
|
|
'${OKTA_ORG_URL}/api/v1/groups/${ADMIN_GROUP_ID}/users/${USER_ID}'" | crontab -
|
||
|
|
```
|
||
|
|
|
||
|
|
### Break-Glass Accounts
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create break-glass accounts that bypass SSO/MFA for emergency access
|
||
|
|
# These accounts must be:
|
||
|
|
# 1. Excluded from conditional access / MFA policies
|
||
|
|
# 2. Protected with extremely long passwords stored in a physical safe
|
||
|
|
# 3. Monitored with alerts on any usage
|
||
|
|
|
||
|
|
# Azure AD: Create break-glass account
|
||
|
|
az ad user create \
|
||
|
|
--display-name "Break Glass 1" \
|
||
|
|
--user-principal-name "breakglass1@company.onmicrosoft.com" \
|
||
|
|
--password "$(openssl rand -base64 48)" \
|
||
|
|
--force-change-password-next-sign-in false
|
||
|
|
|
||
|
|
# Assign Global Administrator role
|
||
|
|
az ad group member add --group "SG-BreakGlass" --member-id "$BREAKGLASS_OID"
|
||
|
|
|
||
|
|
# Set up alert on break-glass sign-in (Azure Monitor)
|
||
|
|
az monitor activity-log alert create \
|
||
|
|
--name "BreakGlass-SignIn-Alert" \
|
||
|
|
--resource-group "security-rg" \
|
||
|
|
--condition category=Administrative and caller=breakglass1@company.onmicrosoft.com \
|
||
|
|
--action-group "/subscriptions/SUB_ID/resourceGroups/security-rg/providers/microsoft.insights/actionGroups/SecurityTeam"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Audit & Compliance
|
||
|
|
|
||
|
|
### Login Audit Logs
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Google Workspace: Pull login audit logs
|
||
|
|
gam report login user all start "2026-03-01" end "2026-03-24" \
|
||
|
|
fields "actorEmail,ipAddress,loginType,isSecondFactor,isSuspicious"
|
||
|
|
|
||
|
|
# Okta: Query system log for authentication events
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/logs?filter=eventType+eq+\"user.session.start\"&since=2026-03-01T00:00:00Z&limit=100" \
|
||
|
|
| jq '.[] | {actor: .actor.displayName, time: .published, outcome: .outcome.result, ip: .client.ipAddress}'
|
||
|
|
|
||
|
|
# Azure AD: Pull sign-in logs via Graph API
|
||
|
|
curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||
|
|
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=createdDateTime ge 2026-03-01T00:00:00Z&\$top=50" \
|
||
|
|
| jq '.value[] | {user: .userDisplayName, app: .appDisplayName, status: .status.errorCode, ip: .ipAddress, mfa: .mfaDetail}'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Access Reviews
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# List all users and their group memberships for quarterly access review
|
||
|
|
# Google Workspace
|
||
|
|
gam print group-members fields email,role > /tmp/access-review-groups.csv
|
||
|
|
|
||
|
|
# Okta: Export all users with their app assignments
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users?limit=200" \
|
||
|
|
| jq -r '.[] | [.profile.email, .status, .lastLogin] | @csv' > /tmp/okta-users.csv
|
||
|
|
|
||
|
|
# For each user, list their app assignments
|
||
|
|
while IFS= read -r user_id; do
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${user_id}/appLinks" \
|
||
|
|
| jq -r '.[] | [.label, .linkUrl] | @csv'
|
||
|
|
done < /tmp/okta-user-ids.txt > /tmp/okta-access-review.csv
|
||
|
|
|
||
|
|
# Azure AD: List role assignments
|
||
|
|
az role assignment list --all --query '[].{principal:principalName, role:roleDefinitionName, scope:scope}' -o table
|
||
|
|
```
|
||
|
|
|
||
|
|
### Compliance Reporting
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Count of users with/without MFA enrolled
|
||
|
|
# Google Workspace
|
||
|
|
echo "=== MFA Enrollment Report ==="
|
||
|
|
echo "Enrolled:"
|
||
|
|
gam print users fields isEnrolledIn2Sv | grep -c True
|
||
|
|
echo "Not enrolled:"
|
||
|
|
gam print users fields isEnrolledIn2Sv | grep -c False
|
||
|
|
|
||
|
|
# Okta: Users without any MFA factor
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users?filter=status+eq+\"ACTIVE\"&limit=200" \
|
||
|
|
| jq '[.[] | select(.credentials.provider.type != "SOCIAL") | .id] | length'
|
||
|
|
|
||
|
|
# Check for stale accounts (no login in 90 days)
|
||
|
|
NINETY_DAYS_AGO=$(date -d "-90 days" +%Y-%m-%dT00:00:00Z 2>/dev/null || date -v-90d +%Y-%m-%dT00:00:00Z)
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users?filter=lastLogin+lt+\"${NINETY_DAYS_AGO}\"&limit=200" \
|
||
|
|
| jq '.[] | {email: .profile.email, lastLogin: .lastLogin}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. Offboarding
|
||
|
|
|
||
|
|
### Account Deactivation Checklist
|
||
|
|
|
||
|
|
Run this sequence when an employee departs. Order matters -- revoke sessions first, then deactivate.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
DEPARTING_USER="alice@company.com"
|
||
|
|
|
||
|
|
# Step 1: Revoke all active sessions immediately
|
||
|
|
# Okta
|
||
|
|
USER_ID=$(curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" | jq -r '.id')
|
||
|
|
|
||
|
|
curl -s -X DELETE \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/sessions"
|
||
|
|
|
||
|
|
# Google Workspace: Revoke tokens and sign out
|
||
|
|
gam user "${DEPARTING_USER}" signout
|
||
|
|
gam user "${DEPARTING_USER}" deprovision
|
||
|
|
|
||
|
|
# Azure AD: Revoke all refresh tokens
|
||
|
|
az ad user update --id "${DEPARTING_USER}" --account-enabled false
|
||
|
|
az rest --method POST \
|
||
|
|
--url "https://graph.microsoft.com/v1.0/users/${DEPARTING_USER}/revokeSignInSessions"
|
||
|
|
|
||
|
|
# Step 2: Deactivate the user account
|
||
|
|
# Okta
|
||
|
|
curl -s -X POST \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/lifecycle/deactivate"
|
||
|
|
|
||
|
|
# Google Workspace
|
||
|
|
gam update user "${DEPARTING_USER}" suspended on
|
||
|
|
|
||
|
|
# Step 3: Transfer data ownership
|
||
|
|
# Google Workspace: Transfer Drive files
|
||
|
|
gam user "${DEPARTING_USER}" transfer drive manager@company.com
|
||
|
|
|
||
|
|
# Google Workspace: Transfer Calendar ownership
|
||
|
|
gam user "${DEPARTING_USER}" transfer calendar manager@company.com
|
||
|
|
|
||
|
|
# Step 4: Remove from all groups (prevents future provisioning)
|
||
|
|
# Okta
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${USER_ID}/groups" \
|
||
|
|
| jq -r '.[].id' | while read gid; do
|
||
|
|
curl -s -X DELETE \
|
||
|
|
-H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/groups/${gid}/users/${USER_ID}"
|
||
|
|
done
|
||
|
|
|
||
|
|
# Step 5: Revoke app-specific tokens
|
||
|
|
# GitHub: Remove from org
|
||
|
|
gh api -X DELETE "orgs/company/members/${DEPARTING_USER}"
|
||
|
|
|
||
|
|
# Slack: Deactivate via SCIM
|
||
|
|
SLACK_USER_ID=$(curl -s -H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
|
||
|
|
"https://api.slack.com/scim/v2/Users?filter=userName+eq+\"${DEPARTING_USER}\"" \
|
||
|
|
| jq -r '.Resources[0].id')
|
||
|
|
|
||
|
|
curl -s -X PATCH \
|
||
|
|
-H "Authorization: Bearer ${SLACK_SCIM_TOKEN}" \
|
||
|
|
-H "Content-Type: application/scim+json" \
|
||
|
|
"https://api.slack.com/scim/v2/Users/${SLACK_USER_ID}" \
|
||
|
|
-d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","value":{"active":false}}]}'
|
||
|
|
|
||
|
|
# AWS: Remove SSO access
|
||
|
|
aws sso-admin delete-account-assignment \
|
||
|
|
--instance-arn "$INSTANCE_ARN" \
|
||
|
|
--target-id "123456789012" \
|
||
|
|
--target-type AWS_ACCOUNT \
|
||
|
|
--permission-set-arn "$PERMISSION_SET_ARN" \
|
||
|
|
--principal-type USER \
|
||
|
|
--principal-id "$AWS_SSO_USER_ID"
|
||
|
|
|
||
|
|
# Step 6: Document and log
|
||
|
|
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | OFFBOARD | ${DEPARTING_USER} | all sessions revoked, account suspended, data transferred to manager@company.com" >> /var/log/offboarding-audit.log
|
||
|
|
```
|
||
|
|
|
||
|
|
### Post-Offboarding Verification
|
||
|
|
|
||
|
|
```bash
|
||
|
|
DEPARTING_USER="alice@company.com"
|
||
|
|
|
||
|
|
# Verify account is suspended/deactivated
|
||
|
|
echo "=== Offboarding Verification ==="
|
||
|
|
|
||
|
|
# Google Workspace
|
||
|
|
gam info user "${DEPARTING_USER}" fields suspended | grep -i "suspended: true" && echo "[OK] Google suspended" || echo "[FAIL] Google still active"
|
||
|
|
|
||
|
|
# Okta
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/users/${DEPARTING_USER}" \
|
||
|
|
| jq -r '.status' | grep -q "DEPROVISIONED" && echo "[OK] Okta deprovisioned" || echo "[FAIL] Okta still active"
|
||
|
|
|
||
|
|
# GitHub
|
||
|
|
gh api "orgs/company/members/${DEPARTING_USER}" 2>&1 | grep -q "404" && echo "[OK] GitHub removed" || echo "[FAIL] GitHub still member"
|
||
|
|
|
||
|
|
# Check for any remaining active sessions in audit logs
|
||
|
|
echo "=== Checking for post-offboard activity ==="
|
||
|
|
curl -s -H "Authorization: SSWS ${OKTA_API_TOKEN}" \
|
||
|
|
"${OKTA_ORG_URL}/api/v1/logs?filter=actor.alternateId+eq+\"${DEPARTING_USER}\"&since=$(date -u +%Y-%m-%dT%H:%M:%SZ)&limit=10" \
|
||
|
|
| jq '.[] | {time: .published, event: .eventType, outcome: .outcome.result}'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
| Task | Google Workspace | Okta | Azure AD |
|
||
|
|
|---|---|---|---|
|
||
|
|
| Create user | `gam create user` | `POST /api/v1/users` | `az ad user create` |
|
||
|
|
| Suspend user | `gam update user suspended on` | `POST /lifecycle/deactivate` | `az ad user update --account-enabled false` |
|
||
|
|
| Enforce MFA | `gam update org 2sv enforced` | MFA enrollment policy | Conditional access policy |
|
||
|
|
| Revoke sessions | `gam user signout` | `DELETE /users/{id}/sessions` | `revokeSignInSessions` |
|
||
|
|
| Audit logins | `gam report login` | `GET /api/v1/logs` | `GET /auditLogs/signIns` |
|
||
|
|
| SCIM provision | Built-in for supported apps | App integration SCIM tab | Enterprise app provisioning |
|