<?xml version="1.0"?>
<rss version="2.0"><channel><title>Project Showcase Latest Topics</title><link>https://wtmsp.com/forum/15-project-showcase/</link><description>Project Showcase Latest Topics</description><language>en</language><item><title>Patrol car laptops couldn't resolve the domain over LTE, and the router was to blame</title><link>https://wtmsp.com/topic/33-patrol-car-laptops-couldnt-resolve-the-domain-over-lte-and-the-router-was-to-blame/</link><description><![CDATA[<p>One of the small cities we support has a rugged laptop in each police vehicle, connected through an in-car cellular router. The router brings up a tunnel back to the city network so officers can reach dispatch, records systems and file shares from the car. The tunnel was up, and pinging internal servers by IP worked. Even so, the laptops couldn't resolve anything in the city's internal Active Directory domain. Group Policy failed, domain authentication fell back to cached credentials, and any application that referred to a server by name broke.</p><p>Officers had been working around it for a while, and every fix tried so far had been on the laptops themselves, like hosts files, manual DNS settings and re-joining the domain. None of it stuck.</p><h2>Asking each DNS server directly</h2><p>From a vehicle laptop we ran the same lookup twice. The first went to the DNS server the laptop had been given by DHCP, and the second went straight to a domain controller's address across the tunnel.</p><ul><li>Querying the <strong>domain controller directly</strong> worked perfectly. Domain records, SRV records for the DCs, all of it.</li><li>Querying the <strong>address handed out by DHCP</strong> returned <strong>NXDOMAIN</strong> for anything in the internal <code>.local</code> zone.</li></ul><p>The DHCP-assigned DNS server was the cellular router itself. Its built-in DNS forwarder was answering internal queries on its own instead of passing them up the tunnel. Two lookups, side by side, pointed straight at it.</p><h2>Why the router said no</h2><p>The routers had a <strong>local domain</strong> setting used for naming DHCP clients, and its configured value overlapped the city's internal <code>.local</code> zone. As best we could tell, that made the router's forwarder treat itself as authoritative for those names. Anything not in its own lease table got an immediate NXDOMAIN, and the query never reached the domain controllers. The laptops trusted the answer, and Windows caches negative responses, which made the symptoms even less consistent from one car to the next.</p><h2>What we changed</h2><ol><li>We <strong>changed the routers' local domain setting fleet-wide</strong> through the vendor's cloud management platform, to a neutral value that doesn't overlap the AD domain. Every vehicle router was in the same configuration group, so this was one change pushed to the whole fleet instead of a visit to every car.</li><li>We added AD sites and subnets for the vehicle address ranges. Without them, the domain controllers couldn't map the laptops to a site, so DC location was unpredictable and a laptop could pick a DC that was slow or unreachable over the tunnel.</li><li>We created reverse lookup zones for the vehicle subnets, so the laptops could register PTR records and tools that rely on reverse lookups behaved properly.</li></ol><div class="ipsRichTextBox" data-i-background-color="orange"><div class="ipsRichTextBox__title"><p><strong>Rollout notes</strong></p></div><ul><li>Negative caching made it look like the fix hadn't worked. After the router change, some laptops still failed because they'd cached the NXDOMAIN answers. Flushing the DNS client cache, or just rebooting, cleared it. We put that in the rollout notes so nobody would decide the fix was a dud.</li><li>A few vehicles were parked with the router powered off, so not every router picked up the change at once. They took the new configuration when they next came online. We tracked the stragglers from the management portal instead of assuming we'd hit 100%.</li><li>We almost fixed the wrong layer. Before the side-by-side test, the plan was to push static DNS servers to the laptops through Group Policy. That would have worked on the tunnel and failed badly as soon as a laptop was on any other network. Fixing the router kept DHCP as the single source of truth.</li></ul></div><h2>Where it stands</h2><p>The vehicle laptops resolve internal names reliably over LTE now. Group Policy applies in the field, and domain sign-in works instead of falling back to cached credentials. Domain controllers associate the laptops with the right site, so DC selection is predictable. The fix lives in one place, the router configuration group, so new vehicles get it automatically.</p><p>A couple of things I'll carry forward. Small and mobile routers often run a local forwarder with features like local domains and rebind protection that can get in the way of internal zones, especially <code>.local</code>. Any time a client on the same router platform has odd DNS problems, that setting is the first thing I check. And whenever a new address range shows up, mobile and VPN ranges included, it goes into Sites and Services the day it's created, not the day something breaks.</p>]]></description><guid isPermaLink="false">33</guid><pubDate>Sun, 18 Oct 2026 20:43:46 +0000</pubDate></item><item><title>Building TechBytes on Invision Community in a Debian 13 container</title><link>https://wtmsp.com/topic/32-building-techbytes-on-invision-community-in-a-debian-13-container/</link><description><![CDATA[<p>We wanted somewhere to share what we learn on the job. That means how-tos, project write-ups, and the gotchas that cost us an afternoon, so hopefully they don't cost you one. A static blog felt too one-way, and a chat server buries everything within a week. We wanted a proper forum with search, tags and accounts, which led us to <strong>Invision Community</strong>. It had to run on our own infrastructure, be easy to rebuild, look like ours, and be backed up properly.</p><h2>What it runs on</h2><ul><li>A <strong>Debian 13</strong> LXC container on our Proxmox cluster. It's one service per container, same as everything else we host.</li><li><strong>nginx</strong> and <strong>PHP 8.4</strong> (PHP-FPM) from the Debian packages.</li><li><strong>MySQL 8.4</strong> running in Docker inside the container, with its socket directory bind-mounted so PHP on the host connects over a local Unix socket rather than TCP.</li><li><strong>Redis</strong> for caching and sessions.</li><li>Our existing <strong>reverse proxy</strong> in front, terminating TLS with a <strong>Let's Encrypt</strong> certificate.</li></ul><p>Putting MySQL in Docker was deliberate. We wanted a specific MySQL version that doesn't depend on the distribution's packaging, and upgrades that are a container image tag instead of a package migration.</p><h2>A script instead of a checklist</h2><p>The whole build is one shell script. It creates the container, installs packages, configures nginx and PHP-FPM, starts MySQL with its data on a dedicated volume, creates the database and user, installs Redis, and sets PHP limits for uploads and memory. The only manual part is running Invision's web installer and entering the licence. If the container dies, we can rebuild it and restore the data in well under an hour. Writing the script took longer than doing the install by hand would have, but only the first time.</p><h2>Making it look like ours</h2><p>We themed it to the Harbor Tech Bytes logo colours and set up <strong>separate light and dark logos</strong>, because a logo that looks right on white often disappears on a dark background. The palette is defined once in the theme settings, so headers, buttons and links stay consistent in both modes.</p><h2>Backups</h2><p>A nightly job dumps the database with a consistent single-transaction dump, archives the uploads directory and configuration, and ships both off the container. On top of that, Proxmox backs up the whole container every night, so we can restore either a single database or the entire thing. We've tested the restore, because I don't count a backup as real until I've restored from it.</p><div class="ipsRichTextBox" data-i-background-color="orange"><div class="ipsRichTextBox__title"><p><strong>Where the install fought back</strong></p></div><h3>An installer error that wasn't about the installer</h3><p>The web installer failed at the database step with an error that looked like a problem with the installer itself. It turned out PHP couldn't find the MySQL socket. PHP was looking at the default socket path, and with MySQL in a container the socket only shows up on the host if the directory is bind-mounted and the permissions let PHP-FPM's user reach it. Once we shared the socket directory properly and set the path explicitly in the configuration, the database step passed.</p><p>Then the installer stalled at a <strong>session check</strong>, where it verifies that PHP sessions persist between requests. That one was on the PHP side too. Sessions weren't being saved anywhere the next request could read them. Once PHP had a working, writable session store, re-running the installer got us through.</p><p>Both failures were obvious in the PHP-FPM and nginx error logs and misleading on screen. An installer tells you which step failed, not why, so check the logs before you believe the message.</p><h3>A custom page that threw a server error</h3><p>We built a custom page for the site and it returned a server error instead of content. <strong>Friendly (clean) URLs weren't enabled</strong> in our install, so nothing could resolve the page's pretty URL. Turning on friendly URLs in the admin panel, along with the matching nginx rewrite that routes requests to Invision's front controller, fixed it. The rest of the site's URLs got nicer as a bonus.</p><h3>Behind the proxy</h3><p>Like every app behind a reverse proxy, the forum first saw every visitor as coming from the proxy's address. Configuring nginx to trust the proxy's forwarded headers gave us real client IPs back, which matters for spam protection and moderation.</p></div><p>So now we have a forum that's fast, searchable and ours, running in a single container next to the rest of our infrastructure. There's a rebuild script and a tested restore, branding that holds up in light and dark mode, and you're reading the result. If you're running something like this yourself, my main advice is to check the PHP and nginx logs before you start debugging the installer.</p>]]></description><guid isPermaLink="false">32</guid><pubDate>Sun, 18 Oct 2026 20:42:46 +0000</pubDate></item><item><title>A VPN that had never worked, and the empty setting behind it</title><link>https://wtmsp.com/topic/31-a-vpn-that-had-never-worked-and-the-empty-setting-behind-it/</link><description><![CDATA[<p>One of our municipal clients had a remote-access VPN appliance that, as far as anyone could remember, had never worked properly. Staff in the VPN group would connect and get a portal page with a single remote-desktop link. If they launched the full VPN client instead, it connected and then dropped within about a second. Over the years people had just learned to use the remote-desktop link and live without mapped drives, internal web apps and anything else that needed a real tunnel. When we asked about it, the answer was basically "oh, the VPN's always been like that."</p><p>On paper the design was fine. Domain laptops with a valid machine certificate get a full network tunnel, and everything else gets remote desktop only. We needed to work out why every laptop was being treated as "everything else".</p><h2>Following one person's session</h2><p>We didn't start with overall connection stats. We followed a single user's session from start to finish in the appliance's per-session logs. The access policy ran its steps in order. It authenticated against AD, checked the client's machine certificate, ran an endpoint antivirus check, and then assigned resources. The certificate check failed on every session, on every laptop, every time. The policy then sent the user down the fallback branch that only granted the remote-desktop link, and the client tunnel got torn down because no network-access resource had been assigned. The aggregate numbers just said connections were succeeding. Only the session log showed which step failed and which branch the user ended up on.</p><h2>The laptops were fine</h2><p>The obvious theory was that the laptops didn't have certificates. They did. Every domain laptop had a valid computer certificate, auto-enrolled from the client's internal AD Certificate Services root, current, and with client-authentication usage. I'm glad we checked first, because otherwise we could easily have spent days rebuilding certificate templates that were already working.</p><h2>An empty trust profile</h2><p>The certificate check on the appliance pointed at a certificate-authority profile, which tells it which CA certificates to trust when validating a client certificate. That profile existed and was attached to the policy, but it had <strong>no CA certificate loaded at all</strong>, so it could never validate anything. Years of "the VPN is flaky" came down to that one empty field.</p><h2>What we changed</h2><ol><li>We loaded the internal root CA into the trust profile used by the certificate check.</li><li><strong>We moved the group check to the front of the policy.</strong> While mapping the policy we found a dormant branch. Once the certificate check started passing, a valid machine certificate would have led to a full tunnel without checking VPN group membership on that path. If we'd only fixed the CA, anyone with a domain laptop would have had full access. With the AD group check first, users outside the group never reach the certificate step.</li><li>We softened the antivirus check. A failed AV check used to deny access entirely, which locked people out when their definitions were a day behind. Now it falls back to remote-desktop-only, so the person can still work and the problem shows up in the logs.</li><li>We fixed the public certificate chain. The appliance's public TLS certificate had been installed without its intermediate. Most browsers coped by fetching it themselves, but some clients didn't, and that explained a separate batch of intermittent connection errors. We installed the full chain.</li><li>We documented it all, including the policy flow as a diagram, which CA the check trusts, where the certificates come from, and when each one expires.</li></ol><div class="ipsRichTextBox" data-i-background-color="orange"><div class="ipsRichTextBox__title"><p><strong>Things that slowed us down</strong></p></div><ul><li>On many appliances of this type, saving a policy edit doesn't make it live until you explicitly apply the policy. We now apply, then confirm the change actually took effect in the next session log instead of trusting the editor.</li><li>Users with existing sessions kept the old behaviour until their session was killed. We cleared the test users' sessions between every test.</li><li>The fix depends on auto-enrolment staying healthy. A laptop that sits off the network long enough for its certificate to expire drops back to remote-desktop-only. That's the correct behaviour, but it's something to watch now, so we added certificate expiry on long-offline devices to our regular checks.</li></ul></div><h2>Where it stands now</h2><p>Domain laptops in the VPN group get a full tunnel with mapped drives and internal apps, for the first time since the appliance went in. Users outside the group are denied correctly and consistently, instead of by accident because of a broken check. The access hole that would have opened the moment someone "fixed" the certificate check got closed before it ever went live. The help desk also stopped getting a whole category of tickets.</p><p>The part I'd pass on is about fixing a check that has always failed. Before you turn it on, read every branch it feeds. A check that always fails hides whatever is sitting on its success path, and nobody has looked at that path in years.</p>]]></description><guid isPermaLink="false">31</guid><pubDate>Sun, 18 Oct 2026 20:41:46 +0000</pubDate></item><item><title>We moved our whole hosting footprint onto our own Proxmox cluster</title><link>https://wtmsp.com/topic/30-we-moved-our-whole-hosting-footprint-onto-our-own-proxmox-cluster/</link><description><![CDATA[<p>Like a lot of small MSPs, our hosting grew one server at a time. We had a VPS for the ticketing system, another for the monitoring stack, and one for the RMM because it wanted its own box. The SIEM got another because it wanted a lot of RAM, a couple more ran WordPress sites and small internal apps, and the documentation wiki and password manager got squeezed in wherever there was room. Each decision made sense at the time. Together they came to about <strong>$334 a month</strong> across two providers, with backups, patching and firewall rules done slightly differently on every box. Whenever something broke, the first question was always "which server is that one on again?"</p><p>We also had retired hyperconverged hardware sitting around, a four-node chassis that came out of a customer refresh and was perfectly healthy. So the plan was to move everything onto hardware we own, in a colo rack, while our clients' tickets, alerts and logins kept working the whole time.</p><h2>The platform</h2><p>We wiped the four nodes and installed <strong>Proxmox VE</strong> with <strong>Ceph</strong> across all of them, so any VM or container can run on any node and survive a node failure. The vendor's own hypervisor stack went away. Proxmox gave us clustering, live migration and built-in backups, with no licensing to keep track of.</p><h2>How the network is laid out</h2><ul><li>A <strong>VyOS</strong> VM is the router and firewall. It holds a public <strong>/28</strong> that the colo routes to us.</li><li>Services live on an internal VLAN behind it. Management interfaces sit on a separate VLAN that isn't reachable from the internet at all.</li><li><strong>Nginx Proxy Manager</strong> takes ports 80 and 443 on one public IP and terminates TLS for every HTTP service.</li><li>The few services that really need their own address get a <strong>1:1 NAT</strong> from a dedicated public IP on the /28. That's mainly the RMM, which has its own agent ports, and the SIEM, for agent enrolment and event ports.</li></ul><h2>One service per container</h2><p>Each service got its own <strong>LXC container</strong>, or a full VM where the software insisted (mainly the RMM and the SIEM). Monitoring (LibreNMS and Grafana), the wiki, the password manager, the ticketing system and each small app all became separate containers. Snapshots, restores and resource limits are now per-service, and a bad PHP upgrade in one app can't take another one down with it.</p><h2>Moving things without anyone noticing</h2><p>We had one rule for every migration. Prove the new origin works before you touch DNS.</p><ol><li>Build the new container and restore data from the old server.</li><li>Test it by pointing a hosts-file entry, or a curl with an explicit resolve, at the new address. That way you're exercising the real hostname and certificate.</li><li>Do a final data sync during a short write freeze where needed (the ticketing and password manager databases).</li><li>Flip DNS. We'd set low TTLs a day ahead, so most clients moved within minutes.</li><li>Leave the old server running, read-only where possible, until traffic to it stops.</li></ol><p>Nothing was deleted on cutover day. We shut the old servers down and kept <strong>provider snapshots</strong> for rollback, and only destroyed each one once its service had run cleanly on the new cluster for a while. Those snapshots cost a few dollars, and it was very nice knowing we could roll back.</p><div class="ipsRichTextBox" data-i-background-color="orange"><div class="ipsRichTextBox__title"><p><strong>What tripped us up</strong></p></div><ul><li><strong>Apps that didn't trust the proxy.</strong> Several applications, LibreNMS included, generated wrong redirect URLs or treated every visitor as coming from the proxy's IP until we told them which proxies to trust. Each app has its own setting for this, and checking for it is now part of every migration.</li><li>Old addresses were hard-coded all over the place. Webhooks, alert transports and integrations still pointed at the old servers' IPs or hostnames. Monitoring alerts went quiet for a while because a webhook target was still the old box, which is not a great feeling for an MSP. Now we grep every config and database for old IPs and hostnames before decommissioning anything.</li><li>The RMM expected a certificate covering several subdomains, and a per-host certificate caused agent connection errors. A wildcard certificate fixed it.</li><li>Adding an interface to the VyOS VM changed the order the guest saw its NICs, which swapped our inside and outside networks on reboot. We pin interfaces to MAC addresses in the router config now.</li><li>Moving lots of data over SSH tripped fail2ban on the old servers, so we were locking ourselves out. Batching commands into fewer connections and whitelisting our office IP sorted it.</li></ul></div><h2>Where it landed</h2><p>Monthly hosting went from about <strong>$334 to about $112</strong>, and most of what's left is leftover services we're still winding down. The goal is close to zero beyond colo and transit. Every service is now backed up, monitored and patched the same way, on the same schedule. Restores are faster too. A container snapshot rollback takes seconds, and backups are local instead of being pulled across the internet. Clients didn't see any outages during the move.</p><p>If you're planning something similar, start with the inventory. Our first list of what ran where was missing about a third of the cron jobs, webhooks and little scripts living on those servers. And go in knowing that owning the hardware means owning its problems. We got the cost savings and the control, and we also took on power, spare parts and hardware monitoring. I'd rather plan for a node dying now than figure it out the night it happens.</p>]]></description><guid isPermaLink="false">30</guid><pubDate>Sun, 18 Oct 2026 20:40:45 +0000</pubDate></item></channel></rss>
