# Microsoft 365 Operating Runbook

**Companion doc** — read alongside `cbb-utils/M365/README.md` and
`02_-_General/M365_Tenant_Configuration.md`. This runbook covers the initial setup run order,
ongoing operations, and credential rotation. It does not repeat per-script usage details
already in the cbb-utils README.

---

## Prerequisites

Before running any script:

1. **PowerShell 7+** (`pwsh`) must be installed. Verify: `pwsh --version`
2. **ExchangeOnlineManagement module** — install once:
   ```powershell
   Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force
   ```
3. **Microsoft.Graph modules** — required by the provisioning and posture scripts:
   ```powershell
   Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force
   ```
4. **Namecheap API access** — required only for `-Publish` DNS operations:
   - Log in to Namecheap → Profile → Tools → API Access
   - Whitelist the public IP of the machine running the scripts
   - Have the API key ready (the scripts prompt for it; it is never stored)
5. **Your machine's public IP** must be whitelisted in Namecheap before any `publish-*`
   command will succeed. If it changes (DHCP, VPN, etc.), re-whitelist before running.

---

## Initial Setup — Run Order

Run from the `cbb-utils/M365/` directory using:
```bash
pwsh -NoProfile -File <script>.ps1 [flags]
```

Each script uses device-code auth — it will print a URL and a code. Open the URL in a browser,
enter the code, and authenticate with `jschwefel@coldboreballisticsllc.com`.

### Step 1 — Baseline snapshot (read-only)
```bash
pwsh -NoProfile -File audit_posture.ps1
```
Changes nothing. Records the current state: Security Defaults, MFA registration, Global Admin
count, DKIM status, Secure Score, external-sender rule. Run this first; run it again at the end
to confirm the backlog shrank.

### Step 2 — Allocate DKIM selectors and push CNAMEs
```bash
pwsh -NoProfile -File enable_dkim.ps1 -Publish
```
Creates DKIM signing configs (2048-bit keys) for each authoritative custom domain if they do not
already exist. Exchange Online may take up to 48 hours to provision the selector keys after the
config is created — if `Selector1CNAME` is empty, the script will say so and skip that domain.
Once the selectors are populated, `-Publish` pushes the two CNAME records per domain to
Namecheap automatically (read-merge-write; prompts for confirmation per domain).

### Step 3 — Wait for DNS propagation
Allow ~15 minutes after Step 2 before proceeding. DKIM signing cannot be enabled until the
CNAMEs resolve globally.

### Step 4 — Enable DKIM signing
```bash
pwsh -NoProfile -File enable_dkim.ps1
```
Re-run without `-Publish`. Now that CNAMEs are live, this flips `Enabled = true` for each
domain. Exchange Online will then verify the CNAMEs and activate signing; allow up to 48 hours.

### Step 5 — Push all mail-auth DNS records
```bash
pwsh -NoProfile -File generate_dns_records.ps1 -Publish
```
Pushes MX, SPF TXT, DMARC TXT, autodiscover CNAME, and DKIM CNAMEs for each domain to
Namecheap. The DKIM CNAMEs are redundant at this point (already pushed in Step 2) but
the upsert is idempotent and harmless. For each domain you will see the full zone, a
`[KEPT]`/`[NEW/UPDATED]` summary, and a confirmation prompt before any write occurs.

### Step 6 — Harden EOP policies
```bash
pwsh -NoProfile -File harden_eop.ps1
```
Tunes the default anti-malware, anti-spam, anti-phishing, and outbound spam policies.
Adds an `[EXTERNAL]` sender-warning transport rule. Idempotent — safe to re-run.
Watch for yellow `!` lines on first run indicating any parameter unsupported on Standard tier.

### Step 7 — Tighten external sharing
```bash
pwsh -NoProfile -File sharing_policy.ps1
```
Restricts SharePoint and OneDrive external sharing to existing guest users only (no new
anonymous links or external invites). Uses Microsoft Graph; does not require the
Windows-only SPO module.

### Step 8 — Provision org structure
Run in any order — all are idempotent:
```bash
pwsh -NoProfile -File provision_security_groups.ps1
pwsh -NoProfile -File provision_distribution_lists.ps1
pwsh -NoProfile -File provision_m365groups.ps1
```

