mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user