Files
certctl/docs/connector-iis.md
T
shankar0123 bc6039a79e chore: sweep github.com/shankar0123/certctl URL refs to certctl-io/certctl
Post-transfer cosmetic + release-critical URL refresh after moving the
repo from github.com/shankar0123/certctl to github.com/certctl-io/certctl
(2026-05-03). GitHub HTTP redirects continue to forward old URLs forever,
so existing operators are not broken — but aligns the canonical
references with the new owner so:

- procurement engineers / contributors browsing the docs see the right
  URL on first read
- operators copying the agent install one-liner hit the new path
  directly without going through a redirect
- the Helm chart's default image repository points at the canonical org
  registry path
- the OnboardingWizard rendered to first-run UI users shows the new
  URL in the install snippets and doc anchor links
- the GitHub Actions release workflow pushes container images to
  ghcr.io/certctl-io/certctl-{server,agent} (was: shankar0123)
- the release-notes Markdown body in release.yml — which gets stamped
  into every future release page — references the post-transfer
  cert-identity (cosign keyless signing now uses the certctl-io
  workflow URL) and the post-transfer SLSA provenance source-uri.
  Without this, every cosign verify / slsa-verifier command on a
  v2.1.0+ release would fail because the cert-identity-regexp would
  not match the signing identity GitHub Actions OIDC issues post-
  transfer. Old releases (v2.0.67 and earlier) keep their immutable
  release-notes pointing at the shankar0123 path and remain
  verifiable via their own published instructions.

Customer impact:
- Operators on ghcr.io/shankar0123/certctl-{server,agent}:latest
  silently freeze on whatever tag was current at transfer time. They
  get no errors; they just stop receiving updates. The next release
  notes need a one-line callout (Phase 3.1 of cowork/transfer-
  certctl-to-org.md) telling them to update their image path to
  ghcr.io/certctl-io/certctl-{server,agent}.
- All other URLs (git clone, install one-liner, raw.githubusercontent
  URLs, browser links, GitHub API) continue to resolve via permanent
  HTTP redirects. The sweep is cosmetic for those.

Files swept (30 total):
  .github/workflows/release.yml — IMAGE_NAMESPACE, source-uri,
    cosign cert-identity-regexp, IMAGE= snippet (5 refs total).
  CHANGELOG.md, README.md — anchor links, badges, install one-liner,
    cosign verify snippets in operator-facing sections.
  api/openapi.yaml — info / externalDocs URLs.
  install-agent.sh — GITHUB_REPO const + systemd unit Documentation=
    field.
  deploy/ENVIRONMENTS.md, deploy/helm/{CHART_SUMMARY,INDEX,
    INSTALLATION,README}.md, deploy/helm/certctl/{Chart.yaml,
    README.md,values.yaml}, deploy/helm/examples/values-*.yaml —
    chart docs + image repository defaults across dev / prod-ha
    overrides.
  docs/{certctl-for-cert-manager-users,connector-iis,connectors,
    migrate-from-acmesh,migrate-from-certbot,quickstart,test-env,
    why-certctl}.md — operator-facing doc URLs.
  examples/{acme-nginx,acme-wildcard-dns01,multi-issuer,
    private-ca-traefik,step-ca-haproxy}/docker-compose.yml +
    examples/step-ca-haproxy/step-ca-haproxy.md — example image:
    paths and accompanying narrative.
  web/src/pages/OnboardingWizard.tsx — first-run-UI URL refs (curl
    install one-liners, agent docker image path, doc anchor links).

Files intentionally NOT swept (Choice A from cowork/transfer-certctl-
to-org.md):
  go.mod, go.sum — module declaration stays github.com/shankar0123/
    certctl. Existing imports compile because Go uses the path
    declared in go.mod, not the URL it was fetched from. Internal-
    only project; no external Go consumers; rename will land as a
    mechanical sed when one materializes.
  ~250 *.go files — every import remains github.com/shankar0123/
    certctl/internal/...
  deploy/test/f5-mock-icontrol/go.mod — separate test sub-module;
    same Choice A logic; module path stays.

