How SpecKit Transformed a Legacy SSL Certificate Feature: A Case Study
A detailed case study on using SpecKit methodology to modernize SSL certificate deployment in a legacy ASP.NET MVC codebase while respecting its constraints.
Our streaming server platform is a legacy codebase: ASP.NET MVC with AngularJS, Entity Framework 6 with MySQL, TypeScript 2.9, and Windows Services. It’s been in production for years. The unspoken rule? Touch as little as possible. Every change introduces risk, and stability trumps code aesthetics.
The task seemed simple: modernize how SSL certificates get deployed to servers. The old way used the S3Upgrader, a deployment package system that bundled certificates in a certificates folder. We wanted an API-driven approach: a background process that polls an endpoint, downloads new certificates, and installs them automatically.
What could go wrong?
Enter SpecKit
Instead of diving into code, I tried a structured approach using SpecKit, a methodology built around progressive specification with Claude CLI. The process unfolds through a series of slash commands, each building on the previous.
Step 1: The Constitution (/speckit.constitution)
Before writing a single spec, I established the ground rules. The prompt was deliberate:
“The codebase is a legacy codebase that when we modify we want to do it with as few as possible changes. We need to add a new feature to update the SSL cert and we are planning on using Claude CLI to help with that.”
What emerged was a Deployed Streaming Server Constitution, a formal document with three core principles:
- Minimal Footprint: Changes MUST be limited to what’s necessary. “While I’m here” improvements are prohibited.
- Critical Path Testing: Focus testing on high-risk areas (SSL, auth, data integrity), not comprehensive coverage.
- AI-Assisted Development: All AI-generated code MUST be reviewed by a human. AI suggestions for unnecessary refactoring MUST be rejected.
This constitution became the guardrail for everything that followed. When Claude later wanted to refactor nearby code or add “helpful” abstractions, the constitution provided grounds to push back.
Step 2: The Specification (/speckit.specify)
With constraints established, I described the feature in natural language:
“The s3upgrader would use the SSL certs located in the certificate modules folder… Going forward we want a process similar to the processes under the processes folder. There is a manager service which is the core service that will launch a process using the process helper…”
SpecKit transformed this into a structured specification with:
- 4 User Stories (prioritized P1-P3)
- 17 Functional Requirements (FR-001 through FR-017)
- 7 Success Criteria (measurable outcomes)
- Edge Cases (what happens when the API returns corrupted data?)
- Assumptions (we only have Connected deployment types anymore)
I reviewed the generated spec and made changes. SpecKit isn’t autopilot. The human refines what the AI generates.
Step 3: The Plan (/speckit.plan)
The planning phase required context about constraints:
“The application should be built using dotnet core 10, as a self contained application, trimmed, and ready to run. For code reusability lets plan on just copying the code needed from where it already exists…”
What emerged was a comprehensive implementation plan with:
- Project structure mapped out (exact file paths)
- Code adaptation matrix: which S3Upgrader files to copy, what changes needed for .NET 10
- Technology decisions documented with rationale
- Constitution check: a gate verifying the plan complies with established principles
A notable exchange happened during planning. Claude initially ruled out using our internal Swank.Logging.Serilog package. I pushed back:
“/speckit.plan you ruled out using Swank.Logging but the latest version of that is just dotnet core. We should use that.”
So the plan was updated. Another correction:
“/speckit.plan update to use nUnit not xunit”
The existing codebase uses NUnit. Consistency matters in legacy systems.
Step 4: The Tasks (/speckit.tasks)
Task generation required specific guidance to avoid common AI pitfalls:
“Break down the work into small, focused tasks. Since we’re porting .NET 4.5.1 code to .NET 10, include tasks for identifying API surface changes that need adjustment. Sequence the work so we can test the certificate retrieval from the API independently before integrating the installation and binding logic.”
The result: 62 tasks across 10 phases, organized with:
- [P] markers for parallelizable tasks
- [US#] labels mapping tasks to user stories
- Dependency graph showing execution order
- Testing milestones: “After Phase 4, you can test API retrieval independently”
Phase 2 was particularly valuable: “API Surface Analysis (.NET 4.5.1 to .NET 10 Port)”, identifying breaking changes before hitting them during implementation.
Step 5: Implementation (/speckit.implement)
The implementation directive set expectations:
“Work through tasks one phase at a time and pause between phases for review. Since we’re copying code from .NET 4.5.1 to .NET 10, highlight any API changes or compatibility issues you encounter as you go. Keep the copied code as similar to the original as possible per the constitution. Ask before making significant architectural decisions.”
I have a single commit that captures the main implementation, it contained 64 unit tests covering:
- API client with mock HTTP responses
- Certificate installation and verification
- IIS binding creation and deprecated cert replacement
- Service lifecycle and graceful shutdown
- Retry logic and error recovery
The commit message tells the story:
“Add a standalone .NET 10 Windows process that automatically retrieves and installs SSL certificates from a central API endpoint, replacing the manual S3Upgrader certificate deployment process.”
The Refinement Phase
Implementation was just the beginning. The commit history shows the real-world iteration:
| Commit | What Happened | What was missing |
|---|---|---|
| 6395f3ff7 | Fix package reference, upgrade to latest packages | Should of mentioned that I want the latest NuGet packages |
| eb8e4c702 | Organize the code place interfaces in their own folder | Needed to mention that I like keeping my interfaces in a separate location, even though the other projects did this it was missed by Claude and Spec-kit |
| 70ca6ecff | Get current thumbprint to make the request | I wanted to pass the current thumbprint to the endpoint for check and it was not there |
| 73618536c | Fix for AOT for JSON result.. With warning message | I needed some JsonSerializerContext to prevent the trimming from messing up the serialization |
| e460252a0 | Use Polly for retry | It was using the older pattern for retry and wanted to upgrade to use Polly for the retry |
| 4622a1ebc | Fix Config binding using init only properties | I do not know why it used init properties for config binding but it did and did not work |
| ed6d54a97 | ”Code removal, Claude added too much. All I wanted was the SSL Cert, Thumbprint and password” | I should have been more specific on the contract during my spec flow |
“AI tools excel at generating code but lack context about legacy system constraints. Human review catches scope creep…”
By the Numbers
The branch produced 71 changed files with ~9,500 lines added:
- A complete .NET 10 standalone process
- Comprehensive test suite (64 tests)
- Full specification documentation
- API contract (OpenAPI YAML)
- Migration notes for .NET 4.5.1 → .NET 10 porting
Specification artifacts created:
- constitution.md - Project-level governance
- spec.md - Feature specification
- plan.md - Implementation plan
- tasks.md - 62 tasks across 10 phases
- research.md - Technology decisions with rationale
- data-model.md - Entity and DTO definitions
- api-migration-notes.md - .NET porting guidance
- ssl-certificate-api.yaml - OpenAPI contract
What SpecKit Got Right
1. Progressive Disclosure
Each phase builds on the previous. You can’t generate tasks without a plan. You can’t plan without a spec. You can’t spec without understanding constraints. This prevented the common failure mode of “let me just start coding.”
2. Constitution as Guardrail
Having formal principles to point to made it easier to reject AI suggestions. “The constitution says no scope creep” is more defensible than “I don’t like that.”
3. Testable Checkpoints
The tasks were sequenced so you could test pieces independently. After Phase 4 (API client), you could verify certificate retrieval without touching IIS. After Phase 6, you had full US1 (core functionality) working.
4. Human Override Points
The workflow assumes human intervention. When the AI chose xUnit over NUnit, or excluded Swank.Logging.Serilog, the human corrected course. SpecKit is a collaboration tool, not autopilot.
What Required Human Correction
1. Over-Engineering
Despite explicit instructions, Claude added more than requested. AI assistance still requires vigilant review.
2. Technology Assumptions
Claude initially made wrong assumptions about logging frameworks and testing libraries. Domain knowledge about what’s already in the codebase had to be injected manually.
3. AOT/Trimming Compatibility
The fix for AOT JSON serialization wasn’t anticipated in planning. Real-world .NET 10 constraints emerged during implementation.
Conclusion
SpecKit didn’t write the feature for me. It provided structure for a complex task that could have easily spiraled. The SSL manager process now:
- Runs as a standalone .NET 10 process
- Polls a defined API for certificate updates
- Installs certificates using the same certutil.exe approach as the legacy system
- Binds to IIS with proper retry logic
- Maintains compatibility with existing deployment infrastructure
Most importantly, it did this while respecting the legacy codebase’s constraints. The constitution kept both the human and the AI honest.
For legacy codebases where “touch nothing unless necessary” is the prime directive, SpecKit’s structured approach provides the guardrails that free-form AI coding lacks. The commit that says “Claude added too much” is proof that even with guardrails, human oversight remains essential.
The feature shipped. The SSL certificates auto-update. And the legacy codebase lives to serve another day, minimally modified.