### Step 9 — Enable Security Defaults
```bash
pwsh -NoProfile -File enable_security_defaults.ps1
```
Enforces MFA for all users and blocks legacy auth protocols. Idempotent. Note: Security Defaults
and Conditional Access are mutually exclusive — if you ever upgrade to Business Premium and enable
CA policies, disable Security Defaults first.

### Step 10 — Enable mailbox audit logging
```bash
pwsh -NoProfile -File enable_mailbox_audit.ps1
```
Records admin, delegate, and owner actions on all mailboxes into the Unified Audit Log.
The Unified Audit Log must be enabled first (it is on by default on Business Standard).

### Step 11 — Configure alert policy notifications
```bash
pwsh -NoProfile -File configure_alert_policies.ps1
```
Adds `jschwefel@coldboreballisticsllc.com` as notification recipient on all High/Medium-severity
built-in Purview alert policies. Uses IPPSSession (Security & Compliance PowerShell).

### Step 12 — Enable Office Message Encryption
```bash
pwsh -NoProfile -File enable_ome.ps1
```
Adds the Encrypt button in Outlook/OWA for sending encrypted mail to external recipients.
Available at Business Standard — no Premium required.

### Step 13 — Verify
```bash
pwsh -NoProfile -File audit_posture.ps1
```
Re-run the baseline snapshot. Compare against Step 1 output — the WARN count should be
significantly lower. Note remaining warnings and resolve them per the guidance in the script output.

---

## Post-Business-Premium-Upgrade Steps

The following cannot be run at Business Standard. Run these immediately after upgrading to
Business Premium. The scripts are already written and idempotent.

### Email retention policy
```bash
pwsh -NoProfile -File purview_retention_policy.ps1
```
**Blocked at Business Standard** — requires the `INFORMATION_PROTECTION_COMPLIANCE` service plan
(not present in the `MICROSOFT_365_BUSINESS_STANDARD_NO_TEAMS` SKU).

When run after upgrading, creates policy `CBB-7yr-Email-Retention` — Exchange All mailboxes,
retain 7 years minimum, no auto-delete. This enforces a floor aligned with the IRS 7-year
business records requirement and Wisconsin's 6-year contract statute of limitations.

After running, verify in the Purview compliance portal:
`compliance.microsoft.com` → Data lifecycle management → Microsoft 365 → Retention policies

---

## Portal-Only Steps

The following cannot be scripted on headless Linux at Business Standard and must be done in a
browser. Both are documented here for completeness.

### Security Defaults (MFA enforcement)
1. Go to the Microsoft Entra admin center: `entra.microsoft.com`
2. Sign in as `jschwefel@coldboreballisticsllc.com`
3. Navigate to: **Identity → Overview → Properties**
4. Click **Manage security defaults** at the bottom of the page
5. Toggle **Security defaults** to **Enabled**, then save

Security Defaults enforces MFA for all users on sign-in. Under Security Defaults, any RFC-6238
TOTP authenticator app works (1Password, Bitwarden, Authy, Google Authenticator, Microsoft
Authenticator). SMS/voice/FIDO2/Windows Hello require Conditional Access (Business Premium).

### Basic Mobility & Security (device management)
1. Go to the Microsoft 365 admin center: `admin.microsoft.com`
2. Sign in as `jschwefel@coldboreballisticsllc.com`
3. Navigate to: **Security → Basic Mobility and Security**
4. Follow the setup wizard to enforce PIN/encryption and enable remote wipe on enrolled devices

---

## Ongoing Operations

### DMARC graduation (quarantine → reject)
After ~2 weeks of receiving clean aggregate reports at `dmarc@coldboreballisticsllc.com`:

1. Verify no legitimate mail is being quarantined by reviewing DMARC aggregate reports
2. Update the `_dmarc` TXT record on each domain from `p=quarantine` to `p=reject`:
   ```bash
   NAMECHEAP_API_USER=coldboreballistics \
   ./namecheap_dns.sh publish-mail <domain>
   ```
   Edit the `DMARC_RUA` value in `namecheap_dns.sh` to `p=reject` first, or push the updated
   record directly:
   ```bash
   NAMECHEAP_API_USER=coldboreballistics \
   ./namecheap_dns.sh publish coldboreballisticsllc.com \
     "_dmarc|TXT|v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@coldboreballisticsllc.com; fo=1|1800|10"
   ```
   The `publish` command does the same read-merge-write and confirmation as `publish-mail`.
