Files
claudetools/clients/glaztech/reports/2026-06-05-least-privilege-db-migration-scope.md
Mike Swanson 1e957fa922 glaztech: least-privilege tom DB migration scope + 2026-06-05 session log
Scope (v0.3) for replacing the website's sysadmin login 'tom' with a
least-privilege login: two-phase plan (GTIware co-residency forces keeping
cc_file in Phase 1), Grok + Gemini independent review folded in, and live
RMM recon findings that materially changed the picture - the website is a
cross-office + Sage accounting + payroll + msdb hub on one sysadmin
credential, SQL is centralized on GTI-INV-SQL\GTISQL:3436 (not per-site).
PARKED pending a full network recon. Session log covers the website outage
fix (incomplete E1 ACL hardening) + the scoping + recon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:01:18 -07:00

185 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Glaztech — Least-Privilege DB Login Migration: Full Scope
**Status:** DRAFT v0.3 — PARKED pending full network recon (see §11). Grok + Gemini reviewed; **live RMM recon (§11) materially changed the picture — §1-§3 are too optimistic and must be reconciled before execution.** · **Date:** 2026-06-05
**Owner:** ACG · **Ticket:** #32378 (Waiting on Customer) · **Coord todo:** `aebaf751`
**Source of truth:** `clients/glaztech/reports/2026-06-03-website-security-assessment.md`
(C0, C0-Extended, H6, and the "Sequencing the least-privilege DB migration" callout)
> Objective: stop the internet-facing website (`D:\web\glaztech_4`, IIS site `glaztech_new`,
> server `WWW` 192.168.8.72 / 65.113.52.88) from connecting to `GTI-INV-SQL`
> (192.168.8.62,3436) as the `sysadmin` login `tom`, replacing it with a least-privilege
> login — without taking the live payment site or the card-charging engine down.
---
## 1. The central constraint that shapes everything: GTIware co-residency
The website's `Web.config` `tom` connection is **shared, in the same IIS worker process**, with the GTIware card-on-file engine DLLs (`gt_auto_process_2020.dll`, `glaztech_utilities_2020.dll` in `D:\web\glaztech_4\Bin\`). Those DLLs **carry no connection strings of their own** — they use the website's `tom` connection — and they legitimately need `cc_file` / `cof_payments_header` (card tables) to write and charge saved cards.
**Consequence:** a *true* least-privilege website login (with `DENY` on `cc_file`) **cannot be applied while the card engine shares that connection** — it would break auto-billing. Per assessment item 22, "you cannot safely swap only the website's connection string while the engine shares the same process and config."
This forces a **two-phase** migration:
- **Phase 1 (achievable now, ACG-drivable):** remove the catastrophic rights — `sysadmin`/`securityadmin`/`dbcreator`, cross-office DB reach, `*_archive`, `msdb`/`master`, linked servers, and the `xp_cmdshell` OS-RCE path — while **retaining** the site's own-office DB access **and** `cc_file`/`cof_payments_header` (because the in-process engine still needs them). This alone collapses the worst of C0: SQLi can no longer reach OS-RCE, the domain-admin password in `msdb`, other offices' data, or the linked-server mesh.
- **Phase 2 (gated on Tom's parallel GTIware decoupling — item 17/22):** once the card engine runs off its own dedicated connection/host, `DENY cc_file`/`cof_payments_header` on the website login, completing true least-privilege.
**Phase 1 delivers ~80% of the risk reduction without depending on Tom's code** — it collapses the catastrophic chain (OS-RCE via `xp_cmdshell`, the cleartext domain-admin password in `msdb`, all *other* offices + archives, the linked-server pivot, raw `web_security` dumps). **But be precise (both models): Phase 1 is "blast radius contained," NOT "cards safe."** Because the web login still holds `cc_file`/`cof_payments_header` GRANT for the engine, a SQLi (C3) plus a guessed customer login still yields full PAN+CVV **for the office this portal serves**. That residual card exposure closes only in Phase 2 (DENY `cc_file`) after the GTIware decoupling — and ultimately in the tokenization/purge workstream (assessment items 16-17).
---
## 2. Pre-execution inventory (callout step 1 — MUST complete before any change)
Two inventories, both **read-only**. These are the gating tasks; do not draft the grant set until they're done.
### 2a. What objects the website actually touches (to build the GRANT set)
- Source-side: enumerate every table/view/stored-proc referenced by the site's SQL — the 948 parameterized calls + 59 concatenated statements. Grep the VB.NET source on the box (`D:\web\glaztech_4`, excluding `Old_bin`/`Old_code`) for `SqlCommand`, `.CommandText`, proc names, `FROM`/`JOIN`/`INTO`/`UPDATE`/`EXEC` targets; dedupe to an object list per database.
- DB-side cross-check: SQL Server **Extended Events / Profiler trace** on the `tom` login over a representative window (a full business day incl. an `gt_auto_process` run) to capture the actual object-access set empirically — catches dynamic SQL the static grep misses. (Read-only; trace metadata only.)
### 2b. Every consumer of the `tom` login (to plan the password rotation, callout step 5)
Rotating `tom` without updating every consumer in the same change window breaks live systems. Enumerate:
- **Website** `Web.config` connection strings (primary).
- **In-process GTIware DLLs** — confirmed users of the website connection.
- **Server-side billing jobs:** `d:\sql_jobs\bin\gt_console_apps.exe` (modes cp/is/oa/lo) run by SQL Agent — confirm whether it authenticates as `tom` or the Agent (domain-admin) account.
- **SQL Agent jobs / job steps** referencing `tom` (`msdb.dbo.sysjobsteps`).
- **Linked servers** whose remote-login mapping uses `tom` (`sys.servers` + `sys.linked_logins`).
- **The second host** `\\192.168.0.147\web\glaztech_4` (same code tree — may hold its own `tom` Web.config).
- **`/webhooks` + `/webhooks1`** (Samsara) apps — do they carry a `tom` connection?
- Any scheduled tasks / internal apps / Excel/Access ODBC DSNs using `tom`.
Queries: `sys.dm_exec_sessions`/`sys.dm_exec_connections` filtered by `login_name='tom'` sampled over time; `sys.linked_logins`; `msdb` job-step scan.
---
## 3. Phase 1 — de-privilege (the achievable-now core)
### 3a. New login + grant/DENY matrix (draft — finalize after §2a)
Create a **new SQL login** (e.g. `web_glaztech_app`) — SQL auth, **no server roles** beyond `public`. Use a **high-entropy but alphanumeric-only password** (avoid `; ' = + " { }`), vaulted — legacy VB.NET/GTIware connection-string parsing may mis-handle special characters and silently truncate/fail the string (Gemini).
| Scope | Action |
|---|---|
| Own-office production DB(s) the site serves (`glaz_prod`, `glaz_prod_tuc`, etc. — per §2a) | `GRANT` EXECUTE on the site's procs + SELECT/INSERT/UPDATE on the specific tables/views it uses (least-priv, object-level, **not** `db_owner`) |
| `cc_file`, `cof_payments_header` (card tables) | **Phase 1: GRANT** (in-process engine needs them) → **Phase 2: DENY** after GTIware decoupling |
| `web_security` (passwords) | GRANT EXECUTE on `get_web_accesslevel` only; **DENY** direct SELECT on the table. Works via **ownership chaining** (proc reads the table on the caller's behalf; raw `SELECT * FROM web_security` injection blocked) — **only if** the proc is `dbo`-owned, has **no dynamic SQL** (`EXEC(@sql)`/`sp_executesql`) and **no `EXECUTE AS`**. Dynamic SQL runs under the caller and would hit the DENY. *Scan the proc set (§2a) for dynamic SQL and explicitly GRANT any tables reached that way* (both models). |
| Every **other office's** DB + all `*_archive` DBs | **DENY** (no cross-DB reach). **But first confirm no legitimate view/proc/report joins current data with archive/other-office data** — cross-DB ownership chaining is OFF by default, so such a join will hard-break under the DENY. The §2a trace must surface cross-DB dependencies (Gemini). |
| `master`, `msdb`, `model` | **DENY**; **no** `xp_cmdshell` (login isn't sysadmin, so it can't enable/run it). **Do NOT `DENY tempdb`**`public` must keep local `#temp` creation, which legacy procs/the engine likely use; a tempdb DENY breaks them on swap. (Watch for `##global` temp / physical tempdb tables in legacy code — those would still fail.) (both models) |
| Linked servers (all 7) | No grants. **Do NOT assume non-sysadmin = blocked** — audit `sys.servers` + `sys.linked_logins` for a catch-all *"connections made using this security context"* mapping to a remote `sa`/sysadmin; the new login would silently inherit those rights on the remote box. Set the per-login mapping to *"Not be made"* and test from the new login during the window. (both models) |
| Server roles | none (`public` only) — removes `sysadmin`/`securityadmin`/`dbcreator` reach |
**Net Phase-1 effect:** a SQLi via `quo()` now executes as a constrained login — no OS RCE, no `msdb` domain-admin password, no other-office/archive data, no linked-server pivot, no raw password-table dump. Card tables remain reachable *only* because the co-resident engine requires them (closed in Phase 2).
### 3b. Sequence (callout steps 2-4)
1. Create `web_glaztech_app` + apply the grant/DENY matrix (no impact yet — nothing uses it).
2. **Staged `Web.config` swap** to the new login, in a maintenance window, with **SQL + app error monitoring live** (SQL error log, IIS/app logs, the new failed-login logging if available). Watch a full cycle including an `gt_auto_process` run.
3. Any missing object permission surfaces as a SQL error → grant it (re-validate §2a) or roll back.
4. Once proven in production across a representative window, **revoke `sysadmin`/`securityadmin`/`dbcreator` from `tom`** (only after confirming no *other* consumer needs those rights — §2b).
**Connection-pool note (Gemini):** both the swap (step 2) **and** the rollback **must** include an IIS app-pool **recycle**. A `Web.config` edit alone does not flush ADO.NET's existing pooled `tom` connections — lingering pooled connections can mask the new login's failures or cause inconsistent behavior. Recycle on every connection-string change so the worker opens fresh connections under the intended login.
**Both website and engine move at once:** the in-process GTIware DLLs read the **same** `Web.config`, so the swap moves the website *and* the card engine to `web_glaztech_app` simultaneously. Phase 1's `cc_file`/`cof_payments_header` GRANT (and every other engine-touched object found in §2a) must be in place before the swap, or auto-billing fails the instant the pool recycles. (First confirm the DLLs truly source the string from `Web.config` and don't hardcode `tom` — §2b / Q in §8.)
### 3c. Rollback
Single-step, fast: revert the `Web.config` connection string to `tom` and **recycle the app pool** (required to flush the pool — see note above). Keep `tom` intact and unchanged until Phase 1 is proven — do **not** demote/rotate `tom` until step 4/5. Take a `Web.config` backup before each edit (those backups still contain the old plaintext `tom` password — pair this work with E1 so they aren't world-readable).
---
## 4. `tom` password rotation (callout step 5 — removes the plaintext sysadmin cred)
**Corrected per Gemini's push-back (the earlier draft was self-contradictory).** Because the in-process card engine reads the **same `Web.config`**, the Phase 1 swap **vacates `tom` entirely from the IIS worker** — both the website and the engine are now on `web_glaztech_app`. So rotation is **NOT** gated on Phase 2 or the engine; it is gated only on the **external** `tom` consumers from §2b (the `.147` host, SQL Agent jobs, `gt_console_apps.exe`, ODBC/Excel DSNs, linked-server remote mappings).
Sequence: after Phase 1 is proven AND §2b is complete, rotate `tom`'s password and update **every** external consumer **in the same change window**. (If §2b reveals the engine does **not** in fact read `Web.config` but hardcodes/uses a separate `tom` string, then §1's shared-connection premise is wrong and the whole plan needs revisiting — so confirming the DLLs' connection source in §2b is a hard prerequisite, not a detail.)
---
## 5. Phase 2 — remove card-table access (gated on GTIware decoupling)
Dependency: Tom's parallel GTIware workstream (assessment items 17 + 22) gives the card engine its own dedicated low-privilege connection/host. Then: `DENY cc_file`, `cof_payments_header` on `web_glaztech_app`; re-validate the website (which per assessment stores no cards itself) still functions; confirm auto-billing runs on the engine's own login.
---
## 6. Environment, change window, testing gates
- **No staging assumed (H1 — dev tooling + source on the live box).** Plan a **maintenance window with immediate rollback**, or stand up a minimal validation copy first. **No in-place hotfixes without a rollback path.**
- Validate per the assessment's testing gates: DB-login change reverts via `Web.config`; per-page validation that queries still return correct results after the swap; confirm `gt_auto_process` auto-charge still runs on the new login.
- **Take a backup-first posture** on any object whose permissions change; never touch `tom` until the replacement is proven.
## 7. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Missed object permission → site error after swap | Empirical XEvents trace (§2a) + live error monitoring during the staged swap + one-step rollback |
| Card engine breaks (loses `cc_file`) | Phase 1 retains `cc_file`; removal only in Phase 2 after decoupling |
| Rotating `tom` breaks an un-inventoried consumer | Full §2b inventory + same-window update; defer rotation if any consumer is unresolved |
| No staging → prod-only change | Maintenance window + immediate `Web.config` rollback; backups |
| `corp` DB `cc_file` "Invalid object name" anomaly | Resolve before finalizing grants (open follow-up) |
| Second host `192.168.0.147` has its own `tom` Web.config | Include in §2b; migrate/rotate in the same window |
| **Legit cross-DB join** (current × archive/other-office) hard-breaks under DENY | Surface cross-DB dependencies in the §2a trace before applying the DENYs |
| **Dynamic SQL in a proc** breaks ownership chaining → hits DENY | Scan procs for `EXEC`/`sp_executesql`; GRANT tables they reach dynamically |
| **ADO.NET pool not flushed** → stale `tom` connections mask failures | App-pool recycle on every connection-string change (swap and rollback) |
| **Connection-string parser** chokes on special chars in the new password | Alphanumeric-only high-entropy password for the interim login |
| **`tempdb` DENY** breaks `#temp` creation | Do not DENY `tempdb`; `public` keeps local temp; watch `##global`/physical tempdb use |
| **Linked-server catch-all mapping** to remote `sa` | Audit `sys.linked_logins`; set "Not be made" for the new login; test |
| `gt_console_apps.exe` (server-side charging) opens its own `tom` connection | Confirm in §2b; if so it's an external consumer for the rotation window |
## 8. Open questions for Tom / Steve
1. Confirm the website reads `web_security` **only** via `get_web_accesslevel` (so we can DENY direct table SELECT).
2. Does `gt_auto_process` re-submit CVV at charge time? (affects the parallel CVV purge, not this migration, but same window).
3. Will GTIware get its own DB connection/host (Phase 2 prerequisite)? Timeline?
4. Is there *any* test/validation environment, or is the maintenance-window-on-prod path required?
5. Authoritative list of `tom` consumers beyond the website (jobs, the `.147` host, Samsara webhooks, ODBC DSNs)?
## 9. Effort / sequencing summary
- **Inventory (§2):** read-only; ~prerequisite, do first.
- **Phase 1 (§3):** ACG-drivable with client sign-off + a maintenance window; the high-value de-sysadmin step. Reversible.
- **`tom` rotation (§4):** after Phase 1 proven + consumer inventory complete.
- **Phase 2 (§5):** gated on Tom's GTIware decoupling (parallel workstream).
---
## 10. Independent review (Grok 4.3 + Gemini 3 Pro, 2026-06-05)
Both models reviewed this scope and the underlying assessment independently and **CONCUR** with the two-phase approach, the grant/DENY matrix, and the inventory-first / prove-before-touching-`tom` posture. They **agreed** on every correction now folded in above:
- Two-phase keeping `cc_file` in Phase 1 is *required* (not just safer) given the in-process engine; no safer pure-interim exists without moving the engine first.
- **Do not DENY `tempdb`** (breaks `#temp`); **don't assume** non-sysadmin blocks linked servers (audit catch-all mappings); **dynamic SQL** in procs breaks ownership chaining (scan + GRANT); the empirical **XEvents trace is the true gate**.
- Confirmed: losing sysadmin reliably removes `xp_cmdshell`; DB-level `DENY` overrides role GRANTs; the `web_security` EXECUTE-only + DENY pattern works via ownership chaining (dbo-owned, no dynamic SQL, no `EXECUTE AS`).
- **Gemini's unique catches:** the §4 rotation-gating contradiction (now fixed — `tom` is vacated from the worker at the Phase 1 swap, so rotation gates on *external* consumers only); the **ADO.NET pool flush / app-pool recycle** requirement on swap *and* rollback; the **alphanumeric-only password** to avoid legacy connection-string parser failures; cross-DB-join breakage under DENY.
- **Grok's unique catches:** don't overstate Phase 1 ("contained," not "cards safe" — served-office cards still exposed via SQLi until Phase 2); proc-level grep for `cc_file` before the Phase 2 DENY; confirm `gt_console_apps.exe`'s connection source; pair the swap with E1 (old `tom` password lingers in `Web.config` backups).
No disagreement between the two models on any material point — a strong signal the plan is sound, contingent on the §2 inventories.
---
## 11. Recon findings (live, via GuruRMM read-only, 2026-06-05) — MATERIAL changes
Read-only SQL + `Web.config` recon (no data modified) corrected several core assumptions. **These supersede parts of §1-§3 and must be reconciled before any execution.**
### Topology — centralized now; per-site SQL was the OLD layout
- `GTI-INV-SQL` (at the **"INV - Involta"** colo, 192.168.8.62) runs **multiple instances**: a **default instance** (SQL **2008 R2** / 10.50, EOL) with only `qqest` + ReportServer + system; the named instance **`GTI-INV-SQL\GTISQL` on port 3436** (SQL **2012** / 11.0, EOL) holding **all office data** (~57 DBs: `glaz_prod_<office>`, `_archive`, `_web` for alb/boi/brl/corp/den/elp/phx/shp/slc/tuc + PDF stores + `gti_samsara` + `qqest`); and a **third** instance on `192.168.8.62,3430`.
- **The per-site-SQL memory matches the OLD topology** — the **commented-out** Web.config strings show per-office ports/instances (`glaz,3430` tuc … `glaz,3439` brl, `sql1,3436`, `sql3,3430`). It has since been **consolidated** onto the single `:3436` instance with per-office *databases*.
- **The "mesh" = a handful of linked servers** on `:3436` (192.168.0.54; 192.168.0.55 = Sage `mas_gti`; 192.168.8.52 + .212 = backups; 192.168.8.62,3430; `GLAZ\TIMEFORCE`), all data-access enabled. On the default instance every linked-login maps `(default-all) → tom`**`tom` is the cross-system credential.** NOT one-SQL-per-office.
### The website's TRUE footprint (Web.config, ~15 active connection strings — all `user id=tom`, all on `:3436` unless noted)
The site is **not** single-office. As sysadmin `tom` it legitimately connects to:
- **All 10 offices' production DBs** (`glaz_prod`, `glaz_prod_phx/_slc/_elp/_den/_alb/_boi/_brl/_shp/_corp`) + PDF stores (`glaz_pdf`, `glaz_pdf_corp`)
- **Sage accounting** — `mas_gti` @ `192.168.0.55,55181` (DIRECT connection, not just linked)
- **Payroll** — `qqest` via `glaz\timeforce`
- **`msdb`** — `glaztech_jobs``192.168.8.62,3436/msdb` (the DB holding the cleartext domain-admin password + Agent jobs)
- Uses the **operational `glaz_prod_*` DBs directly** — NOT the `_web` databases (no `_web` simplification).
### Implications — §1-§3 are too optimistic
- **§3's "scope to own-office DB; DENY other offices/archives/msdb/accounting/payroll" is contradicted** — the app legitimately uses all of them. A least-privilege login can't DENY what the app connects to.
- **Phase 1's blast-radius win shrinks:** removing sysadmin still kills `xp_cmdshell` RCE + arbitrary-instance control, but the new login would still legitimately reach all offices, accounting, payroll, and `msdb` — most of the estate. Retaining `msdb` means the cleartext domain-admin password in `sysjobsteps` stays reachable unless **E2 (rotate it) is done first** — so E2 is now a hard prerequisite, and we must learn WHAT the website does in `msdb`.
- **The durable fix is architectural** (assessment items 16-17, 22): separate the website's data + decouple — as-built the website is a cross-office hub wired to accounting + payroll + msdb under one sysadmin credential, so a "clean least-privilege login" barely exists.
- **EOL SQL** (2008 R2 + 2012) adds platform risk and limits options.
### Recon limits / still needed
- `SYSTEM` (the agent) is sysadmin on the **default** instance but **NOT** on `:3436` — the authoritative `tom`-role / sysadmin-login / linked-login map on `:3436` needs the `tom` credential (or a sysadmin Windows login).
- **Full network recon PENDING** — Mike is enrolling the site servers in RMM to confirm no per-site SQL remains and map how accounting/payroll/backups/the 0.x + 8.x subnets fit together.
- The §2a object-inventory (XEvents trace) is even more important given the multi-DB, cross-server footprint.
---
*v0.3 — Grok + Gemini corrections incorporated (§10); live RMM recon findings added (§11). **PARKED** by Mike 2026-06-05: the migration is larger than the assessment framed (website is a multi-office + accounting + payroll + msdb hub on one sysadmin credential), and a real network recon is needed to understand how the infrastructure fits together before this can be safely executed. Also still pending Tom/Steve answers to §8. Whole web-app remediation remains parked on #32378.*