Automatic HTTPS development certificate management for .NET or Aspire projects in devcontainers and VS Code remote environments.
When developing .NET applications or Aspire orchestration projects inside devcontainers, you need HTTPS certificates that are trusted on both sides: the host (so browsers accept forwarded ports) and the container (so servers can terminate HTTPS and allow inter-service calls work). This project automates the entire process.
Add the devcontainer feature to your devcontainer.json and everything works automatically:
{
"features": {
"ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": {}
}
}No dotnet dev-certs commands, no manual PFX exports, no environment variable configuration.
- VS Code 1.100 or later
- The Dev Containers extension
- Docker or a compatible container runtime
Add the dev container feature to your project's devcontainer.json:
{
"features": {
"ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": {}
}
}Then rebuild or reopen your project in the dev container. The feature declares both companion extensions, so VS Code installs the remote extension inside the container automatically. The host extension is installed on your local VS Code automatically as well; if it isn't, the remote extension prompts you with an Install Host Extension button on first use.
On first use:
- The host extension shows a one-time consent prompt, then generates a development certificate and trusts it in your OS certificate store. On Windows this triggers a system dialog; on macOS the keychain may prompt for a password.
- The remote extension receives the certificate and installs it in the container's .NET X509 store and OpenSSL trust directory.
- ASP.NET, Aspire, and CLI tools like
curlandwgettrust the certificate automatically — no environment variables or manual configuration needed. - Your host browser trusts the certificate on forwarded ports.
You normally don't need to do this — the feature handles it. If you'd rather install the host extension (dnegstad.devcontainer-dev-certs-host) ahead of time:
- VS Code Marketplace: Dev Container Dev Certificates (Host)
- Extensions view: search for
dnegstad.devcontainer-dev-certs-host - CLI:
code --install-extension dnegstad.devcontainer-dev-certs-host
The remote extension (dnegstad.devcontainer-dev-certs-remote) runs inside the container and is installed by the feature. Don't install it on your local VS Code — it has no effect there.
Starting with v1.0.0, each release publishes SLSA build provenance attestations that bind the artifact back to the GitHub Actions workflow run that produced it. Attestations are minted from short-lived OpenID Connect tokens — no long-lived publishing credentials are stored in the repository — and the publishing workflow runs in a protected release environment scoped to release tags. Earlier (0.x) releases predate this pipeline and are not attested.
You can verify any release artifact with the gh attestation verify command:
- Devcontainer feature. The provenance attestation is pushed to GHCR alongside the OCI artifact:
gh attestation verify \ oci://ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:<version> \ --repo dnegstad/devcontainer-dev-certs - Extension VSIXes. Both
dnegstad.devcontainer-dev-certs-hostanddnegstad.devcontainer-dev-certs-remoteVSIXes are attached as assets to each GitHub Release with attestations stored on GitHub:gh attestation verify <path-to>.vsix --repo dnegstad/devcontainer-dev-certs
Verification confirms that the artifact was built from this repository, on the workflow run referenced in the attestation, and has not been modified since.
The solution has three components that work together:
-
Devcontainer Feature sets up the container's trust infrastructure: creates the .NET X509 store and OpenSSL trust directories, configures
SSL_CERT_DIR, and requests installation of the two companion VS Code extensions. -
Host Extension (
extensionKind: ["ui"]) runs on your local machine. It generates certificates identical todotnet dev-certs https(same OID marker, same SAN entries, same key parameters) using Node's built-incryptoplus@peculiar/x509andpkijsfor X.509 / PKCS#12 — supporting RSA, ECDSA, and Ed25519 keys. On first use, it generates a cert and trusts it in the host OS certificate store. It then serves the certificate material to the remote side via VS Code's cross-host command routing. -
Remote Extension (
extensionKind: ["workspace"]) runs inside the container. On activation, it requests certificate material from the host extension, decodes it, and places it in two locations:- The .NET X509 store (
~/.dotnet/corefx/cryptography/x509stores/my/) where Kestrel discovers it automatically via itsGetDevelopmentCertificateFromStore()fallback - An OpenSSL trust directory (
~/.aspnet/dev-certs/trust/) with hash symlinks (c_rehash, implemented in pure TypeScript) socurl,wget, and other OpenSSL-based tools trust it
- The .NET X509 store (
The two extensions communicate using VS Code's cross-host executeCommand() routing. The remote extension detects whether the host extension is installed and prompts to install it if missing. This architecture is transport-agnostic — it works for devcontainers today and can support SSH remoting, WSL, or any future VS Code remote backend.
src/
vscode-ui-extension/ VS Code host extension (extensionKind: ui)
src/
cert/ Certificate generation, export, and management
generator.ts X.509 certificate generation (matches ASP.NET CertificateManager)
properties.ts OID constants, SAN entries, key parameters
exporter.ts PFX and PEM export
manager.ts Orchestrates generate/trust/export/check
platform/ OS-specific cert store implementations
windowsStore.ts Windows cert store via PowerShell
macStore.ts macOS keychain via security CLI
linuxStore.ts Linux X509Store + OpenSSL trust directory
certProvider.ts Serves cert material to the workspace extension
vscode-workspace-extension/ VS Code remote extension (extensionKind: workspace)
src/
certInstaller.ts Writes cert files to correct paths
util/rehash.ts Pure TypeScript c_rehash (OpenSSL subject hash computation)
util/sslCertDir.ts SSL_CERT_DIR management for non-devcontainer remotes
util/paths.ts .NET store and OpenSSL trust directory paths
devcontainer-feature/ Devcontainer feature
src/devcontainer-dev-certs/
devcontainer-feature.json Feature metadata, options, extension references
install.sh Container build-time setup (creates directories)
test/
sample-project/ Test project template (hydrated into .out/ for testing)
hydrate.mjs Assembles a runnable test project from the template + feature
.github/workflows/ CI/CD (build, extension packaging, feature publishing)
{
"features": {
"ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": {
"trustNss": true
}
}
}| Option | Default | Description |
|---|---|---|
trustNss |
false |
Install NSS tools for Chromium/Firefox trust inside the container |
sslCertDirs |
Standard distro paths | System CA directories for SSL_CERT_DIR. Override for non-standard base images. |
generateDotNetCert |
true |
Auto-generate the ASP.NET / Aspire compatible HTTPS dev cert. Set to false to skip generation (useful when you only want to sync user-managed certs). |
syncUserCertificates |
true |
Per-container opt-out for syncing certs configured in the host devcontainerDevCerts.userCertificates VS Code setting. |
extraCertDestinations |
"" |
Comma-separated list of additional directories to write cert artifacts to. Each entry is <abs-dir>[=<format>] where format is pem, key, pem-bundle, pfx, or all (default). Every synced cert is written under the directory as {name}.{pem,key,pfx} (and/or {name}-bundle.pem). Example: /etc/nginx/certs=pem,/var/myapp. |
The host extension can sync arbitrary host-side certificates into your dev containers alongside (or instead of) the auto-generated dev cert. Configure them in your user or workspace VS Code settings:
{
"devcontainerDevCerts.userCertificates": [
{
"name": "corp-ca",
"pemCertPath": "/Users/me/certs/corp-ca.pem"
},
{
"name": "staging",
"pfxPath": "/Users/me/certs/staging.pfx",
"pfxPassword": "hunter2",
"trustInContainer": true
}
]
}Each entry supplies exactly one of pfxPath (+ optional pfxPassword) or pemCertPath (+ optional pemKeyPath). Omitting the key produces a CA-only entry — the cert is still planted in the container trust store, but no private key is synced and no PFX is written to the .NET store. Expired certificates are synced anyway but produce a one-time warning notification so you know why TLS clients are rejecting them.
name is used verbatim as a filename stem both on the host (temp export directory) and inside the container (trust PEM and extra-destination files), so it's constrained to [A-Za-z0-9._-] (1–64 chars, no leading dot, no . / ..). Entries with an invalid name are rejected with an error notification and skipped.
User-managed certs are never added to the host OS trust store; the assumption is you already trust them on the host if you're syncing them.
extraCertDestinations writes cert artifacts into additional directories inside the container — useful for non-.NET workloads (nginx, Java keystores, Python requests bundles, etc.). Each entry is a directory; every synced cert gets a set of files under it named after the cert. Formats:
| Format | Writes per cert |
|---|---|
pem |
{name}.pem (cert only) |
key |
{name}.key (private key; skipped when no key is available) |
pem-bundle |
{name}-bundle.pem (cert + key concatenated) |
pfx |
{name}.pfx (skipped when no key is available) |
all (default) |
all of the above |
After every cert has been written, OpenSSL's c_rehash runs once per unique destination directory (not once per cert and not once per write), so adding more synced certs doesn't multiply the rehash cost.
Every cert written to an extra destination uses a stable, documented {name} that downstream configuration (nginx ssl_certificate, Java keystore scripts, etc.) can rely on:
| Cert | Filename stem |
|---|---|
| Auto-generated .NET dev cert | aspnetcore-dev |
| User-managed cert | the name field of the matching userCertificates entry |
So with extraCertDestinations = /etc/nginx/certs and a user cert named corp-ca, the directory ends up containing aspnetcore-dev.pem, aspnetcore-dev.key, aspnetcore-dev.pfx, aspnetcore-dev-bundle.pem, corp-ca.pem, corp-ca.key, corp-ca.pfx, and corp-ca-bundle.pem (subject to the format filter). The thumbprint-keyed filenames ({thumbprint}.pfx, aspnetcore-localhost-{thumbprint}.pem) remain only in the canonical .NET directories where Kestrel requires them — they do not appear in extra destinations.
- Node.js 22+
- Docker (for devcontainer testing)
- VS Code with the Dev Containers extension
Open the repo in VS Code and press F5. The build-extensions task will:
- Build both TypeScript extensions with esbuild
- Hydrate a test project from the template into
.out/test-project/ - Package the workspace extension VSIX into the test project's
.devcontainer/
The Extension Development Host opens with the UI extension loaded on the host side. To test the full devcontainer flow, reopen .out/test-project/ in a container.
- Auto-generated dev cert matches .NET's format only. The
generateDotNetCertflow produces a cert identical todotnet dev-certs https(specific OID marker, subject, SAN entries). To sync differently-shaped certs (corporate CAs, custom wildcard certs, etc.), add them via thedevcontainerDevCerts.userCertificatesVS Code setting — they're copied as-is. - VS Code only. The companion extension pattern relies on VS Code's cross-host command routing. Other editors (JetBrains, Vim, etc.) are not supported, though the devcontainer feature includes a
setup-cert.shfallback script (with a--bundle-jsonform for multi-cert bundles) for manual use. - Host trust requires user interaction. On Windows, trusting the auto-generated dev cert triggers a system dialog. On macOS, the keychain may prompt for a password. This only happens once and only for the .NET dev cert — user-managed certs are never added to the host OS trust store.
| Platform | Architecture |
|---|---|
| Windows | x64, ARM64 |
| macOS | x64, ARM64 |
| Linux (glibc) | x64, ARM64 |
| Linux (musl/Alpine) | x64 |