3. Repeat for each domain.

### Adding a new user
New users are provisioned using `cbb-utils/M365/provision_user.ps1`. Each additional user
requires a Business Standard (or Business Premium) license — confirm availability before
running or the script will fail at the license assignment step.

```bash
pwsh -NoProfile -File provision_user.ps1
```

The script runs interactively and handles end-to-end provisioning:

1. Creates the Entra ID user account with a temporary password (forced change on first sign-in)
2. Assigns a Microsoft 365 Business Standard license
3. Adds alias addresses on all owned domains (`scope-dope.com`, `ballisticcomputer.com`, `ballisticscomputer.com`)
4. Adds the user to all standard security groups (`sg-`) and distribution lists (`dl-`)
5. Grants access to standard SharePoint sites
6. Grants SendAs on the `admin@` and `support@` shared mailboxes
7. Optionally assigns admin roles
8. Sends a branded HTML welcome email to the user's alternate (personal) address

**Welcome email contents:**
- CBB banner image (dark, `cbb_banner_dark.png` embedded as base64)
- Sign-in credentials (email address and temporary password)
- MFA setup instructions
- "Start Here →" button linking to the CBB SharePoint document library
- Contact link to `support@coldboreballisticsllc.com`

The welcome email is sent from `jschwefel@coldboreballisticsllc.com` via `Send-MgUserMail`
(delegated `Mail.Send`) and saved to Sent Items.

**Banner source:** `OneDrive/CBB_Corporate/01_-_Public/Logos_and_Wordmarks/cbb_banner_dark.png`
(618×200 source SVG: `cbb_banner_dark.svg`). If the banner is updated, re-embed the new PNG
into `Get-WelcomeEmailHtml` in `provision_user.ps1` using:
```bash
python3 -c "import base64; print(base64.b64encode(open('cbb_banner_dark.png','rb').read()).decode())"
```
Replace the base64 string in the `<img src="data:image/png;base64,…">` tag.

### Adding a shared mailbox
Use the `exo_shared_mailboxes.ps1` script in `cbb-utils/M365/` as a template. The script handles
cross-domain aliases and `jschwefel` access grant. Read the script header before running — it
is a template, not a general-purpose tool, and you will need to adapt the recipient list.

### Rotating the Namecheap API key
1. Log in to Namecheap → Profile → Tools → API Access
2. Generate a new API key and copy it
3. The `namecheap_dns.sh` script prompts for the key at runtime — no config file to update
4. If the calling IP has changed, re-whitelist it on the same page before running any script
5. Update the `M365/generate_dns_records.ps1` and `M365/enable_dkim.ps1` scripts prompt for
   the key via `Read-Host -AsSecureString` — same process, no stored value to rotate

### Re-running the posture audit
Run `audit_posture.ps1` at any time — it is read-only and safe to run repeatedly:
```bash
pwsh -NoProfile -File audit_posture.ps1
```
Review the WARN lines and address any new findings. Recommended cadence: quarterly, or after
any significant tenant change.

---

## Break-Glass Account

`admin@coldboreballistics.onmicrosoft.com` is the emergency Global Admin account. It is
cloud-only (not federated), has no license, and should not be used for day-to-day work.

- Store the password and any MFA seed in a secure offline location
- Do not assign a license to this account
- `audit_posture.ps1` verifies that at least 2 Global Admins exist — this is the second one
- If this account is ever deleted, re-create it immediately before proceeding with any
  other tenant work

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/M365/M365_Operating_Runbook.md` |
| **Cross-references** | `M365_Architecture_Reference.md`; `M365_Rebuild_Guide.md`; `Entra_App_Registration_Policy.md` |
| **Created** | 2026-06-06 |
| **Last updated** | 2026-06-06 |
| **Next full review due** | 2027-06-06 |

---

## Revision Control Block

| Rev | Date | Changed By | Reviewed By | Approved By | Summary |
|-----|------|------------|-------------|-------------|---------|
| 0.9 | 2026-06-06 | jschwefel | — | — | Initial release |
