This commit is contained in:
Toby
2026-03-24 18:02:50 -04:00
parent 2d209b9258
commit ba9e489584
111 changed files with 48382 additions and 3099 deletions
@@ -0,0 +1,869 @@
---
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 |
@@ -0,0 +1,771 @@
---
name: mdm-device-management
description: Manage and secure company devices with MDM solutions — enroll macOS, Windows, iOS, and Android devices, enforce security policies, and automate software deployment. Use when setting up device management for a growing team.
license: MIT
metadata:
author: devops-skills
version: "1.0"
---
# Mobile Device Management (MDM) for Startups & Small Teams
A practical guide to enrolling, securing, and managing company devices across
macOS, Windows, iOS, and Android — from zero-touch onboarding to remote wipe.
---
## 1. When to Use MDM
MDM becomes essential when any of the following apply:
- **Team size crosses ~10 people** — manual laptop setup no longer scales.
- **Compliance requirements** — SOC 2, HIPAA, ISO 27001, or customer security
questionnaires demand proof that endpoints are encrypted and patched.
- **Remote / hybrid workforce** — you cannot walk over to someone's desk to
fix a configuration or verify disk encryption.
- **Contractor or BYOD devices** — you need a way to separate corporate data
from personal data and revoke access on offboarding.
- **Insurance or investor due diligence** — cyber-insurance carriers and VCs
increasingly ask for evidence of endpoint management.
If you are still under 10 people and everyone is in-office, a simple checklist
plus a configuration management tool (Ansible) may suffice — but plan for MDM
early so enrollment is painless when you scale.
---
## 2. MDM Platform Comparison
| Platform | Best For | Pricing Model | Open Source | Key Strength |
|----------|----------|---------------|-------------|--------------|
| **Jamf Pro** | macOS / iOS fleets | Per-device/yr | No | Deepest Apple integration, DEP/ADE native |
| **Microsoft Intune** | Windows + M365 shops | Bundled w/ M365 E3/E5 | No | Seamless Azure AD + Autopilot |
| **Kandji** | macOS-first startups | Per-device/yr | No | Pre-built compliance templates, fast setup |
| **Mosyle** | Education & SMB Apple | Per-device/yr | No | Apple School/Business Manager integration |
| **Fleet** | Cross-platform, eng-led | Free (OSS) / paid cloud | Yes | osquery-powered, GitOps-friendly, API-first |
| **SimpleMDM** | Small Apple-only teams | Per-device/mo | No | Simple UI, quick onboarding |
### Decision heuristic
```text
if (team < 50 AND engineering-led AND multi-OS):
consider Fleet (open-source, osquery-native)
elif (team is macOS-dominant AND compliance-heavy):
consider Kandji or Jamf
elif (team is Windows-dominant AND already on M365):
consider Intune (likely already licensed)
else:
evaluate Fleet or Kandji based on OS mix
```
---
## 3. Fleet (Open Source MDM) — Self-Hosted Deployment
Fleet is the leading open-source MDM. It uses osquery under the hood and
supports macOS, Windows, Linux, iOS, and Android.
### 3.1 Docker Compose deployment
```yaml
# docker-compose.yml
version: "3.8"
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: "${FLEET_MYSQL_ROOT_PASSWORD}"
MYSQL_DATABASE: fleet
MYSQL_USER: fleet
MYSQL_PASSWORD: "${FLEET_MYSQL_PASSWORD}"
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
fleet:
image: fleetdm/fleet:v4.47.0
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
environment:
FLEET_MYSQL_ADDRESS: mysql:3306
FLEET_MYSQL_DATABASE: fleet
FLEET_MYSQL_USERNAME: fleet
FLEET_MYSQL_PASSWORD: "${FLEET_MYSQL_PASSWORD}"
FLEET_REDIS_ADDRESS: redis:6379
FLEET_SERVER_TLS: "true"
FLEET_SERVER_TLS_COMPATIBILITY: modern
FLEET_SERVER_CERT: /tls/fleet.crt
FLEET_SERVER_KEY: /tls/fleet.key
FLEET_LOGGING_JSON: "true"
volumes:
- ./tls:/tls:ro
ports:
- "8080:8080"
volumes:
mysql-data:
```
### 3.2 Initial setup
```bash
# Generate TLS certs (use real certs in production)
mkdir -p tls
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 \
-nodes -keyout tls/fleet.key -out tls/fleet.crt \
-subj "/CN=fleet.yourcompany.com"
# Start services
docker compose up -d
# Create admin account
docker compose exec fleet fleet prepare db
docker compose exec fleet fleet setup \
--email admin@yourcompany.com \
--name "IT Admin" \
--password "${FLEET_ADMIN_PASSWORD}" \
--org-name "YourCompany"
```
### 3.3 Enroll a macOS host with fleetctl
```bash
# Install fleetctl
brew install fleetdm/tap/fleetctl
# Authenticate
fleetctl config set --address https://fleet.yourcompany.com:8080
fleetctl login --email admin@yourcompany.com
# Generate an installer package for macOS
fleetctl package --type pkg \
--fleet-url https://fleet.yourcompany.com:8080 \
--enroll-secret "$(fleetctl get enroll-secret)" \
--fleet-certificate tls/fleet.crt
# The .pkg file can be distributed via Apple Business Manager or manually
```
### 3.4 Enroll a Windows host
```powershell
# Download the Fleet osquery MSI installer
fleetctl package --type msi `
--fleet-url https://fleet.yourcompany.com:8080 `
--enroll-secret "$(fleetctl get enroll-secret)" `
--fleet-certificate tls/fleet.crt
# Install silently
msiexec /i fleet-osquery.msi /quiet /norestart
```
### 3.5 osquery policy examples in Fleet
```yaml
# fleet-policies.yml — apply with: fleetctl apply -f fleet-policies.yml
apiVersion: v1
kind: policy
spec:
name: FileVault enabled (macOS)
query: >
SELECT 1 FROM disk_encryption
WHERE user_uuid IS NOT '' AND encrypted = 1;
description: Ensures FileVault disk encryption is enabled.
resolution: "Enable FileVault: System Settings > Privacy & Security > FileVault."
platform: darwin
---
apiVersion: v1
kind: policy
spec:
name: BitLocker enabled (Windows)
query: >
SELECT 1 FROM bitlocker_info
WHERE protection_status = 1;
description: Ensures BitLocker drive encryption is active.
resolution: "Enable BitLocker via Settings > Privacy & Security > Device Encryption."
platform: windows
---
apiVersion: v1
kind: policy
spec:
name: Firewall enabled (macOS)
query: >
SELECT 1 FROM alf WHERE global_state >= 1;
description: macOS Application Layer Firewall must be on.
resolution: "Enable firewall: System Settings > Network > Firewall."
platform: darwin
---
apiVersion: v1
kind: policy
spec:
name: OS up to date (macOS)
query: >
SELECT 1 FROM os_version
WHERE platform = 'darwin' AND major >= 14;
description: Requires macOS 14 (Sonoma) or later.
resolution: "Update macOS via System Settings > General > Software Update."
platform: darwin
```
---
## 4. macOS Enrollment
### 4.1 Apple Business Manager (ABM) / Automated Device Enrollment
```bash
# In ABM (business.apple.com):
# 1. Settings > MDM Servers > Add MDM Server
# 2. Upload the public key from your MDM (Fleet, Jamf, Kandji)
# 3. Download the ABM token and upload it to your MDM
# 4. Assign devices to the MDM server by serial number
# Verify DEP assignment with fleetctl (Fleet)
fleetctl get mdm-apple
```
### 4.2 Manual MDM profile enrollment (non-DEP devices)
```bash
# Generate enrollment profile URL (Fleet example)
fleetctl get enrollment-profile > enrollment.mobileconfig
# Distribute to user — they open the .mobileconfig file
# Then approve in System Settings > Profiles
```
### 4.3 Enforce FileVault via MDM configuration profile
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.MCX.FileVault2</string>
<key>PayloadIdentifier</key>
<string>com.yourcompany.filevault</string>
<key>PayloadUUID</key>
<string>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>Enable</key>
<string>On</string>
<key>Defer</key>
<true/>
<key>DeferForceAtUserLoginMaxBypassAttempts</key>
<integer>0</integer>
<key>ShowRecoveryKey</key>
<false/>
<key>UseRecoveryKey</key>
<true/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>FileVault Enforcement</string>
<key>PayloadIdentifier</key>
<string>com.yourcompany.filevault.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>F1E2D3C4-B5A6-7890-FEDC-BA0987654321</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
```
### 4.4 macOS firewall enforcement
```bash
# Enable firewall via MDM command or script
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setallowsigned enable
```
---
## 5. Windows Enrollment
### 5.1 Azure AD Join + Intune auto-enrollment
```powershell
# Check current join status
dsregcmd /status
# Join Azure AD (user will be prompted for credentials)
Start-Process "ms-settings:workplace"
# Verify Intune enrollment
Get-WmiObject -Namespace "root\cimv2\mdm\dmmap" `
-Class "MDM_DevDetail_Ext01" | Select DeviceID
```
### 5.2 Windows Autopilot hardware hash collection
```powershell
# Collect hardware hash for Autopilot registration
Install-Script -Name Get-WindowsAutoPilotInfo -Force
Get-WindowsAutoPilotInfo -OutputFile C:\temp\autopilot.csv
# Upload autopilot.csv to Intune > Devices > Windows Enrollment > Devices
```
### 5.3 BitLocker enforcement via Group Policy or Intune
```powershell
# Enable BitLocker on the OS drive with TPM
Enable-BitLocker -MountPoint "C:" `
-EncryptionMethod XtsAes256 `
-TpmProtector
# Add a recovery password and back it up to Azure AD
Add-BitLockerKeyProtector -MountPoint "C:" -RecoveryPasswordProtector
BackupToAAD-BitLockerKeyProtector -MountPoint "C:" `
-KeyProtectorId (Get-BitLockerVolume -MountPoint "C:").KeyProtector[1].KeyProtectorId
# Verify encryption status
Get-BitLockerVolume | Select-Object MountPoint, VolumeStatus, EncryptionPercentage
```
### 5.4 Windows Firewall baseline
```powershell
# Ensure all profiles are enabled
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
# Block all inbound by default, allow outbound
Set-NetFirewallProfile -Profile Domain,Public,Private `
-DefaultInboundAction Block `
-DefaultOutboundAction Allow
# Allow specific inbound rules (example: RDP only from VPN subnet)
New-NetFirewallRule -DisplayName "Allow RDP from VPN" `
-Direction Inbound -Protocol TCP -LocalPort 3389 `
-RemoteAddress 10.0.0.0/8 -Action Allow
```
---
## 6. Security Policies — Cross-Platform
### 6.1 Password / passcode requirements
```xml
<!-- macOS configuration profile — password policy -->
<dict>
<key>PayloadType</key>
<string>com.apple.mobiledevice.passwordpolicy</string>
<key>minLength</key>
<integer>12</integer>
<key>requireAlphanumeric</key>
<true/>
<key>maxInactivity</key>
<integer>5</integer>
<key>maxPINAgeInDays</key>
<integer>90</integer>
</dict>
```
```json
// Intune Windows password policy (JSON for Graph API)
{
"@odata.type": "#microsoft.graph.windows10GeneralConfiguration",
"passwordRequired": true,
"passwordMinimumLength": 12,
"passwordRequiredType": "alphanumeric",
"passwordMinutesOfInactivityBeforeScreenTimeout": 5,
"passwordExpirationDays": 90,
"passwordBlockSimple": true
}
```
### 6.2 Screen lock enforcement
```bash
# macOS — require password after sleep/screensaver (via script or profile)
sudo defaults write /Library/Preferences/com.apple.screensaver askForPassword -int 1
sudo defaults write /Library/Preferences/com.apple.screensaver askForPasswordDelay -int 0
sudo defaults write /Library/Preferences/com.apple.screensaver idleTime -int 300
```
```powershell
# Windows — lock screen after 5 minutes of inactivity
powercfg /change monitor-timeout-ac 5
# Registry-based enforcement
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" `
-Name "InactivityTimeoutSecs" -Value 300
```
### 6.3 Encryption enforcement summary
| OS | Tool | Verify Command |
|----|------|----------------|
| macOS | FileVault | `fdesetup status` |
| Windows | BitLocker | `manage-bde -status C:` |
| Linux | LUKS | `lsblk -o NAME,FSTYPE,MOUNTPOINT \| grep crypt` |
| iOS | Native (always-on with passcode) | Managed via MDM profile |
| Android | Native | `adb shell getprop ro.crypto.state` |
---
## 7. Software Deployment
### 7.1 macOS — Homebrew Bundle
```ruby
# Brewfile — deploy via MDM script or Git checkout
tap "homebrew/bundle"
# Core tools
brew "git"
brew "gh"
brew "jq"
brew "wget"
brew "gnupg"
# Security
brew "1password-cli"
cask "1password"
cask "tailscale"
cask "cloudflare-warp"
# Development
cask "visual-studio-code"
cask "iterm2"
cask "docker"
brew "node"
brew "python@3.12"
# Communication
cask "slack"
cask "zoom"
```
```bash
# Deploy Brewfile on a new Mac
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew bundle --file=/path/to/Brewfile --no-lock
```
### 7.2 Windows — winget / Chocolatey
```powershell
# winget import from a JSON manifest
# packages.json
@"
{
"Sources": [{
"Packages": [
{ "PackageIdentifier": "Git.Git" },
{ "PackageIdentifier": "Microsoft.VisualStudioCode" },
{ "PackageIdentifier": "Docker.DockerDesktop" },
{ "PackageIdentifier": "SlackTechnologies.Slack" },
{ "PackageIdentifier": "Zoom.Zoom" },
{ "PackageIdentifier": "Tailscale.Tailscale" },
{ "PackageIdentifier": "AgileBits.1Password" },
{ "PackageIdentifier": "OpenJS.NodeJS.LTS" },
{ "PackageIdentifier": "Python.Python.3.12" }
],
"SourceDetails": {
"Name": "winget",
"Type": "Microsoft.Winget.Source.Type.Microsoft"
}
}]
}
"@ | Out-File -FilePath packages.json -Encoding utf8
winget import -i packages.json --accept-package-agreements --accept-source-agreements
```
### 7.3 Automatic update enforcement
```bash
# macOS — enable automatic updates via MDM or command
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool true
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -bool true
sudo softwareupdate --schedule on
```
```powershell
# Windows — configure Windows Update via registry
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" `
-Name "NoAutoUpdate" -Value 0
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" `
-Name "AUOptions" -Value 4 # 4 = Auto download and schedule install
```
---
## 8. Compliance Checks with osquery
These queries work with Fleet, osquery standalone, or any osquery-compatible
platform.
```sql
-- Check disk encryption on macOS
SELECT de.encrypted, de.type, du.username
FROM disk_encryption de
JOIN disk_util du ON de.name = du.name
WHERE du.mountpoint = '/' AND de.encrypted = 1;
-- Check disk encryption on Windows
SELECT drive_letter, protection_status, conversion_status
FROM bitlocker_info
WHERE drive_letter = 'C:' AND protection_status = 1;
-- Verify firewall is enabled (macOS)
SELECT global_state, stealth_enabled, logging_enabled
FROM alf;
-- Verify firewall is enabled (Windows)
SELECT name, enabled FROM windows_firewall_profiles
WHERE enabled = 1;
-- Check OS version (macOS)
SELECT name, version, major, minor, patch
FROM os_version
WHERE major >= 14;
-- Check OS version (Windows)
SELECT name, version, build
FROM os_version
WHERE build >= '22631';
-- List users with admin privileges (macOS)
SELECT u.username, u.uid
FROM users u
JOIN user_groups ug ON u.uid = ug.uid
JOIN groups g ON ug.gid = g.gid
WHERE g.groupname = 'admin';
-- Detect unencrypted removable drives (Windows)
SELECT device_id, drive_letter, protection_status
FROM bitlocker_info
WHERE protection_status = 0;
-- Check screen lock timeout (macOS)
SELECT domain, key, value FROM preferences
WHERE domain = 'com.apple.screensaver'
AND key = 'idleTime';
-- Verify automatic updates are enabled (macOS)
SELECT domain, key, value FROM preferences
WHERE domain = 'com.apple.SoftwareUpdate'
AND key = 'AutomaticCheckEnabled';
```
---
## 9. Remote Wipe & Lock
### 9.1 macOS remote wipe (Fleet)
```bash
# Lock a device immediately with a 6-digit PIN
fleetctl mdm lock --host "serial=C02X12345678"
# Wipe a device (factory reset) — DESTRUCTIVE
fleetctl mdm erase --host "serial=C02X12345678"
# Or via the Fleet API
curl -X POST https://fleet.yourcompany.com/api/v1/fleet/hosts/42/wipe \
-H "Authorization: Bearer ${FLEET_API_TOKEN}"
```
### 9.2 Windows remote wipe (Intune)
```powershell
# Via Microsoft Graph API
$body = @{
keepEnrollmentData = $false
keepUserData = $false
} | ConvertTo-Json
Invoke-MgGraphRequest -Method POST `
-Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/{deviceId}/wipe" `
-Body $body -ContentType "application/json"
```
### 9.3 Lost device runbook
```text
1. Employee reports device lost/stolen via Slack #it-help or PagerDuty.
2. IT admin verifies identity (video call or manager confirmation).
3. Immediately issue remote lock command (wipe only if data-sensitive).
4. Rotate any credentials cached on the device:
- Revoke SSO sessions (Okta/Google Workspace admin console)
- Rotate API keys stored on the device
- Revoke VPN certificates
5. File a police report if theft is suspected.
6. Remove device from MDM after 30 days or once replacement is shipped.
7. Update asset inventory and notify finance for insurance claim.
```
---
## 10. Onboarding Automation — Zero-Touch Enrollment
### 10.1 macOS zero-touch flow
```bash
#!/usr/bin/env bash
# onboard-mac.sh — runs as a post-enrollment script via MDM
set -euo pipefail
LOG="/var/log/onboarding.log"
exec > >(tee -a "$LOG") 2>&1
echo "=== Starting onboarding $(date) ==="
# 1. Install Rosetta 2 on Apple Silicon
if [[ "$(uname -m)" == "arm64" ]]; then
softwareupdate --install-rosetta --agree-to-license
fi
# 2. Install Homebrew
if ! command -v brew &>/dev/null; then
NONINTERACTIVE=1 /bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# 3. Install standard tooling from Brewfile
curl -fsSL https://internal.yourcompany.com/brewfile -o /tmp/Brewfile
brew bundle --file=/tmp/Brewfile --no-lock
# 4. Configure Git defaults
git config --global init.defaultBranch main
git config --global pull.rebase true
# 5. Enable FileVault (will prompt at next login)
sudo fdesetup enable -defer /var/db/FileVaultDeferred.plist \
-forceatlogin 0 -dontaskatlogout
# 6. Enable firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
# 7. Set screen lock
defaults write com.apple.screensaver askForPassword -int 1
defaults write com.apple.screensaver askForPasswordDelay -int 0
defaults write com.apple.screensaver idleTime -int 300
# 8. Enroll in Tailscale VPN
open -a "Tailscale"
echo "=== Onboarding complete $(date) ==="
```
### 10.2 Windows zero-touch flow (Autopilot + Intune)
```powershell
# deploy.ps1 — assigned as an Intune PowerShell script
$ErrorActionPreference = "Stop"
$logFile = "C:\ProgramData\onboarding.log"
Start-Transcript -Path $logFile -Append
Write-Host "=== Starting onboarding $(Get-Date) ==="
# 1. Install winget packages
$packages = @(
"Git.Git",
"Microsoft.VisualStudioCode",
"Docker.DockerDesktop",
"SlackTechnologies.Slack",
"Tailscale.Tailscale",
"AgileBits.1Password"
)
foreach ($pkg in $packages) {
Write-Host "Installing $pkg..."
winget install --id $pkg --accept-package-agreements --accept-source-agreements --silent
}
# 2. Enable BitLocker
Enable-BitLocker -MountPoint "C:" -EncryptionMethod XtsAes256 -TpmProtector
Add-BitLockerKeyProtector -MountPoint "C:" -RecoveryPasswordProtector
# 3. Configure firewall
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
Set-NetFirewallProfile -Profile Domain,Public,Private `
-DefaultInboundAction Block -DefaultOutboundAction Allow
# 4. Set power and lock settings
powercfg /change monitor-timeout-ac 5
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" `
-Name "InactivityTimeoutSecs" -Value 300
# 5. Enable automatic updates
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" `
-Name "AUOptions" -Value 4
Write-Host "=== Onboarding complete $(Get-Date) ==="
Stop-Transcript
```
### 10.3 Onboarding checklist (for IT automation)
```yaml
# onboarding-checklist.yml — track in your ticketing system or Fleet
new_hire_onboarding:
pre_day_one:
- Purchase and ship device via CDW/Apple Business Manager
- Assign device to MDM server in ABM/Autopilot
- Create accounts: Google Workspace / M365, Okta SSO, GitHub, Slack
- Generate VPN invite (Tailscale, WireGuard)
- Prepare welcome documentation link
day_one_automated:
- Device powers on and auto-enrolls in MDM (zero-touch)
- MDM pushes security profiles (encryption, firewall, password policy)
- Software bundle installs automatically
- User signs into SSO — all apps authenticate via SAML/OIDC
- Compliance policies begin evaluation
day_one_manual:
- IT schedules 15-min welcome call to verify setup
- Employee confirms disk encryption enabled (fdesetup status / manage-bde)
- Employee joins #it-help Slack channel
- Employee completes security awareness training link
week_one_verification:
- Fleet/MDM dashboard shows device as compliant
- All critical policies passing (encryption, firewall, OS version)
- VPN connectivity verified
- MFA enrolled on all critical services
```
---
## Quick Reference
| Task | macOS Command | Windows Command |
|------|---------------|-----------------|
| Check encryption | `fdesetup status` | `manage-bde -status C:` |
| Enable firewall | `socketfilterfw --setglobalstate on` | `Set-NetFirewallProfile -Enabled True` |
| Force OS update | `softwareupdate -ia` | `usoclient StartInstallD` |
| Lock screen now | `pmset displaysleepnow` | `rundll32.exe user32.dll,LockWorkStation` |
| List MDM profiles | `profiles show -type enrollment` | `dsregcmd /status` |
| Check compliance | `fleetctl get hosts --query "..."` | `fleetctl get hosts --query "..."` |
@@ -0,0 +1,400 @@
---
name: saas-security-posture
description: Audit and harden your SaaS tool stack — enforce SSO, review OAuth grants, manage shadow IT, and secure admin accounts across Slack, GitHub, Google Workspace, and AWS. Use when tightening security across company SaaS tools.
license: MIT
metadata:
author: devops-skills
version: "1.0"
---
# SaaS Security Posture Management for Startups
Secure every SaaS tool your company relies on with practical, command-driven hardening.
## 1. When to Use This Skill
- **SOC 2 preparation** — auditors need evidence of MFA, access controls, and OAuth governance.
- **Suspicious OAuth app** — an employee authorized a third-party app with broad scopes.
- **SaaS sprawl** — teams sign up for tools with company email and nobody tracks them.
- **Post-incident hardening** — after phishing or credential leaks, tighten every surface.
## 2. SaaS Inventory Audit
### Google Workspace — OAuth Grants
```bash
gam all users show tokens > oauth_tokens_audit.csv
```
### GitHub — Installed Apps
```bash
gh api /orgs/{ORG}/installations --paginate \
--jq '.installations[] | {app: .app_slug, permissions: .permissions, created: .created_at}'
gh api /orgs/{ORG}/credential-authorizations --paginate \
--jq '.[] | {login: .login, credential_type: .credential_type}'
```
### Slack — Approved and Pending Apps
```bash
curl -s -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
"https://slack.com/api/admin.apps.approved.list" | jq '.approved_apps[] | {name: .app.name, id: .app.id}'
curl -s -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
"https://slack.com/api/admin.apps.requests.list" | jq '.app_requests[]'
```
### AWS — IAM Credential Report
```bash
aws iam generate-credential-report
aws iam get-credential-report --output text --query 'Content' | base64 -d > iam_credential_report.csv
```
### Master Inventory Template
```yaml
tools:
- name: Google Workspace
owner: it@company.com
sso: true
mfa: enforced
- name: GitHub Enterprise
owner: engineering@company.com
sso: true
mfa: enforced
- name: Slack Business+
owner: it@company.com
sso: true
app_approval: required
- name: AWS Organizations
owner: platform@company.com
sso: true
scp_enforced: true
```
---
## 3. GitHub Security Hardening
```bash
# Enforce 2FA and find non-compliant members
gh api -X PATCH /orgs/{ORG} -f two_factor_requirement_enabled=true
gh api /orgs/{ORG}/members?filter=2fa_disabled --paginate --jq '.[].login'
# Verify SAML SSO identities
gh api /orgs/{ORG}/credential-authorizations --paginate \
--jq '.[] | {login: .login, saml_name_id: .saml_name_id}'
# Add IP allow list entry
gh api -X POST /orgs/{ORG}/ip-allow-list \
-f allow_list_value="203.0.113.0/24" -f name="Office VPN" -F is_active=true
# Branch protection on main
gh api -X PUT /repos/{ORG}/{REPO}/branches/main/protection \
-H "Accept: application/vnd.github+json" --input - <<'EOF'
{
"required_status_checks": {"strict": true, "contexts": ["ci/build","ci/test"]},
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 2,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true
},
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
}
EOF
# Audit PATs and revoke stale tokens
gh api /orgs/{ORG}/personal-access-tokens --paginate \
--jq '.[] | {owner: .owner.login, name: .token_name, expires: .token_expires_at}'
gh api -X DELETE /orgs/{ORG}/personal-access-tokens/{PAT_ID}
# Audit deploy keys and webhooks
for repo in $(gh repo list {ORG} --limit 500 --json name -q '.[].name'); do
gh api /repos/{ORG}/${repo}/keys --jq '.[] | {title: .title, read_only: .read_only}'
done
gh api /orgs/{ORG}/hooks --jq '.[] | {url: .config.url, events: .events, active: .active}'
```
---
## 4. Slack Security
```bash
# Require app approval
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.apps.config.set" -d '{"app_approval_enabled": true}'
# Set workspace to invite-only
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.teams.settings.setDiscoverability" \
-d '{"team_id": "T0XXXXXXX", "discoverability": "invite_only"}'
# Force re-authentication every 24 hours
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.teams.settings.setSessionDuration" \
-d '{"team_id": "T0XXXXXXX", "session_duration": 86400}'
# Set message retention to 1 year
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.teams.settings.setRetentionPolicy" \
-d '{"team_id": "T0XXXXXXX", "retention_type": "all", "retention_duration": 365}'
# Audit Slack Connect shared channels
curl -s -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
"https://slack.com/api/admin.conversations.search?search_channel_types=connect" \
| jq '.conversations[] | {name: .name, is_ext_shared: .is_ext_shared}'
```
---
## 5. Google Workspace Hardening
```bash
# Enforce 2-Step Verification and strong passwords
gam update org "/" settings 2sv enforced
gam update org "/" settings password_length 14
# Block all third-party OAuth apps, then whitelist specific ones
gam update org "/" settings oauth_access block_all
gam update org "/" settings oauth_access whitelist client_id:APP_CLIENT_ID_1
# Disable external Drive sharing and file transfers
gam update org "/" settings drive sharing_outside_domain off
gam update org "/" settings drive transfer_to_personal off
gam update org "/" settings groups external_members off
# Verify email authentication records
dig TXT company.com | grep "v=spf1"
dig TXT google._domainkey.company.com
dig TXT _dmarc.company.com
# Expected: v=DMARC1; p=reject; rua=mailto:dmarc-reports@company.com; pct=100
# Mobile device management
gam update org "/" settings mobile management advanced
gam update org "/" settings mobile screen_lock required
gam update org "/" settings mobile encryption required
gam update mobile ${DEVICE_ID} action wipe # compromised device
```
---
## 6. AWS Account Security
```bash
# Root account lockdown — verify MFA, remove access keys
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent'
# SSO permission set with least privilege
aws sso-admin create-permission-set --instance-arn "${SSO_INSTANCE_ARN}" \
--name "DeveloperAccess" --session-duration "PT8H"
aws sso-admin attach-managed-policy-to-permission-set \
--instance-arn "${SSO_INSTANCE_ARN}" --permission-set-arn "${PERMISSION_SET_ARN}" \
--managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"
```
### Service Control Policies
```json
{
"Version": "2012-10-17",
"Statement": [
{"Sid": "DenyRootActions", "Effect": "Deny", "Action": "*", "Resource": "*",
"Condition": {"StringLike": {"aws:PrincipalArn": "arn:aws:iam::*:root"}}},
{"Sid": "DenyLeaveOrg", "Effect": "Deny",
"Action": "organizations:LeaveOrganization", "Resource": "*"}
]
}
```
```bash
aws organizations create-policy --name "DenyRootActions" \
--type SERVICE_CONTROL_POLICY --content file://deny-root-actions.json
aws organizations attach-policy --policy-id "${POLICY_ID}" --target-id "${ORG_ROOT_ID}"
# Organization-wide CloudTrail
aws cloudtrail create-trail --name org-security-trail \
--s3-bucket-name company-cloudtrail-logs \
--is-multi-region-trail --is-organization-trail --enable-log-file-validation
aws cloudtrail start-logging --name org-security-trail
```
---
## 7. OAuth App Review
### Identify High-Risk Grants
```bash
# Google — find apps with dangerous scopes
gam all users show tokens | grep -E "(drive|gmail|admin)" > high_risk_oauth.txt
# GitHub — find apps with write access
gh api /orgs/{ORG}/installations --paginate \
--jq '.installations[] | select(.permissions.contents == "write") | {app: .app_slug}'
```
### Revoke Dangerous Grants
```bash
gam user compromised@company.com delete token clientid APP_CLIENT_ID # single app
gam user compromised@company.com delete tokens # all apps
gh api -X DELETE /orgs/{ORG}/installations/{INSTALLATION_ID} # GitHub app
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.apps.uninstall" -d '{"app_id": "A0XXXXXXX"}'
```
### Scope Risk Classification
```
CRITICAL — revoke unless justified:
Google: mail.google.com, admin.directory.user | GitHub: admin:org, repo | Slack: admin
HIGH — review carefully:
Google: googleapis.com/auth/drive | GitHub: contents:write | Slack: channels:read
LOW — generally safe:
Google: userinfo.email | GitHub: read:org | Slack: identity.basic
```
---
## 8. Admin Account Protection
```bash
# Dedicated admin account in Google Workspace
gam create user admin-jdoe@company.com firstname "John (Admin)" lastname "Doe" \
password "$(openssl rand -base64 32)" org "/Admins"
gam update user admin-jdoe@company.com admin on
# Require hardware security keys for the Admins OU
gam update org "/Admins" settings 2sv security_key_only
# AWS MFA enforcement policy
cat <<'EOF' > enforce-mfa-policy.json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyUnlessMFA", "Effect": "Deny",
"NotAction": ["iam:CreateVirtualMFADevice","iam:EnableMFADevice",
"iam:GetUser","iam:ListMFADevices","sts:GetSessionToken"],
"Resource": "*",
"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}
}]
}
EOF
aws iam create-policy --policy-name EnforceMFA --policy-document file://enforce-mfa-policy.json
# Break-glass account for SSO outages
BREAK_GLASS_PW=$(openssl rand -base64 48)
gam create user breakglass@company.com firstname "Break" lastname "Glass" \
password "${BREAK_GLASS_PW}" org "/Admins" admin on
# Store password in a sealed envelope in a physical safe
# After every use: rotate password, re-seal, log the incident
```
---
## 9. Data Loss Prevention
```bash
# Google Drive — block external sharing and restrict viewers
gam update org "/" settings drive sharing_outside_domain off
gam update org "/" settings drive disable_download_print_copy_for_viewers on
# GitHub — enable secret scanning and push protection org-wide
gh api -X PATCH /orgs/{ORG} -f security_product=secret_scanning -f enablement=enable_all
gh api -X PATCH /orgs/{ORG} -f security_product=secret_scanning_push_protection -f enablement=enable_all
gh api /orgs/{ORG}/secret-scanning/alerts --paginate \
--jq '.[] | {repo: .repository.name, secret_type: .secret_type, state: .state}'
# Slack — restrict data export to org admins
curl -s -X POST -H "Authorization: Bearer ${SLACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
"https://slack.com/api/admin.teams.settings.setExportRestrictions" \
-d '{"team_id": "T0XXXXXXX", "export_type": "org_admins_only"}'
# AWS — block all public S3 access at account level
aws s3control put-public-access-block --account-id "${AWS_ACCOUNT_ID}" \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
```
---
## 10. Shadow IT Detection
### DNS-Based Discovery
```bash
SHADOW_IT_DOMAINS=("airtable.com" "notion.so" "trello.com" "asana.com"
"monday.com" "clickup.com" "figma.com" "canva.com" "miro.com"
"zapier.com" "dropbox.com" "box.com" "wetransfer.com")
for domain in "${SHADOW_IT_DOMAINS[@]}"; do
count=$(grep -c "${domain}" /var/log/dns/query.log 2>/dev/null || echo "0")
[ "${count}" -gt 0 ] && echo "DETECTED: ${domain}${count} queries"
done
```
### Google Workspace Login Audit
```bash
gam report login parameters app_name \
start_time "2026-03-01T00:00:00Z" end_time "2026-03-24T23:59:59Z" > login_audit.csv
gam report token > token_usage_report.csv
```
### Proxy Log Analysis
```bash
awk '{print $7}' /var/log/squid/access.log | sed 's|https\?://||;s|/.*||' \
| sort | uniq -c | sort -rn | head -50 > top_domains.txt
comm -23 <(awk '{print $2}' top_domains.txt | sort) \
<(yq '.tools[].domains[]' saas-inventory.yaml | sort) > unapproved.txt
```
### Automated Alerting
```bash
cat <<'SCRIPT' > /usr/local/bin/shadow-it-check.sh
#!/usr/bin/env bash
set -euo pipefail
APPROVED="/etc/security/approved-saas-domains.txt"
YESTERDAY=$(date -d "yesterday" +%d-%b-%Y)
grep "${YESTERDAY}" /var/log/dns/query.log | awk '{print $4}' | sort -u > /tmp/today.txt
NEW=$(comm -23 /tmp/today.txt <(sort "${APPROVED}"))
[ -n "${NEW}" ] && mail -s "[ALERT] Shadow IT" security@company.com <<< "${NEW}"
SCRIPT
chmod +x /usr/local/bin/shadow-it-check.sh
echo "0 8 * * * root /usr/local/bin/shadow-it-check.sh" >> /etc/cron.d/shadow-it-check
```
---
## Quick Reference — Top 10 Priority Actions
| # | Action | Scope |
|---|--------|-------|
| 1 | Enforce MFA/2FA everywhere | Google, GitHub, AWS, Slack |
| 2 | Enable SSO with your IdP | All tools |
| 3 | Audit and revoke OAuth grants | Google, GitHub |
| 4 | Require Slack app approval | Slack |
| 5 | Branch protection on main | GitHub |
| 6 | Secret scanning + push protection | GitHub |
| 7 | Block public S3 buckets | AWS |
| 8 | Enable org-wide CloudTrail | AWS |
| 9 | Disable external Drive sharing | Google |
| 10 | Create break-glass admin accounts | Google, AWS |
## Maintenance Cadence
**Weekly:** Review OAuth grants, secret scanning alerts, Slack app queue.
**Monthly:** AWS IAM report, rotate service keys, admin account review, shadow IT scan.
**Quarterly:** Full SaaS inventory refresh, OAuth pruning, break-glass test, SCP updates.
@@ -4,36 +4,396 @@ description: Practical IT troubleshooting playbooks for small teams without dedi
license: MIT
metadata:
author: devops-skills
version: "1.0"
version: "2.0"
---
# Startup IT Troubleshooting
Run lightweight IT operations for startups and small teams.
Runbooks for startups and small teams where engineers double as the IT department.
## Priority Triage Order
## When to Use
1. Company-wide outages (internet, SSO, email)
2. Executive or customer-facing blockers
3. Team-wide performance degradations
4. Individual workstation issues
You are the "accidental IT person." Nobody has IT in their title, but laptops freeze, Wi-Fi drops during investor demos, someone gets locked out of Google Workspace at midnight, and a new hire starts Monday with zero accounts. This skill gives you copy-paste commands to handle it all.
## Common Fix Playbooks
**Priority triage:** (1) Company-wide outages, (2) Executive/customer-facing blockers, (3) Team-wide degradations, (4) Individual workstation issues. Always ask: "How many people are affected?" and "Is revenue impacted?"
- Identity and access lockouts
- VPN and Wi-Fi reliability issues
- Laptop disk and memory pressure
- Endpoint patching and update failures
- Printer and conferencing room failures
---
## Process Best Practices
## SSO / Identity Lockouts
- Keep an internal runbook and known-issues log.
- Standardize onboarding/offboarding checklists.
- Track asset ownership and warranty windows.
- Escalate recurring incidents into root-cause fixes.
### Google Workspace via GAM
```bash
bash <(curl -s -S -L https://gam-shortn.appspot.com/gam-install) # install GAM
gam oauth create # authorize
gam update user jane@company.com password "TempPass123!" changepassword on # reset password
gam update user jane@company.com suspended off # unsuspend locked-out user
gam user jane@company.com signout # force sign-out all sessions
gam user jane@company.com update backupcodes # new MFA backup codes
gam user jane@company.com turnoff2sv # disable 2SV (re-enable within 24h)
```
### Okta API
```bash
OKTA="company.okta.com"; T="your-api-token"; UID="00u1abcdef"
curl -X POST -H "Authorization: SSWS $T" "https://$OKTA/api/v1/users/$UID/lifecycle/unlock"
curl -X POST -H "Authorization: SSWS $T" "https://$OKTA/api/v1/users/$UID/lifecycle/reset_password?sendEmail=true"
curl -X POST -H "Authorization: SSWS $T" "https://$OKTA/api/v1/users/$UID/lifecycle/reset_factors"
curl -X DELETE -H "Authorization: SSWS $T" "https://$OKTA/api/v1/users/$UID/sessions"
```
**MFA recovery flow:** Verify identity via video call, generate backup codes or reset factors, have user re-enroll immediately, confirm old device is deregistered, log the incident.
---
## Network Troubleshooting
### Wi-Fi Debugging
```bash
# macOS
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I
networksetup -setairportpower en0 off && sleep 2 && networksetup -setairportpower en0 on
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux
nmcli device wifi list && nmcli connection show --active
nmcli device disconnect wlan0 && nmcli device connect wlan0
sudo systemd-resolve --flush-caches
```
```powershell
netsh wlan show interfaces
netsh wlan disconnect; netsh wlan connect name="OfficeWiFi"
ipconfig /flushdns
netsh winsock reset # full stack reset, reboot after
```
### DNS Issues
```bash
nslookup company.com 8.8.8.8 # test against known-good DNS
dig @1.1.1.1 company.com # Linux/macOS detail
sudo networksetup -setdnsservers Wi-Fi 8.8.8.8 8.8.4.4 # macOS temp override
```
```powershell
$a = Get-NetAdapter | Where-Object {$_.Status -eq "Up"}
Set-DnsClientServerAddress -InterfaceIndex $a.ifIndex -ServerAddresses ("8.8.8.8","8.8.4.4")
```
### VPN Not Connecting
```bash
nc -zv vpn.company.com 443 # test port reachability
sudo wg show # WireGuard status
sudo wg-quick down wg0 && sudo wg-quick up wg0 # restart WireGuard
tailscale status && sudo tailscale up --reset # Tailscale re-auth
```
### Slow Internet
```bash
speedtest-cli --simple # bandwidth test (pip install speedtest-cli)
ping -c 50 8.8.8.8 # packet loss check
networkQuality -s # macOS 12+ bufferbloat test
```
---
## Laptop Performance
### Disk Space
```bash
df -h # volume overview
du -sh ~/* | sort -rh | head -15 # biggest dirs in home
docker system df # Docker disk usage (common culprit)
docker system prune -a --volumes # reclaim Docker space
brew cleanup --prune=all # macOS Homebrew cleanup
```
```powershell
Get-PSDrive -PSProvider FileSystem | Select Name,@{N='Free(GB)';E={[math]::Round($_.Free/1GB,2)}}
Get-ChildItem C:\ -Recurse -File -EA SilentlyContinue | Sort Length -Desc | Select -First 15 FullName,@{N='MB';E={[math]::Round($_.Length/1MB,2)}}
```
### Memory Pressure and Runaway Processes
```bash
# macOS
memory_pressure
top -o rsize -l 1 -n 10 -stats pid,command,rsize
pkill -f "Google Chrome Helper"
# Linux
free -h && ps aux --sort=-%mem | head -11
sudo dmesg | grep -i "oom\|out of memory"
```
```powershell
Get-Process | Sort WorkingSet64 -Desc | Select -First 10 Name,@{N='MB';E={[math]::Round($_.WorkingSet64/1MB,2)}}
Stop-Process -Name "Teams" -Force
```
### Battery Health
```bash
system_profiler SPPowerDataType | grep -E "Cycle Count|Condition" # macOS
upower -i /org/freedesktop/UPower/devices/battery_BAT0 # Linux
```
```powershell
powercfg /batteryreport /output "$env:USERPROFILE\Desktop\battery.html"
```
---
## macOS Administration
```bash
profiles status -type enrollment # MDM enrollment check
sudo systemsetup -setremotelogin on # enable SSH for remote admin
# Homebrew fleet setup — standard Brewfile
cat > Brewfile <<'EOF'
brew "git"; brew "node"; brew "python@3.12"; brew "awscli"; brew "jq"; brew "gh"
cask "google-chrome"; cask "slack"; cask "1password"; cask "visual-studio-code"; cask "docker"; cask "zoom"
EOF
brew bundle install --file=Brewfile
brew bundle dump --file=~/Brewfile --force # export current setup
# FileVault
sudo fdesetup status && sudo fdesetup enable # store recovery key in 1Password
# Updates
softwareupdate -l && sudo softwareupdate -ia --restart
```
---
## Windows Administration
```powershell
gpresult /r; gpupdate /force # check and refresh Group Policy
# Windows Update
Install-Module PSWindowsUpdate -Force -Scope CurrentUser
Install-WindowsUpdate -AcceptAll -AutoReboot
# If stuck: reset update components
Stop-Service wuauserv,cryptSvc,bits,msiserver -Force
Remove-Item "C:\Windows\SoftwareDistribution" -Recurse -Force
Start-Service wuauserv,cryptSvc,bits,msiserver
# BitLocker
manage-bde -status C:
Enable-BitLocker -MountPoint "C:" -EncryptionMethod XtsAes256 -UsedSpaceOnly -TpmProtector
# Remote Desktop
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
```
---
## Linux Desktop
```bash
# Ubuntu — fix broken packages
sudo apt --fix-broken install && sudo dpkg --configure -a && sudo apt update && sudo apt upgrade -y
# Fedora — fix broken packages
sudo dnf check && sudo dnf distro-sync && sudo dnf update -y
# Service failures
systemctl --failed
journalctl -p err -b
# Drivers
sudo ubuntu-drivers autoinstall # Ubuntu proprietary drivers
lspci | grep -i vga && sudo lshw -C display # GPU info
sudo dmesg | grep -i firmware # missing firmware
# Display issues
xrandr --auto # reset to auto-detect
xrandr --output HDMI-1 --mode 1920x1080 --rate 60 # force resolution
echo $XDG_SESSION_TYPE # Wayland vs X11 check
```
---
## Email / Calendar Issues
### Google Workspace
```bash
gam user jane@company.com show forwarding # check rogue forwarding rules
gam user jane@company.com delete forwarding # remove forwarding
gam user jane@company.com show delegates # check email delegation
gam user jane@company.com show filters # check mail filters
```
### Microsoft 365
```powershell
Install-Module ExchangeOnlineManagement -Force -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@company.com
Get-MessageTrace -SenderAddress jane@company.com -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)
Get-MailboxStatistics -Identity jane@company.com | Select DisplayName,TotalItemSize
```
### Email Deliverability
```bash
dig TXT company.com | grep "v=spf1" # SPF
dig TXT google._domainkey.company.com # DKIM
dig TXT _dmarc.company.com # DMARC
```
---
## Onboarding Checklist
```bash
# 1. Google Workspace account
gam create user newhire@company.com firstname "Jane" lastname "Smith" \
password "Welcome2Company!" changepassword on org "/Engineering"
gam update group engineering@company.com add member newhire@company.com
# 2. 1Password
op user provision --email newhire@company.com --name "Jane Smith"
# 3. Slack
curl -X POST "https://slack.com/api/admin.users.invite" \
-H "Authorization: Bearer xoxp-your-admin-token" \
-d "email=newhire@company.com&channel_ids=C01GENERAL,C02ENGINEERING&team_id=T01YOURTEAM"
# 4. GitHub
gh api orgs/your-company/invitations -f email="newhire@company.com" -f role="direct_member"
gh api orgs/your-company/teams/engineering/memberships/newhire-username -f role="member" -X PUT
# 5. VPN / Tailscale
tailscale up --authkey tskey-auth-abc123
```
### First-Day Setup Script (macOS)
Give new hires this script. It installs Homebrew, your standard tools from a hosted Brewfile, configures Git, authenticates GitHub CLI, clones core repos, and enables FileVault.
```bash
#!/bin/bash
set -e
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
curl -sL https://internal.company.com/setup/Brewfile -o /tmp/Brewfile && brew bundle install --file=/tmp/Brewfile
read -p "Full name: " N; read -p "Email: " E
git config --global user.name "$N" && git config --global user.email "$E" && git config --global pull.rebase true
gh auth login && mkdir -p ~/src && cd ~/src && gh repo clone your-company/main-app
sudo fdesetup enable
```
## Offboarding Checklist
Run these **immediately** when someone departs. Speed matters for security.
```bash
gam update user departed@company.com suspended on # 1. block all access
gam user departed@company.com signout # 2. kill sessions
gam user departed@company.com transfer drive manager@company.com # 3. transfer Drive
gam user departed@company.com add delegate manager@company.com # 4. delegate email 30d
curl -X POST "https://slack.com/api/admin.users.remove" \
-H "Authorization: Bearer xoxp-your-admin-token" \
-d "user_id=U01DEPARTED&team_id=T01YOURTEAM" # 5. remove Slack
gh api orgs/your-company/members/departed-username -X DELETE # 6. remove GitHub
op user suspend departed@company.com # 7. revoke 1Password
aws iam delete-login-profile --user-name departed # 8. revoke AWS console
aws iam list-access-keys --user-name departed # then delete each key
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Offboarded departed@company.com" >> ~/offboarding-log.txt
```
---
## Video Conferencing
```bash
# macOS
lsof | grep "AppleCamera\|VDC" # check what owns the camera
pkill -f zoom.us && open -a zoom.us # restart Zoom
tccutil reset Camera # reset camera permissions
# Linux
pactl list short sources # list mics
pactl set-source-mute @DEFAULT_SOURCE@ 0 # unmute mic
```
```powershell
Get-CimInstance Win32_SoundDevice | Select Name, Status
```
**Quick fixes:** No audio = check OS mute + correct device. No video = close other conferencing apps. Echo = use headphones. Choppy = need 3+ Mbps upload.
---
## Printer / Peripheral Issues
```bash
# macOS
lpstat -p -d && cancel -a # list printers, clear queue
sudo launchctl stop org.cups.cupsd && sudo launchctl start org.cups.cupsd
system_profiler SPUSBDataType # USB devices
# Linux
sudo systemctl restart cups # restart print system
lsusb && dmesg | tail -20 # USB diagnostics
```
```powershell
Restart-Service Spooler -Force # restart print spooler
Get-PrintJob -PrinterName "OfficePrinter" | Remove-PrintJob # clear stuck jobs
```
---
## Security Basics
### Endpoint Protection
```bash
# macOS
spctl --status # Gatekeeper
csrutil status # SIP
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
# Linux
sudo ufw enable && sudo ufw default deny incoming && sudo ufw default allow outgoing
```
```powershell
Get-MpComputerStatus | Select AntivirusEnabled, RealTimeProtectionEnabled
Start-MpScan -ScanType QuickScan
Get-NetFirewallProfile | Select Name, Enabled
```
### Phishing Response
```bash
gam update user compromised@company.com password "$(openssl rand -base64 16)" changepassword on
gam user compromised@company.com signout # kill sessions
gam user compromised@company.com turnoff2sv # reset MFA
gam user compromised@company.com show tokens # check rogue OAuth apps
gam user compromised@company.com show forwarding # check attacker persistence
```
### Lost / Stolen Device Protocol
1. **Immediately** -- Remote wipe via MDM or Find My Mac.
2. **Within 15 min** -- Reset password and kill sessions (SSO commands above).
3. **Within 1 hour** -- Rotate API keys and secrets: `gh auth refresh`, delete AWS access keys.
4. **Within 24 hours** -- Review access logs for suspicious activity.
## Related Skills
- [incident-management](../../../compliance/continuity/incident-management/) - Structured incident handling
- [runbook-creation](../../../compliance/continuity/runbook-creation/) - Documentation standards
- [incident-management](../../../compliance/continuity/incident-management/) -- Structured incident handling
- [runbook-creation](../../../compliance/continuity/runbook-creation/) -- Documentation standards