Files intentionally NOT swept (other reasons):
  README.md lines 244-245 — Scarf-pixel docker-pull commands.
    shankar0123.docker.scarf.sh/... is a Scarf-account hostname
    (per-user, not per-repo) and the pixel keeps tracking pulls
    against the operator's personal Scarf account. Migrating to a
    certctl-io Scarf account is a separate decision (create org
    Scarf account → re-create package → update README).
  deploy/test/f5-mock-icontrol/f5-mock-icontrol — checked-in
    compiled binary with shankar0123/certctl baked into Go build
    info via the sub-module path. Out of scope for a URL sweep;
    will refresh on the next `make test-integration` rebuild.

Verification:
  gofmt: clean (no .go files touched).
  go vet ./...: clean (verified at this SHA in 1.3 of the transfer
    checklist; no .go changes since).
  go build ./...: clean (same).
  go test -short on representative packages: green (same).
  Diff shape: 30 files, 74 insertions / 74 deletions, net-zero size,
    pure URL substitution.
2026-05-03 23:39:50 +00:00

6.6 KiB

Microsoft IIS Connector — Operator Deep-Dive

Per Phase 14 of the deploy-hardening II master bundle.

Overview

The IIS connector (internal/connector/target/iis/) deploys TLS certs to Windows IIS servers via PowerShell (Import-PfxCertificate

  • New-WebBinding + SNI binding). Pre-deploy snapshot of the existing thumbprint allows rollback if the new binding fails.

Vendor versions tested

  • Windows Server 2019 with IIS 10
  • Windows Server 2022 with IIS 10

CI runner constraint

Per frozen decision 0.4: Windows containers run only on Windows hosts. Linux CI runners CAN'T run the IIS sidecar. IIS e2e tests run on a separate windows-vendor-e2e GitHub Actions matrix job on windows-latest runners. Operators on Linux-only CI use //go:build integration && !no_iis to skip.

Per-quirk operator guidance

App-pool recycle (opt-in)

TestVendorEdge_IIS_AppPoolRecycle_OptInForCertChange_E2E

By default, IIS picks up new SSL bindings without app-pool recycle (the binding-edit path is hot). Some sites need recycle to fully reload (e.g., apps that cache cert handles).

Operator action: set AppPoolRecycle: true per-target. The connector then runs Restart-WebAppPool <pool> after binding update.

SNI multi-binding per site

TestVendorEdge_IIS_SNIMultiBindingPerSite_DeployUpdatesCorrectBinding_E2E

When a site has multiple SNI bindings (different hostnames on the same site), connector targets the binding matching the operator-supplied hostname. Other bindings unchanged.

CCS (Centralized Certificate Store)

TestVendorEdge_IIS_CCSCentralizedCertStoreVariant_DeployToSharedStore_E2E

CCS is the file-based variant where multiple IIS servers share a UNC path of cert files. Connector writes to the shared path; all IIS servers pick it up automatically.

WinRM remote vs local PowerShell

TestVendorEdge_IIS_WinRMRemotePath_vs_LocalPowerShellPath_BothWork_E2E

Two code paths produce equivalent cert installs:

  • WinRMHost: "" → local PowerShell (agent runs on the IIS server)
  • WinRMHost: "iis.example" → remote PowerShell via WinRM

Both rotate the same way. WinRM path requires network reachability to port 5985/5986.

Server 2019 vs 2022 PowerShell compat

TestVendorEdge_IIS_WindowsServer2019_vs_2022_PowerShellCompat_E2E

Import-PfxCertificate + New-WebBinding semantics are stable across server versions. PowerShell 5.1 (2019) + PowerShell 7.x (2022) both work.

Friendly name

TestVendorEdge_IIS_FriendlyNameUpdatedOnRotation_E2E

Connector preserves operator-supplied FriendlyName on the cert across rotation. Useful for IIS GUI identification.

HTTP/2 + ALPN

