IT Manager (Senior System Administrator)Infrastructure · Networks · Security · Automation
I build infrastructure that runs itself, and lead the team that keeps it honest. Twenty-one years with one city, from its government TV station to running two hyperconverged clusters, plus a self-hosted platform at home that I treat like production.
A fix without a cause is a future outage. I trace symptoms to the mechanism before touching anything.
02
Back up, validate, then apply
Copy the config, test it offline, change only what's affected, then check the live result, not the exit code.
03
Least privilege by default
Read-only roles, scoped tokens, socket proxies and policy gates, so a compromise in one place stays there.
04
Write it down
Decisions are recorded with the failure or risk that drove them, so the next person doesn't relearn it.
Experience
One city, five roles.
Each role at the City of Titusville grew out of the last: from broadcast systems, to end-user support, to designing the city's cloud and datacenter, to leading the team. Since 2025 I also run technology for a small healthcare practice.
May 2023 — Present
IT Manager (Senior System Administrator)
City of Titusville · Titusville, FL
Lead a six-person IT team supporting 550+ users, 1,000+ devices and 15+ facilities, sharply reducing staff complaints through a stronger customer-service focus.
Partnered with the IT Director to introduce ITIL-based processes, improving service quality, service-level objectives and incident response.
Administer two hyperconverged clusters: the city's Hyper-V failover cluster on Storage Spaces Direct (50+ production VMs) and the police department's VMware vSphere cluster on Dell VxRail with vSAN (about 20 more VMs).
Administer multi-domain Active Directory and Microsoft 365.
Strengthened security and CJIS compliance with next-generation firewalls, intrusion prevention, Entra Privileged Identity Management and Active Directory hardening.
Automate software deployment, patching and reporting with PowerShell and MECM, and built self-updating infrastructure documentation.
Jul 2025 — Present
Technology Lead (Part-Time)
WholeMotion Inc. · Titusville, FL
Manage all IT for a licensed massage therapy practice, from selecting and setting up systems to day-to-day support.
Built automated data integrations and reporting across business systems with PostgreSQL, Python and AI-assisted development, streamlining payroll and bookkeeping.
Protect client health information through least-privilege access and data minimization.
Dec 2021 — May 2023
System Administrator
City of Titusville
Deployed hyperconverged infrastructure (Dell VxRail and Hyper-V), consolidating 100+ servers and an IBM AS/400 onto fewer than 12 hosts.
Managed 120+ virtual machines through upgrades, patching and CJIS compliance.
Migrated a third of users to a new Active Directory child domain with PowerShell automation.
Represented IT in leadership meetings; onboarded and mentored new technicians.
Sep 2019 — Dec 2021
PC Network Specialist
City of Titusville
Designed the city's Microsoft 365 environment and migrated hundreds of mailboxes to Exchange Online.
Automated OS deployment with SCCM, cutting build time from hours to under an hour.
Deployed Microsoft LAPS to secure local administrator accounts across endpoints.
May 2016 — Sep 2019
Network Technician
City of Titusville
Provided desktop and end-user support through migrations and upgrades; introduced a city-wide IT request system.
Built SharePoint farms for city government, Emergency Operations and Disaster Recovery teams.
Supported a core network switch replacement and an Exchange Server upgrade.
Jan 2005 — May 2016
Media Specialist
City of Titusville · Government TV station
Managed the city's government TV station, including production, broadcast and its editing systems.
Leadership
City of Titusville2023 — now
Service management and team leadership
Turned a reactive help desk into a measured service.
Introduced ITIL-based incident and request processes with the IT Director.
Coach a six-person team; complaints dropped as customer service became the focus.
Represent IT in leadership meetings and onboard new technicians.
ITILTeam leadershipMentoringProcess improvement
City of Titusville2026
AI usage policy for the IT department
A practical policy for which AI tool does what, and how AI-built work stays maintainable.
Sets a default assistant inside existing licensing, a separate tool for agentic build and troubleshooting work, and written approval for specialist agent frameworks.
Requires reuse of existing hosting and rules out "black box" projects: whoever builds it must be able to explain and support it.
GovernanceAI adoptionPolicy writing
Infrastructure
Two clusters, no downtime.
The city's datacenter: two hyperconverged clusters, the documentation that keeps them knowable, and the dependency maps that show what breaks when something does.
City of Titusville2021 — now
Datacenter consolidation and two hyperconverged clusters
Retired a sprawl of physical servers: 100+ servers and an IBM AS/400 now run on fewer than 12 hosts.
Deployed Dell VxRail and Hyper-V, then migrated and right-sized 120+ virtual machines.
Kept CJIS compliance and patch cadence intact through the move.
I run both clusters: the city's 4-node Hyper-V failover cluster on Storage Spaces Direct with 50+ production VMs, and the police department's VMware vSphere cluster on Dell VxRail with vSAN, running about 20 more.
Both are changed under a strict no-downtime rule, with the police cluster held to CJIS requirements.
Mapped the ERP and timekeeping systems from desktop to database as one dependency chain.
Found that both systems rest on the same clustered SQL backend, a shared blast radius neither vendor's documentation showed.
Confirmed what is internal-only versus published, correcting an assumption about exposure.
SQL ServerJava / WildFlyERPRisk analysis
City of Titusville2026
Self-maintaining infrastructure documentation
Documentation that updates itself from the live environment instead of going stale the week it's written.
Every host, VM and service is a Markdown file with a machine-readable data block, refreshed by idempotent PowerShell collectors that never overwrite human notes.
Where remote access into guests was restricted, read true guest identities from the hypervisor itself, which exposed several VMs whose labels didn't match what they actually were.
Rebuilt a 42-database inventory from backup naming conventions when the database tier was behind an access boundary.
Dependency-free JavaScript front end that loads the whole dataset from flat files: no backend, no build step.
PowerShellHyper-VJSONVanilla JSDocs as code
Network & security
Many vendors, one perimeter.
Firewalls and switching from several vendors over my career, the city's UniFi wireless, camera and door-access estate that I installed, and the identity controls that sit on top.
Firewalls I've configured
Palo Alto Networks
Sophos
Cisco ASA
WatchGuard Firebox
Switching I've configured
Cisco
Dell Networking OS6 · OS10
UniFi
UniFi I installed
Access points
Cameras
Door access control electronic locks
Switches
City of Titusville
Wireless, cameras and door access on UniFi
Put in the city's UniFi access points, cameras and door access control, plus UniFi switches.
Wi-Fi, video surveillance and electronic door locks for city facilities.
Strengthened security and CJIS compliance with next-generation firewalls and intrusion prevention.
Next-generation firewall policy and intrusion prevention at the edge.
Supported the core network switch replacement earlier in my career.
NGFWIDS / IPSVPNVLANsCJIS
City of Titusville2023 — now
Identity and access hardening
Moved privileged access to just-in-time and closed delegation risks across the cluster.
Rolled out Microsoft Entra Privileged Identity Management for admin roles.
Audited Kerberos delegation and service-principal hygiene, and tightened it on every cluster node with zero operational impact.
Moved a third of users into a new AD child domain with PowerShell.
Active DirectoryEntra IDPIMKerberosCJIS
Microsoft & endpoints
From mailbox to desktop.
Microsoft 365, System Center for deployment and ticketing, and the collaboration platforms the city runs on.
City of Titusville2019 — 2021
Microsoft 365 design and Exchange Online migration
Designed the city's tenant and moved hundreds of mailboxes to the cloud.
Tenant design, identity sync and mail flow for a government organization.
Deployed Microsoft LAPS so no two endpoints share a local admin password.
Microsoft 365Exchange OnlineEntra IDLAPS
City of TitusvilleOngoing
SCCM, from setup to automated builds
Set up SCCM years ago; OS builds went from hours to under an hour.
Stood up System Center Configuration Manager (now MECM) for the city.
Automated OS deployment with task sequences, then software deployment and patching.
Traced a stalled patch pipeline to the update server by its error code instead of re-scanning and hoping.
MECM / SCCMWSUSPowerShellApp packaging
City of TitusvilleRecent
Ticketing on System Center Service Manager
Set up System Center Service Manager as the IT ticketing system.
Part of the same System Center family as SCCM.
Earlier, launched the city-wide IT request system that replaced ad-hoc calls.
SCSMSystem CenterITSM
City of Titusville2016 — 2019
Collaboration platforms for emergency operations
Built the SharePoint farms that city government, Emergency Operations and Disaster Recovery teams worked from.
Supported the Exchange Server upgrade.
SharePointExchange
Software & code
Tools that save the typing.
Small applications built for real chores. Where AI did the typing, I set the design, the guardrails and the definition of done.
City of Titusville2026
Employee IT surplus store
An internal store where staff buy retired city computers. Bulk import asks Dell what each machine is, so nobody retypes model details for every unit.
Upload the asset spreadsheet (CSV or Excel). Every service tag is looked up in Dell's TechDirect API, two at a time with a cached OAuth token, and Dell's model and description win over whatever the sheet said.
CPU, RAM and storage, which that API doesn't return, fall back to a built-in catalog of common business models.
Every import lands as a draft for review; existing items are matched and updated by service tag.
No payment data collected: a work-email code holds the item, payment is in person, and pickup notices go out by email.
One container on open-source parts. Each completed sale is written to a CSV ledger.
A fragile script became a scheduled, config-driven tool.
Rebuilt the ERP client-module update process in PowerShell with error handling and a one-click GUI.
Config-driven and scheduled, replacing hard-coded scripts and fixed sleeps.
PowerShellGUIScheduling
excerpt · surplus store
/**
* Live lookup against Dell's TechDirect Asset Entitlement API
* (OAuth2 client-credentials, then GET .../asset-entitlements?servicetags=...).
* Confirmed response shape for a real tag:
* [{ serviceTag, invalid, productCode, productId, productFamily,
* productLineDescription, productLobDescription, systemDescription,
* shipDate, entitlements: [...] }]
*
* This API answers "what model/warranty is this tag", not "what CPU/RAM/
* storage does it have" — Dell doesn't expose full build specs by tag on
* this endpoint, so callers should still fall back to the offline
* dell-catalog guesses (by model name) to fill in CPU/RAM/storage.
*
* Every call is best-effort: on any failure this returns null (or omits
* the tag from lookupMany's result map) rather than throwing, so a bulk
* import never gets blocked by one bad tag or a network hiccup.
*/
let cachedToken = null; // { value, expiresAt }
function apiConfigured() {
const d = config.dell;
return Boolean(d.clientId && d.clientSecret && d.tokenUrl);
}
async function getToken() {
if (cachedToken && cachedToken.expiresAt > Date.now() + 30_000) return cachedToken.value;
const { clientId, clientSecret, tokenUrl } = config.dell;
const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: 'client_credentials' });
const res = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
signal: AbortSignal.timeout(config.dell.timeoutMs),
});
if (!res.ok) throw new Error(`Dell token request failed: ${res.status}`);
const json = await res.json();
cachedToken = { value: json.access_token, expiresAt: Date.now() + (json.expires_in || 3600) * 1000 };
return cachedToken.value;
}
// … categoryFromFamily(): maps Dell's product-family path to a store category
/** One service tag -> {model, description, family, category, shipDate} or null. Never throws. */
async function lookupByServiceTag(tag) {
if (!apiConfigured() || !tag) return null;
try {
const token = await getToken();
const url = `${config.dell.apiBase}/asset-entitlements?servicetags=${encodeURIComponent(tag)}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
signal: AbortSignal.timeout(config.dell.timeoutMs),
});
if (!res.ok) return null;
const arr = await res.json();
const row = Array.isArray(arr) ? arr[0] : null;
if (!row || row.invalid) return null;
return {
model: titleCaseModel(row.productLineDescription || ''),
description: (row.systemDescription || '').trim(),
family: row.productFamily || '',
category: categoryFromFamily(row.productFamily),
shipDate: row.shipDate || '',
};
} catch (e) {
console.error(`[dell] lookup failed for ${tag}:`, e.message);
return null;
}
}
/**
* Looks up many tags with a small worker pool so a large import doesn't
* fire hundreds of requests at once or serialize one-at-a-time for minutes.
* Returns a Map<tag, result|null>; tags Dell doesn't recognize are simply
* absent-or-null so callers fall back to the offline catalog for them.
*/
async function lookupMany(tags, { concurrency = 2 } = {}) {
const unique = [...new Set((tags || []).filter(Boolean))];
const results = new Map();
if (!apiConfigured() || unique.length === 0) return results;
let cursor = 0;
async function worker() {
while (cursor < unique.length) {
const tag = unique[cursor++];
results.set(tag, await lookupByServiceTag(tag));
}
}
await Promise.all(Array.from({ length: Math.min(concurrency, unique.length) }, worker));
return results;
}
/**
* Imports rows into the database. `rows` come from parse() (optionally
* edited by the admin first). Matches existing items by service_tag when
* present, otherwise always creates a new draft item.
*
* When the Dell API is configured (DELL_CLIENT_ID/SECRET), every unique
* service tag in the batch is looked up first (a handful of parallel
* requests, not one big serial wait) so the real model/description Dell has
* on file wins over whatever the spreadsheet's "Product" column said. Blank
* CPU/RAM/storage — which that API doesn't return — still falls back to the
* offline catalog guess by model name, same as when the API isn't set up.
*/
async function commit(rows, filename) {
// … open an import-batch record for this upload …
const dellResults = dell.apiConfigured()
? await dell.lookupMany(rows.map((r) => r.service_tag))
: new Map();
let dellMatched = 0;
let created = 0, updated = 0, skipped = 0;
for (const r of rows) {
if (!r.title && !r.model && !r.service_tag) { skipped++; continue; }
const dellInfo = r.service_tag ? dellResults.get(r.service_tag) : null;
if (dellInfo) dellMatched++;
const existing = r.service_tag ? rawDb.get('SELECT * FROM items WHERE service_tag = ? AND service_tag != ?', [r.service_tag, '']) : null;
const fields = {
title: r.explicitTitle || (dellInfo && dellInfo.model) || r.title,
make: r.make || (dellInfo ? 'Dell' : ''),
model: (dellInfo && dellInfo.model) || r.model,
cpu: r.cpu,
ram: r.ram,
storage: r.storage,
category: r.category || (dellInfo && dellInfo.category) || '',
form_factor: r.form_factor,
service_tag: r.service_tag,
serial: r.serial,
description: r.description || (dellInfo && dellInfo.description) || '',
dell_matched: dellInfo ? 'dell-api' : '',
price_cents: r.price_cents,
quantity: r.quantity,
status: 'draft',
source: 'import',
import_batch_id: batchId,
};
if (existing) {
items.update(existing.id, fields, { autofill: true });
updated++;
} else {
items.create(fields, { autofill: true });
created++;
}
}
// … save per-batch counts, including how many tags Dell matched …
}
Lightly trimmed where marked “…”. Credentials and API endpoints come from configuration, never from the code.
WholeMotion · Technology Lead (Part-Time)
A small practice, run like an enterprise.
Every system a licensed massage therapy practice runs on, plus a private data platform that reconciles its books automatically and keeps health information out of reach.
WholeMotion2025 — now
Private business data platform
Practice management, accounting, payroll and two payment processors reconciled in one database, with no manual exports.
PostgreSQL with a land-raw, transform-later design: source records stored verbatim, cleaned in views, so mismatches can be debugged without re-pulling data.
Idempotent, resumable ingestion keyed on content hashes and watermarks, so every sync is safe to re-run.
Integrated a system that has no public API through supervised browser automation with two-factor sign-in.
A read-only reporting site with a "synchronize" button that queues work for a separate worker, so the web tier never holds credentials.
Payroll reconciliation designed into the chart of accounts, so it balances automatically every run.
Compliance built into the data flow, not bolted on afterwards.
Direct identifiers dropped at ingest; only internal IDs are stored.
Client details sit in an isolated schema, joined only where a report truly needs them.
The web app connects as a read-only role with no path to raw or sensitive data.
PHILeast privilegeData minimizationAudit trail
WholeMotion2025 — now
An AI agent that can extend itself, safely
I direct an always-on AI agent that deploys its own services without ever holding host-level power.
The agent can edit its own container definitions but can only request a deployment; a separate control service checks every change against a policy first.
The policy blocks host mounts, foreign networks and privilege-granting options.
Through that path it has shipped a browser service, a reporting site and a sync worker.
Claude CodeDockerPolicy as codePython
WholeMotion2025
A small business, stood up end to end
Selected, configured and now administer every system the practice runs on.
Domain, DNS, website, business phone.
Segmented, firewalled office network and Wi-Fi that isolates regulated systems.
Physical door access, practice management, accounting, payroll and card processing.
NetworkingAccess controlVoIPSaaS administration
Home lab
Every request, one front door.
About 50 containers on one GPU host, run like production: a single TLS edge, single sign-on in front of every app, tested backups and documentation as code.
Swipe sideways to see the whole diagram →
Off to the side: nightly backups plus encrypted off-site snapshots, and a git-backed wiki that documents all of it.
Platform
Home labAlways on
Production-grade platform on one GPU host
About 50 containers in 9 Compose projects on a Hyper-V virtual machine with 35 vCPUs, 147 GiB of RAM and a passed-through NVIDIA RTX A1000.
GPU passed through with Discrete Device Assignment, then exposed to exactly the services that need it.
Compose files are the source of truth: secrets split from tunables, drift detected by comparing live containers to the files, updates that restart only what changed.
Only a read-only proxy and a policy-gated deployer touch the Docker API.
58 hostnames through one HAProxy edge with two-factor SSO and automatic certificates.
Map-driven routing: adding a service is one line and a backend.
Forward authentication to Authelia written in Lua, with separate trust zones for home and business tools.
Wildcard certificates renewed by DNS challenge and hot-reloaded with no downtime.
Every config change validated in a throwaway container before it touches the live proxy.
HAProxyLuaAutheliaLet's EncryptCloudflare
Home lab2026
AI-assisted operations
I run the platform with an always-on AI agent under guardrails I designed.
The agent reaches DNS, the proxy and the firewall only through a policy-checked executor, never raw root.
Secrets come from a password vault at run time and are never copied into scripts.
Claude CodeProton Pass CLIGuardrails
Security & family
Home lab2026
Family network protection with Zero Trust
Strict, tamper-resistant filtering on family devices, and an on-demand tunnel home for me.
Cloudflare Gateway DNS policies scoped by identity: adult content, ads and trackers blocked, SafeSearch and YouTube Restricted Mode enforced, bypass routes closed.
A ~67,000-domain ad and tracker blocklist synced into Gateway daily by a small diff-based job.
A WireGuard profile that sends all of my traffic home when I'm on untrusted Wi-Fi.
Cloudflare Zero TrustWARPWireGuardUniFiPython
Home lab2026
Self-hosted remote support and game servers
My own remote-support relay for family devices, and Minecraft servers for the kids.
RustDesk rendezvous and relay servers, so support sessions never ride a third-party relay.
Bedrock servers managed from one panel, each on its own hostname.
Home Assistant on its own VM, published through the same ingress.
RustDeskCrafty ControllerHome Assistant
Home lab2026
Media platform with GPU transcoding
Streams to 7 household accounts on phones, TVs and laptops, with hardware transcoding on the same GPU that batch-optimizes the collection.
NVIDIA NVENC transcoding, verified with live test encodes after every change.
A restricted child account sees only curated, age-appropriate educational content.
JellyfinNVENCTdarrGPU
Data & resilience
Home lab2026
2.7 TB recovered, with zero false positives
Took a 13 TB volume from 98% full to healthy without deleting a single file that mattered.
Root cause first: an ingest process was copying files instead of hardlinking them.
Size index plus partial-content hashing matched 2,120 byte-identical duplicates (3.31 TB).
Item-level comparison caught 8 of 10 "duplicate" folders that actually held unique content.
PythonXFSHashingVerification
Home lab2026
Backups and disaster recovery
Nightly local backups plus encrypted off-site snapshots, with a tested restore.
Database dumps, every app's configuration and the documentation repository, on a separate volume.
Encrypted, deduplicated off-site copies with daily, weekly and monthly retention.
A rebuild runbook gives the order to stand everything back up from bare metal.
resticpg_dumpsystemd timersRunbooks
Home lab2026
Documentation as code
A wiki of 36 reference pages written in Markdown, version-controlled in git and synced both ways.
A publish script commits, rebases, pushes and forces an immediate import.
Pages name the vault entry for a credential, never the secret itself.
Wiki.jsGitGraphQLMarkdown
Problems solved
Root cause, not workaround.
A sample from the change log. Each one closed with the actual cause found and the fix verified against live state, not just a clean exit code.
When
Symptom
Root cause
Fix
2026-10
Game server reachable on the LAN, invisible from the internet
Three stacked faults: a new default transport ignored the configured port, replies left by the wrong network, and a disabled discovery flag stripped the server's status reply
Fixed each in turn, verified from the public address after every change
2026-10
Hardware transcodes failing
A container recreated outside its Compose file had silently lost its GPU reservation
Recreated from the canonical file; verified with a live NVENC test encode
2026-09
All services down for a minute during SSO rollout
The Lua forward-auth client needed modules that weren't present, and a missing one is fatal
Added the modules and made throwaway-container validation mandatory before every reload
2026-09
Internet clients silently failing through the proxy
On a dual-homed container the internal bridge owned the default route, so replies were NATed and dropped
Pinned the default route to the LAN interface; verified the routing table inside the container
2026-09
Media volume 98% full
An ingest process was copying instead of hardlinking, and staging folders were never cleaned up
Hash-verified deduplication; 2.7 TB recovered with zero false positives
2026
Domain VM showing a "Public" network profile
A public resolver listed ahead of internal DNS was winning the race during network-location detection
Reordered resolvers to keep both requirements, then re-triggered detection without a reboot
2026
Patch scans failing across servers
Error 0x80244022: HTTP 503 from the internal update server, not the clients
Proved it with a controlled, temporary bypass, then restored the governed patch path
2026-09
Vault CLI logged out between sessions
The default kernel-keyring key dies with the login session