The Alarm Bell Rings: Understanding the Scope of the Problem

It was a Monday morning, like any other, until the security alert landed in my inbox. The subject line was terse: “Critical Vulnerability Detected – Out-of-Support .NET Runtimes.” My stomach did a little flip. I knew, vaguely, that we had some older .NET Core applications scattered across our legacy IIS servers. “Legacy” is a kind word for environments that have grown organically over a decade, with different teams, different deployment methods, and varying levels of documentation. The thought of identifying, tracking, and ultimately retiring every single out-of-support .NET runtime felt like trying to find needles in a haystack – if the haystack was on fire.
The immediate problem was clear: unpatched runtimes are an open invitation for exploits. But the practical challenge was far more daunting. How do you even begin to audit an environment like that? Which applications were affected? Who owned them? And, most importantly, how do you remove something without bringing down mission-critical services that nobody fully understands anymore? This article isn’t just about the technical steps; it’s about the journey of tackling a daunting infrastructure challenge, one runtime at a time, and emerging with a cleaner, more secure environment.
The Alarm Bell Rings: Understanding the Scope of the Problem
The initial security report was a wake-up call, but also a starting point. It flagged specific versions of .NET Core that had long since reached end-of-life. These were versions where Microsoft no longer provided security updates, making them significant liabilities. The report gave us a list of servers, but not the specific applications or their owners. This meant the first step wasn’t about fixing anything, but about truly understanding the monster we were facing.
Our environment was a sprawling mix of Windows Server 2012 R2, 2016, and some newer 2019 instances, all running IIS. Applications ranged from internal dashboards nobody touched to public-facing customer portals. Each server could host dozens of sites, and each site could potentially be using one of the offending .NET runtimes. The sheer scale was overwhelming. I remember thinking, “This isn’t just a technical task; it’s an archaeological dig.”
Why Out-of-Support Runtimes Are a Silent Threat
Beyond the immediate security vulnerability, out-of-support runtimes represent significant technical debt. They hinder future upgrades, make troubleshooting harder, and complicate compliance efforts. Imagine trying to get a new feature deployed, only to find the underlying runtime is so old that modern development tools won’t even support it. It’s a drag on productivity and innovation, quietly accumulating risk until a security alert forces your hand.
The Grand Audit: Unmasking Hidden Runtimes
With the scope defined and the risks understood, it was time to put on our detective hats. The core of this challenge was discovery. We needed to identify every single application running an out-of-support .NET runtime and, crucially, map it back to its specific IIS site and application pool. This required a methodical, two-pronged approach.
Hunting the `runtimeconfig.json` Files
The secret sauce for identifying .NET Core and .NET 5+ applications lies in the `*.runtimeconfig.json` file. This small JSON file, typically found in the root of a published application, specifies the target framework. For example, a file might contain "framework": { "name": "Microsoft.NETCore.App", "version": "3.1.0" }, instantly telling us that this application targets .NET Core 3.1 – a version that’s definitely out of support.
Our first step was to script a search across all our IIS servers. PowerShell was our best friend here. We used `Get-ChildItem -Recurse -Filter *.runtimeconfig.json` to scour common deployment paths and application folders. The output was a raw list of file paths, thousands of them. It was a lot of noise, but within that noise were the critical clues.
We then enhanced the script to parse these files, extracting the `version` field. This gave us a clear picture of which versions were present and, more importantly, which ones were dangerous. This initial sweep revealed far more problematic runtimes than we had anticipated, confirming our “haystack on fire” analogy.
Mapping Runtimes to IIS Applications
Finding a `runtimeconfig.json` file tells you a runtime exists, but it doesn’t tell you which IIS application is actually *using* it. This mapping was critical for two reasons: identifying the application owner for communication, and understanding the potential impact if we were to retire a runtime. This required correlating the file paths from our first step with the physical paths configured in IIS.
Again, PowerShell came to the rescue. We combined our `runtimeconfig.json` script with `Get-Website` and `Get-WebApplication` cmdlets to iterate through every IIS site and application pool. For each, we extracted its physical path and compared it to the paths where we found the `runtimeconfig.json` files. This allowed us to build a comprehensive inventory: Server Name, IIS Site Name, Application Pool, Application Path, and most importantly, the Out-of-Support .NET Runtime Version.
The output was a beautiful, albeit slightly terrifying, spreadsheet. It became our single source of truth, detailing every offending application. This process wasn’t just about technical discovery; it was about laying the groundwork for a systematic remediation plan.
Strategy & Surgical Strikes: Prioritizing and Planning Retirement
With our inventory in hand, the next phase was crucial: planning the attack. We couldn’t just yank runtimes; that would be a recipe for disaster. We needed a strategy that balanced risk, effort, and impact.
Prioritization Matrix: Risk vs. Effort
Not all legacy applications are created equal. We categorized each identified application based on:
- Criticality: Is it public-facing? Mission-critical? Or an obscure internal tool?
- Impact: What would happen if it went down?
- Effort to Upgrade/Retire: Was it a simple runtime update, or did the application codebase itself need significant modernization?
- Owner Engagement: How responsive were the application owners to addressing the issue?
This matrix helped us prioritize. Low-risk, easy-to-fix applications went first. High-risk, complex applications required more planning, dedicated team resources, and thorough communication with stakeholders.
The Upgrade or Retire Dilemma
For each identified application, we had two main paths: upgrade or retire. Many applications, especially those on .NET Core 3.1, could often be upgraded to a supported Long-Term Support (LTS) version like .NET 6 or .NET 8 with relatively minor code changes. This usually involved updating the target framework in the project files and re-publishing. This was our preferred option as it maintained functionality while improving security.
However, some applications were so old, so poorly maintained, or so fundamentally dependent on outdated libraries that an upgrade wasn’t feasible. For these, the discussion shifted to retirement. This meant either sunsetting the application entirely, migrating its functionality to a new, modern platform, or rebuilding it from scratch. This was a longer-term play, but sometimes the only viable option for truly egregious technical debt.
The Safe Extraction: Removing Runtimes Without Breaking Things
This was the moment of truth. After all the discovery and planning, the actual removal of out-of-support .NET runtimes felt like disarming a bomb. The fear of breaking production was palpable. Our approach was highly cautious:
- Test Environments First: Every removal or upgrade was first piloted in a development or staging environment.
- Full Backups: Before any changes on production, full system backups and application backups were taken.
- Rollback Plans: Clear, documented rollback procedures were in place for every change. This included knowing how to quickly reinstall the old runtime if an unexpected issue occurred.
- Scheduled Windows: All production changes were performed during planned maintenance windows, with adequate monitoring in place.
- Surgical Uninstallation: We meticulously uninstalled only the specific out-of-support .NET SDKs and runtimes using the official installers or `dotnet` CLI commands, ensuring we didn’t accidentally remove a dependency for a still-supported application.
- Post-Removal Verification: After removal, we rigorously tested the affected applications and monitored server logs for any anomalies.
The process was iterative. We’d tackle a batch of low-hanging fruit, learn from it, refine our scripts and procedures, and then move onto more complex cases. Each successful retirement felt like a small victory, a chip away at a mountain of technical debt.
A Cleaner Slate and a Smarter Future
The journey to track and retire out-of-support .NET runtimes across our legacy IIS servers was a marathon, not a sprint. It was a stark reminder of how technical debt can silently accumulate, posing significant security and operational risks. What started as a daunting security alert transformed into a comprehensive infrastructure audit that significantly improved our overall security posture and operational efficiency.
Beyond the immediate fixes, this exercise instilled some invaluable lessons. It highlighted the critical importance of a robust asset inventory, continuous monitoring for end-of-life software, and the need for clear ownership of applications. It also underscored the power of scripting and automation in tackling large-scale infrastructure challenges. While the legacy systems will always present their unique challenges, we now have a much clearer picture, better tools, and a more proactive mindset for managing them. Our IIS servers are breathing a little easier, and so are we, knowing that one less critical vulnerability is lurking in the shadows.