TestVendorEdge_IIS_HTTP2ALPNPreserved_E2E

IIS h2 negotiation preserved across cert rotation. The netsh http show sslcert ALPN attribute survives the binding swap.

Binding-type validation

TestVendorEdge_IIS_BindingTypeHttpsValidated_E2E

Connector refuses to deploy to non-https bindings (e.g., http, net.tcp). Surfaces actionable error.

ARR reverse-proxy

TestVendorEdge_IIS_ARRReverseProxyCertRotation_E2E

Sites using Application Request Routing as reverse proxy: cert rotation does not invalidate ARR routes. The cert-binding edit is independent of the ARR config.

Atomic SNI binding swap

TestVendorEdge_IIS_RemovePreviousBindingOnRotate_E2E

Connector removes the previous SNI binding BEFORE inserting the new one (atomicity at the IIS API level). Prevents brief window where two bindings serve different certs for the same hostname.

Troubleshooting matrix

Symptom Test name Operator action
Cert installed but app pool serving old cert AppPoolRecycle_OptInForCertChange_E2E set AppPoolRecycle: true
Wrong SNI binding updated SNIMultiBindingPerSite_E2E verify hostname selector
Permission denied on cert install n/a agent must run as administrator
WinRM connection failed WinRMRemotePath_vs_LocalPowerShellPath_E2E check WinRM port 5985/5986 reachability
h2 negotiation broken post-rotate HTTP2ALPNPreserved_E2E re-run netsh http add sslcert with appid + clientcertnegotiation=enable

V3-Pro deferrals

  • IIS Application Initialization module integration (warm cert cache after rotation).
  • Azure Key Vault + IIS integration (operator opt-in).

Operator validation playbook (Windows host)

CI no longer runs the IIS + WinCertStore vendor-e2e tests on every push. Per ci-pipeline-cleanup bundle frozen decision 0.5 (which revises Bundle II decision 0.4), the Windows matrix was deleted because (a) it couldn't physically work on windows-latest GitHub runners (Docker not started in Windows-containers mode by default; bridge network driver doesn't exist on Windows Docker — uses nat), and (b) all IIS + WinCertStore vendor-edge tests are t.Log placeholder stubs that exercise no IIS-specific behavior.

The real IIS connector validation lives in:

  1. internal/connector/target/iis/ unit tests (run on Linux in the regular Go Build & Test job — already green on every push).
  2. This playbook — operator manual smoke against a real Windows host pre-release.

Prerequisites

  • Windows Server 2019 or 2022 host (or Windows 10/11 Pro with Hyper-V)
  • Docker Desktop in Windows containers mode (Settings → "Switch to Windows containers")
  • Go 1.25.9 + git

Procedure

# Clone + checkout
git clone https://github.com/certctl-io/certctl.git
cd certctl
git fetch --tags
git checkout v2.X.0  # whichever release is being validated

# Bring up the Windows IIS sidecar
docker compose --profile deploy-e2e-windows `
  -f deploy/docker-compose.test.yml `
  up -d windows-iis-test
Start-Sleep -Seconds 30

# Run IIS + WinCertStore vendor-edge tests
$env:INTEGRATION = "1"
go test -tags integration -race -count=1 `
  -run 'VendorEdge_(IIS|WinCertStore)' `
  ./deploy/test/... | Tee-Object -FilePath iis-validation.log

# Tear down
docker compose --profile deploy-e2e-windows `
  -f deploy/docker-compose.test.yml `
  down -v

Acceptance

Per Bundle II frozen decision 0.14, the IIS / WinCertStore cells in docs/deployment-vendor-matrix.md flip from "CI" / "pending" → "✓" only when ALL of the following are true:

  • ≥1 happy-path e2e passes against the real Windows IIS sidecar
  • ≥1 specific-quirk test for that Windows Server version passes
  • This playbook's full procedure ran clean once on a real Windows host

Operator records the validation date + Windows Server version in cowork/<bundle>/iis-validation-receipts.md for audit trail.