From 5053283f7f0e83da7c8c725634633cb09eb256e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 02:47:26 +0000 Subject: [PATCH 1/2] docs: restructure READMEs so the happy path leads and advanced usage is findable Main README: - Reorder sections so usage leads: Getting Started -> How It Works -> Configuration reference -> workflows -> Troubleshooting -> reference -> Development -> Provenance. Release provenance and Repository Layout were sitting at positions 3 and 4, ahead of any configuration docs. - Add a Contents list; the file has 25+ top-level and nested sections with no navigation outside GitHub's outline sidebar. - Add "Verify it worked" with the three store/trust checks and a curl call, so the happy path ends in confirmation rather than an assumption. - Add a Troubleshooting section: output channel names for both extensions, plus the common failure modes (nothing happened, browser still warns, curl fails, Kestrel picks the wrong cert, container push rejected). - Add a Commands reference. All three commands were previously mentioned only inside settings-table cells, and the host-side browser trust command not at all. - Add a Linux browser trust section. NSS/certutil was documented only in the host extension README, though without it forwarded ports still warn in the browser on Linux hosts. - Add the per-entry field table for userCertificates. The host settings table pointed at a "per-entry schema" that did not exist, leaving trustInContainer used in an example but never defined. - Disambiguate generateDotNetCert: the feature option and the host setting share a name and had near-identical descriptions in adjacent tables. The feature row now describes pulling the host cert into the container. - Document setup-cert.sh: both invocation forms, the bundle JSON shape, the openssl/jq prerequisites, and the fact that the feature does not copy the script into the container. - Add an environment variable reference covering SSL_CERT_DIR, the DEVCONTAINER_DEV_CERTS_* vars, and the DOTNET_GENERATE_ASPNET_CERTIFICATE suppression, and move the suppression rationale out of How It Works. - Fix a broken sentence in the opening paragraph; normalize "Dev Container" capitalization in prose; link Prerequisites to Supported Platforms. Extension READMEs: - Host: add a Commands table; promote browser trust from a fourth-level subsection to a top-level section next to Quick Start; add a Troubleshooting pointer with the output channel name. - Remote: drop a duplicated install-prompt sentence; add "Verify it worked" and a Troubleshooting pointer. - Both: trim the repeated 7-row feature options table to the options that affect that extension, linking to the main reference for the rest, and replace the duplicated dotnet suppression prose with a summary plus link. - Both: link back to the repository, which neither marketplace page did. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KjJpJFDYwZTq7oYF7zLAkH --- README.md | 396 +++++++++++++++++------ src/vscode-ui-extension/README.md | 158 +++++---- src/vscode-workspace-extension/README.md | 55 ++-- 3 files changed, 432 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 40b398b..6d4e7f8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Devcontainer Dev Certificates +# Dev Container Dev Certificates -Automatic HTTPS development certificate management for .NET or Aspire projects in devcontainers and VS Code remote environments. +Automatic HTTPS development certificate management for .NET or Aspire projects in Dev Containers and VS Code remote environments. ## What This Does -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. +When developing .NET applications or Aspire orchestration projects inside Dev Containers, 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 inter-service calls succeed). This project automates the entire process. -Add the devcontainer feature to your `devcontainer.json` and everything works automatically: +Add the Dev Container feature to your `devcontainer.json` and everything works automatically: ```json { @@ -18,6 +18,20 @@ Add the devcontainer feature to your `devcontainer.json` and everything works au No `dotnet dev-certs` commands, no manual PFX exports, no environment variable configuration. +## Contents + +- [Getting Started](#getting-started) — install it and confirm it worked +- [How It Works](#how-it-works) — the three components +- [Configuration reference](#configuration-reference) — every option, setting, command, and environment variable +- [User-managed certificates](#user-managed-certificates) — sync your own certs (corporate CAs, wildcards) +- [Extra destinations](#extra-destinations) — write certs for nginx, Java, and other non-.NET workloads +- [Syncing a certificate from the container to the host](#syncing-a-certificate-from-the-container-to-the-host) — reverse sync (opt-in) +- [Troubleshooting](#troubleshooting) — logs and the common failure modes +- [Known issues](#known-issues) +- [Non-VS Code editors](#non-vs-code-editors) — the manual fallback script +- [Limitations](#limitations) · [Supported Platforms](#supported-platforms) +- [Development](#development) · [Verifying Release Provenance](#verifying-release-provenance) + ## Getting Started ### Prerequisites @@ -25,10 +39,11 @@ No `dotnet dev-certs` commands, no manual PFX exports, no environment variable c - VS Code 1.100 or later - The [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension - Docker or a compatible container runtime +- A host OS on the [supported platform list](#supported-platforms) ### Add the Feature -Add the dev container feature to your project's `devcontainer.json`: +Add the Dev Container feature to your project's `devcontainer.json`: ```json { @@ -38,7 +53,7 @@ Add the dev container feature to your project's `devcontainer.json`: } ``` -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. +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: @@ -47,6 +62,34 @@ On first use: 3. ASP.NET, Aspire, and CLI tools like `curl` and `wget` trust the certificate automatically — no environment variables or manual configuration needed. 4. Your host browser trusts the certificate on forwarded ports. +### Verify it worked + +Inside the container, three checks confirm the certificate landed where it needs to be: + +```bash +# 1. The .NET X509 store — Kestrel reads the cert from here +ls ~/.dotnet/corefx/cryptography/x509stores/my/ +# expect: {thumbprint}.pfx + +# 2. The OpenSSL trust directory — curl/wget read from here +ls ~/.aspnet/dev-certs/trust/ +# expect: aspnetcore-localhost-{thumbprint}.pem plus a {hash}.0 symlink + +# 3. If the .NET SDK is installed in the container +dotnet dev-certs https --check +# expect: "A valid HTTPS certificate is already present." +``` + +Then start your app and call it over HTTPS from inside the container **without** `-k` / `--insecure`: + +```bash +curl https://localhost: +``` + +On the host, open the forwarded port in your browser — it should show a padlock with no warning. (On Linux hosts, browsers need one extra step; see [Linux hosts: browser trust](#linux-hosts-browser-trust).) + +If any of these fail, see [Troubleshooting](#troubleshooting). + ### Installing the Host Extension Manually (Optional) 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: @@ -57,30 +100,37 @@ You normally don't need to do this — the feature handles it. If you'd rather i 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. -## Verifying Release Provenance +### Linux hosts: browser trust -Starting with **v1.0.0**, each release publishes [SLSA build provenance](https://slsa.dev/spec/v1.0/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. +On Windows and macOS, trusting the certificate in the OS store is enough for browsers — you can skip this section. -You can verify any release artifact with the [`gh attestation verify`](https://cli.github.com/manual/gh_attestation_verify) command: +On Linux, Firefox and Chromium use [NSS](https://firefox-source-docs.mozilla.org/security/nss/) for certificate trust and do **not** read OpenSSL trust directories or the .NET root store. Without this step, forwarded ports still show a certificate warning in the browser even though `curl` works fine. -- **Devcontainer feature.** The provenance attestation is pushed to GHCR alongside the OCI artifact: - ```bash - gh attestation verify \ - oci://ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs: \ - --repo dnegstad/devcontainer-dev-certs - ``` -- **Extension VSIXes.** Both `dnegstad.devcontainer-dev-certs-host` and `dnegstad.devcontainer-dev-certs-remote` VSIXes are attached as assets to each [GitHub Release](https://github.com/dnegstad/devcontainer-dev-certs/releases) with attestations stored on GitHub: - ```bash - gh attestation verify .vsix --repo dnegstad/devcontainer-dev-certs - ``` +Run **Dev Certs: Trust Certificate in Browsers** from the Command Palette on the host. It: -Verification confirms that the artifact was built from this repository, on the workflow run referenced in the attestation, and has not been modified since. +- Checks for `certutil` (from `libnss3-tools`) on the `PATH` +- If available, trusts the cert in Chromium (`~/.pki/nssdb/`) and in your Firefox profile NSS databases +- If `certutil` is missing or no browser databases are found, shows the certificate path so you can import it manually + +To install `certutil`: + +| Distro | Command | +|--------|---------| +| Debian / Ubuntu | `sudo apt install libnss3-tools` | +| Fedora / RHEL | `sudo dnf install nss-tools` | +| Arch | `sudo pacman -S nss` | + +To import manually in Firefox: **Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import**, then select the PEM file from `~/.aspnet/dev-certs/trust/`. + +Restart the browser after trusting so it picks up the updated NSS database. + +For browser trust *inside* the container (rarely needed — usually only when running a headless browser there), set the [`trustNss`](#dev-container-feature-options) feature option. ## How It Works The solution has three components that work together: -1. **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. +1. **Dev Container 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. 2. **Host Extension** (`extensionKind: ["ui"]`) runs on your local machine. It generates certificates identical to `dotnet dev-certs https` (same OID marker, same SAN entries, same key parameters) using Node's built-in `crypto` plus `@peculiar/x509` and `pkijs` for 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. @@ -88,58 +138,17 @@ The solution has three components that work together: - The .NET X509 store (`~/.dotnet/corefx/cryptography/x509stores/my/`) where Kestrel discovers it automatically via its `GetDevelopmentCertificateFromStore()` fallback - An OpenSSL trust directory (`~/.aspnet/dev-certs/trust/`) with hash symlinks (c_rehash, implemented in pure TypeScript) so `curl`, `wget`, and other OpenSSL-based tools trust it -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. - -### Suppressing dotnet's first-run certificate auto-generation - -When the host is the dev cert source (the default flow — `generateDotNetCert: true` and `syncContainerCert: false`), the devcontainer feature also exports `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` inside the container. Without it, the first `dotnet run` / `dotnet new webapi` / `dotnet build` of an HTTPS-enabled project triggers dotnet's implicit `CertificateManager` flow, which writes a self-signed cert into `~/.dotnet/corefx/cryptography/x509stores/my/` at roughly the same time the remote extension is trying to install OUR cert there. Whichever write lands last wins on disk, but the OS trust + .NET Root-store state may have been driven by the other side — yielding a "partially valid certificate on first run" combo where TLS works for some clients and fails for others. - -The override is **gated** on the host being the source: it's only set when `generateDotNetCert: true` AND `syncContainerCert: false`. When `syncContainerCert: true` (the container is the source), dotnet's implicit auto-generation might literally BE the source — typically a `dotnet run` somewhere bootstrapping the cert this feature then pushes to the host — so suppressing it would break the source. When both options are disabled and you've opted out of every managed flow, the override stays off so dotnet behaves normally for users still relying on its own cert provisioning. - -The override only affects the IMPLICIT path. Explicit `dotnet dev-certs https` commands always work regardless of the env var; the `syncContainerCert` flow that relies on a container-build step running `dotnet dev-certs https --trust` is unaffected. - -## Repository Layout - -``` -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/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) -``` +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 Dev Containers today and can support SSH remoting, WSL, or any future VS Code remote backend. ## Configuration reference -All configurable surfaces in one place. Three categories: the devcontainer feature options (set in `devcontainer.json`), the host VS Code settings (set on your local machine), and the workspace VS Code settings (set inside the Dev Container — and inherited by SSH/WSL remotes). The detailed prose sections that follow this reference cover the workflows these knobs participate in. +All configurable surfaces in one place. Four categories: the Dev Container feature options (set in `devcontainer.json`), the host VS Code settings (set on your local machine), the workspace VS Code settings (set inside the Dev Container — and inherited by SSH/WSL remotes), and the commands both extensions contribute. The prose sections that follow this reference cover the workflows these knobs participate in. -A historical-naming note: two prefixes are in use. `devcontainerDevCerts.*` (camelCase) is the newer convention for settings that describe cert *content* the extensions share across the host/container boundary; `devcontainer-dev-certs.*` (hyphenated) is the older convention for extension behavior knobs. Both stay supported; we don't rename them to avoid churning user settings. +**Two settings prefixes are in use.** `devcontainerDevCerts.*` (camelCase) is the newer convention for settings that describe cert *content* the extensions share across the host/container boundary; `devcontainer-dev-certs.*` (hyphenated) is the older convention for extension behavior knobs. Both stay supported; we don't rename them to avoid churning user settings. -### Devcontainer feature options +**One name, two scopes.** `generateDotNetCert` exists both as a *feature option* (container-side: "pull the host's dev cert into this container") and as a *host VS Code setting* `devcontainerDevCerts.generateDotNetCert` (host-side: "generate and trust a managed dev cert on this machine at all"). They are separate knobs at different layers — turning off the host setting means no managed dev cert exists anywhere; turning off the feature option means this one container doesn't receive it. Check which table you're reading. + +### Dev Container feature options Set under the feature entry in `devcontainer.json`: @@ -155,13 +164,13 @@ Set under the feature entry in `devcontainer.json`: | Option | Default | Description | |--------|---------|-------------| -| `trustNss` | `false` | Install NSS tools for Chromium/Firefox trust inside the container. | +| `trustNss` | `false` | Install NSS tools for Chromium/Firefox trust inside the container. For browser trust on a Linux *host*, see "[Linux hosts: browser trust](#linux-hosts-browser-trust)" instead. | | `sslCertDirs` | Standard distro paths | System CA directories for `SSL_CERT_DIR`. Override for non-standard base images. | | `pruneMissingCertDirs` | `true` | Filter out non-existent directories from `sslCertDirs` before writing `SSL_CERT_DIR` (some TLS stacks error on a missing entry). Set to `false` to use `sslCertDirs` verbatim — e.g. for a directory created after install but before it's needed. | -| `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). | +| `generateDotNetCert` | `true` | Pull the host-generated ASP.NET / Aspire compatible HTTPS dev cert into this container. Set to `false` to skip it (useful when this container only needs user-managed certs). Distinct from the host setting of the same name — see the note above. | | `syncUserCertificates` | `true` | Per-container opt-out for syncing certs configured in the host `devcontainerDevCerts.userCertificates` VS Code setting. | | `syncContainerCert` | `false` | **Reverse sync (opt-in).** When the container itself already has a valid ASP.NET dev certificate (e.g. baked into the image with `dotnet dev-certs https`), push it to the host so the host trusts it instead of generating its own. Enabling this also implicitly overrides `generateDotNetCert` for this container — you don't need to set both. See "[Syncing a certificate from the container to the host](#syncing-a-certificate-from-the-container-to-the-host)". | -| `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `[=]` 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`. | +| `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `[=]` 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`. See "[Extra destinations](#extra-destinations)". | ### Host VS Code settings @@ -171,9 +180,9 @@ Set in your host VS Code's user or workspace settings. Provided by the **Dev Con |---------|---------|-------------| | `devcontainer-dev-certs.autoProvision` | `true` | Allow certificate provisioning when the workspace extension requests one. On first use, a consent prompt explains what will happen before any certificates are generated. Set to `false` to disable provisioning entirely (host-generation AND acceptance of container-pushed certs). | | `devcontainerDevCerts.generateDotNetCert` | `true` | Auto-generate the ASP.NET / Aspire compatible HTTPS dev cert and trust it in the host OS store. When `false`, user-managed certificates (if any) are still synced, but no managed dev cert lives on the host — this also implicitly disables acceptance of container-pushed certs (a container push would land one in the same trust store the user opted out of). | -| `devcontainerDevCerts.userCertificates` | `[]` | Host-managed certificates to sync from the host into dev containers. See "[User-managed certificates](#user-managed-certificates)" for the per-entry schema (`name`, `pfxPath`/`pemCertPath`, etc.). User-managed certs are never added to the host OS trust store. | +| `devcontainerDevCerts.userCertificates` | `[]` | Host-managed certificates to sync from the host into Dev Containers. See "[User-managed certificates](#user-managed-certificates)" for the per-entry field table. User-managed certs are never added to the host OS trust store. | | `devcontainerDevCerts.installUserCertsToDotNetStore` | `false` | When `true`, also copies every entry from `userCertificates` into the container's .NET X509Store. **Security note:** the on-disk PFX there is passwordless (Linux's `StoreName.My` enumeration can't accept per-file passwords), so opting in strips your user cert's password on the in-container copy. Per-entry exemption via `excludeFromDotNetStore: true`. The auto-generated dotnet-dev cert is always installed to the store regardless. | -| `devcontainerDevCerts.defaultKestrelCertificate` | `""` | Name of a `userCertificates` entry to use as the default Kestrel certificate inside dev containers. When set, the remote extension writes that cert's PFX to `~/.aspnet/dev-certs/https/kestrel-default.pfx` and exports `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` to processes launched from VS Code (terminals, debug, tasks). Leave empty to opt out — Kestrel will still discover the auto-generated dev cert via X509Store. See "[Default Kestrel certificate (opt-in)](#default-kestrel-certificate-opt-in)" for scope and caveats. | +| `devcontainerDevCerts.defaultKestrelCertificate` | `""` | Name of a `userCertificates` entry to use as the default Kestrel certificate inside Dev Containers. When set, the remote extension writes that cert's PFX to `~/.aspnet/dev-certs/https/kestrel-default.pfx` and exports `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` to processes launched from VS Code (terminals, debug, tasks). Leave empty to opt out — Kestrel will still discover the auto-generated dev cert via X509Store. See "[Default Kestrel certificate (opt-in)](#default-kestrel-certificate-opt-in)" for scope and caveats. | | `devcontainerDevCerts.allowNonLocalContainerCertSans` | `false` | When accepting a Dev Container-managed dev certificate (via `syncContainerCert`), override the default SAN restriction that limits trusted certificates to localhost / loopback / private IPs / docker host names. Only enable when you fully understand the SAN entries the container will push. Has no effect when `generateDotNetCert` or `autoProvision` is `false`. | ### Workspace (Remote) VS Code settings @@ -185,9 +194,47 @@ Set in your workspace settings or user settings inside the Dev Container / remot | `devcontainer-dev-certs.autoInject` | `true` | Automatically inject and configure the dev cert when a remote session starts. Set to `false` to require manual invocation via the "Dev Certs: Inject Certificate into Remote" command. | | `devcontainer-dev-certs.warnOnStaleDevCerts` | `true` | Show a warning when multiple dev certificates are detected in this Dev Container's .NET certificate stores after install — alongside the extension-managed certificate. Pairs with the "Dev Certs: Clean Up Other Dev Certificates in Dev Container" command. | +### Commands + +All three are available from the Command Palette (`F1`). The host command runs on your local machine; the remote commands run inside the Dev Container. + +| Command | Runs on | Command ID | Description | +|---------|---------|------------|-------------| +| **Dev Certs: Trust Certificate in Browsers** | Host | `devcontainer-dev-certs.trustInBrowsers` | Trust the dev cert in Firefox / Chromium NSS databases. Linux hosts only — on Windows and macOS browser trust follows the OS store automatically. See "[Linux hosts: browser trust](#linux-hosts-browser-trust)". | +| **Dev Certs: Inject Certificate into Remote** | Remote | `devcontainer-dev-certs.injectCert` | Re-run the certificate injection flow manually. Normally automatic on activation; needed when `autoInject` is `false`, or to retry after a failure. | +| **Dev Certs: Clean Up Other Dev Certificates in Dev Container** | Remote | `devcontainer-dev-certs.cleanupStaleDevCerts` | Remove dev cert artifacts in the container's .NET stores that aren't the extension-managed one, then rehash the OpenSSL trust directory. Refuses to run when no managed dev cert is known, so it can't delete every dev cert on disk. | + +### Environment variables + +The feature writes these into the container at build time (via `/etc/profile.d` and the container environment), so they apply to VS Code terminals, tasks, debug sessions, and `docker exec` alike: + +| Variable | Value | Set when | +|----------|-------|----------| +| `SSL_CERT_DIR` | `$HOME/.aspnet/dev-certs/trust:` + the resolved `sslCertDirs` | Always | +| `DEVCONTAINER_DEV_CERTS_GENERATE_DOTNET` | The `generateDotNetCert` option | Always | +| `DEVCONTAINER_DEV_CERTS_SYNC_USER` | The `syncUserCertificates` option | Always | +| `DEVCONTAINER_DEV_CERTS_SYNC_FROM_CONTAINER` | The `syncContainerCert` option | Always | +| `DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS` | The `extraCertDestinations` option | Always | +| `DOTNET_GENERATE_ASPNET_CERTIFICATE` | `false` | Only when `generateDotNetCert: true` AND `syncContainerCert: false` — see below | + +The remote extension sets two more at runtime, and honors one: + +| Variable | Notes | +|----------|-------| +| `ASPNETCORE_Kestrel__Certificates__Default__Path` / `__Password` | Set only when `devcontainerDevCerts.defaultKestrelCertificate` names a user cert, and only for processes launched from VS Code. See "[Default Kestrel certificate (opt-in)](#default-kestrel-certificate-opt-in)". | +| `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY` | *Honored, not set.* Overrides the default OpenSSL trust directory (`~/.aspnet/dev-certs/trust/`), matching the official .NET `CertificateManager` behavior. | + +#### Why `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` + +When the host is the dev cert source (the default flow), the first `dotnet run` / `dotnet new webapi` / `dotnet build` of an HTTPS-enabled project would otherwise trigger dotnet's implicit `CertificateManager` flow, writing a self-signed cert into `~/.dotnet/corefx/cryptography/x509stores/my/` at roughly the same time the remote extension is installing ours there. Whichever write lands last wins on disk, but the OS trust and .NET root-store state may have been driven by the other side — yielding a "partially valid certificate on first run" state where TLS works for some clients and fails for others. + +The override is **gated** on the host being the source. When `syncContainerCert: true`, dotnet's implicit auto-generation might literally *be* the source — typically a `dotnet run` bootstrapping the cert this feature then pushes to the host — so suppressing it would break the source. When both options are disabled and you've opted out of every managed flow, the override stays off so dotnet behaves normally. + +The override only affects the *implicit* path. Explicit `dotnet dev-certs https` commands always work regardless of the environment variable, so a `syncContainerCert` flow that relies on a container-build step running `dotnet dev-certs https --trust` is unaffected. + ## User-managed certificates -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: +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 host user or workspace VS Code settings: ```json { @@ -208,10 +255,20 @@ The host extension can sync arbitrary host-side certificates into your dev conta 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. +### Per-entry fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | yes | Filename stem used inside the container (`{name}.pem`, etc.) and on the host's temp export directory. 1–64 chars from `[A-Za-z0-9._-]`, no leading dot, not `.` or `..`. Entries with an invalid name are rejected with an error notification and skipped. | +| `pfxPath` | one of | Absolute path on the host to a PFX/PKCS#12 file. | +| `pfxPassword` | optional | Password for the PFX (or, for `pemCertPath` sources, the password used when synthesizing the `.pfx` for extra destinations). | +| `pemCertPath` | one of | Absolute path on the host to a PEM-encoded certificate. | +| `pemKeyPath` | optional | Absolute path on the host to a PEM-encoded private key. Omit for CA-only entries. | +| `trustInContainer` | optional, default `true` | Plant the cert in the container's OpenSSL trust directory and .NET root store, so tools inside the container trust it. Set to `false` to sync the cert files without making the container trust the issuer. | +| `excludeFromDotNetStore` | optional, default `false` | When `installUserCertsToDotNetStore` is on globally, exempt this single cert from the `my/` write (avoids the password-stripping copy for sensitive entries). No effect when the global setting is `false`. | + ### Password handling The user's PFX password is preserved end-to-end. For `pfxPath` sources, the original file bytes are sent to the container verbatim — no decrypt-then-reencrypt round trip strips the password on the wire or on disk. For `pemCertPath` sources, the `pfxPassword` field doubles as the encryption password used to synthesize a `.pfx` for extra destinations; if unset, the synthesized `.pfx` is passwordless (matching the source PEM key file's on-disk posture — neither carries a password, so there's nothing to strip). @@ -230,13 +287,13 @@ If you specifically need `X509Store(StoreName.My, StoreLocation.CurrentUser)` en Setting this acknowledges that the in-container PFX copy is passwordless. The original source file on the host is untouched, and the password-preserving copies still flow to extra destinations. -To exempt an individual entry from the global setting (e.g., keep most of your user certs in the store but carve out one sensitive cert), add `"excludeFromDotNetStore": true` to that `userCertificates` entry. Has no effect when the global setting is `false` (no user certs go to the store anyway). +To exempt an individual entry from the global setting (e.g., keep most of your user certs in the store but carve out one sensitive cert), add `"excludeFromDotNetStore": true` to that `userCertificates` entry. The auto-generated dotnet-dev cert is always installed to the store regardless of these settings — it's intrinsically passwordless and the store IS its canonical location. ### Default Kestrel certificate (opt-in) -By default, Kestrel discovers the auto-generated dev cert through its X509Store fallback — no environment variables are set, and `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` remain untouched. If you'd like to pin a *custom* user certificate as Kestrel's default instead, add the following to your VS Code user `settings.json`: +By default, Kestrel discovers the auto-generated dev cert through its X509Store fallback — no environment variables are set, and `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` remain untouched. If you'd like to pin a *custom* user certificate as Kestrel's default instead, add the following to your host VS Code user `settings.json`: ```json { @@ -284,7 +341,7 @@ So with `extraCertDestinations = /etc/nginx/certs` and a user cert named `corp-c ## Syncing a certificate from the container to the host -By default the host is the source of truth: the host extension generates the ASP.NET dev cert, trusts it locally, then pushes it into every dev container that asks for it. That keeps the OS-level trust prompt count low — you confirm trust once on the host and every container reuses the same cert. +By default the host is the source of truth: the host extension generates the ASP.NET dev cert, trusts it locally, then pushes it into every Dev Container that asks for it. That keeps the OS-level trust prompt count low — you confirm trust once on the host and every container reuses the same cert. Some projects flip that around: the container's own build step (or a `dotnet dev-certs https` invocation baked into the image) is the canonical generator of the cert. In that case you can opt the container in to pushing its dev cert *to* the host, so the host trusts the container's cert instead of generating its own. @@ -308,7 +365,7 @@ With `syncContainerCert` enabled: - If a usable cert is found, the workspace extension pushes **just the public certificate** (PEM-encoded) to the host via a new IPC command. The private key never leaves the container — Kestrel keeps using its own copy of the key inside the container, and the host's job in this flow is to act purely as a trust anchor (so forwarded HTTPS ports and browser-side validation work on the host) rather than as a cert distribution point. If no usable cert is found, the push is a no-op — there's no fallback to host generation. - **`syncContainerCert: true` overrides the `generateDotNetCert` feature option for this container.** You don't need to also set `generateDotNetCert: false` to opt out of host generation — when the container is pushing its own cert to the host, the workspace extension drops the dotnet dev cert from its pull-from-host request automatically. (Otherwise the container would end up with both its own cert AND a different host-generated cert in its .NET store.) User-managed certificates configured via `userCertificates` are unaffected — those still flow normally. - The host extension independently re-validates the cert (same `isValidDevCert` rules; matches dev-cert OID, version, validity window). It then restricts SAN entries to local-only scopes by default — `localhost`, `*.localhost`, `*.dev.localhost`, `*.dev.internal`, `host.docker.internal`, `host.containers.internal`, IPv4 loopback / RFC1918 / link-local, IPv6 loopback / unique-local / link-local. A cert with SAN entries outside that set is rejected. -- If validation passes, the host shows a one-time modal consent prompt before adding the cert to the platform trust store. The OS-level trust prompt (macOS keychain dialog, Windows MMC dialog) fires for each unique cert as usual. The cert lands in the OS trust surfaces only — the .NET Root store on Linux, the login keychain's policy settings on macOS, CurrentUser/Root on Windows — never in `CurrentUser/My`, the keychain's identity slot, or the .NET store's `my/` directory. The host has nothing keyed by this thumbprint that contains a private key. +- If validation passes, the host shows a one-time modal consent prompt before adding the cert to the platform trust store. The OS-level trust prompt (macOS keychain dialog, Windows MMC dialog) fires for each unique cert as usual. The cert lands in the OS trust surfaces only — the .NET root store on Linux, the login keychain's policy settings on macOS, CurrentUser/Root on Windows — never in `CurrentUser/My`, the keychain's identity slot, or the .NET store's `my/` directory. The host has nothing keyed by this thumbprint that contains a private key. To allow SAN entries that aren't local (rare; security-sensitive — the cert will be trusted by your host browser for the listed names), opt in explicitly: @@ -322,37 +379,117 @@ When this is on, non-local SAN entries are shown in the consent modal so you can Pushes from a Dev Container without the matching feature option are ignored — the host setting on its own doesn't do anything until a container actively pushes. Host trust prompts fire per unique thumbprint, so opening multiple containers with different container-generated certs will accumulate trust prompts; this is intentional and is why the option isn't on by default. -## Development +## Troubleshooting -### Prerequisites +### Start with the logs -- Node.js 22+ -- Docker (for devcontainer testing) -- VS Code with the Dev Containers extension +Both extensions log to output channels (**View → Output**, then pick the channel from the dropdown): -### Building +| Channel | Where to look for it | +|---------|----------------------| +| **Dev Container Dev Certs** | Your local VS Code window — generation, host trust, and what was served to the container | +| **Dev Container Dev Certs (Remote)** | The Dev Container window — what was received, where each file was written, thumbprints considered | -Open the repo in VS Code and press F5. The `build-extensions` task will: +Failure notifications from the remote extension usually name which side to check. A failure to *obtain* certificates points at the host channel; a failure to *install* them points at the remote channel. -1. Build both TypeScript extensions with esbuild -2. Hydrate a test project from the template into `.out/test-project/` -3. Package the workspace extension VSIX into the test project's `.devcontainer/` +### Nothing happened at all + +Work through these in order: + +1. **Is the host extension installed?** The remote extension shows an **Install Host Extension** prompt when it can't reach the host side. If you dismissed it, install `dnegstad.devcontainer-dev-certs-host` on your local VS Code and reload. +2. **Is auto-injection on?** If `devcontainer-dev-certs.autoInject` is `false`, run **Dev Certs: Inject Certificate into Remote** from the Command Palette. +3. **Is provisioning on?** If `devcontainer-dev-certs.autoProvision` or `devcontainerDevCerts.generateDotNetCert` is `false` on the host, no managed dev cert is generated or served. +4. **Did the feature actually install?** Check `echo $SSL_CERT_DIR` in a container terminal — it should start with `~/.aspnet/dev-certs/trust`. If it's empty, the feature didn't run; rebuild the container. + +Running **Dev Certs: Inject Certificate into Remote** is the standard way to retry any of this without rebuilding. + +### The browser still warns on a forwarded port -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. +- **Linux host:** browsers use NSS, not the OS/OpenSSL trust stores. Run **Dev Certs: Trust Certificate in Browsers** and restart the browser — see "[Linux hosts: browser trust](#linux-hosts-browser-trust)". +- **Windows / macOS:** confirm you accepted the OS trust dialog on first use. If you dismissed it, the cert exists but isn't trusted — reload the window to re-trigger the flow. +- **Any host:** if the certificate was regenerated (expiry, a new container-pushed cert), browsers may hold the old one until restarted. + +### `curl` / `wget` fails inside the container + +Check that the trust directory has both the PEM and its hash symlink: + +```bash +ls -l ~/.aspnet/dev-certs/trust/ +echo $SSL_CERT_DIR +``` + +You should see `aspnetcore-localhost-{thumbprint}.pem` and a `{hash}.0` symlink pointing at it, and `SSL_CERT_DIR` should include that directory. If the directory is populated but `SSL_CERT_DIR` doesn't reference it, the shell probably didn't source `/etc/profile.d` — open a fresh VS Code terminal. + +If a *specific* tool still fails while `curl` works, it may read a bundle file rather than a directory; use [`extraCertDestinations`](#extra-destinations) to write a PEM where that tool expects one. + +### Kestrel doesn't pick up the certificate + +- Confirm the PFX is in `~/.dotnet/corefx/cryptography/x509stores/my/`. +- If more than one dev cert is present, Kestrel may select a different one. Run **Dev Certs: Clean Up Other Dev Certificates in Dev Container** — this is exactly what the stale-cert warning is for. +- If you disabled `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` handling or run in a `syncContainerCert` flow, a `dotnet run` may have written its own cert alongside ours; see "[Why `DOTNET_GENERATE_ASPNET_CERTIFICATE=false`](#why-dotnet_generate_aspnet_certificatefalse)". + +### A container-pushed certificate was rejected + +With `syncContainerCert: true`, the host rejects certs whose SAN entries fall outside local scopes, and rejects everything when `autoProvision` or `devcontainerDevCerts.generateDotNetCert` is `false`. The host output channel names the reason. If the SAN entries are legitimately non-local, see [`allowNonLocalContainerCertSans`](#syncing-a-certificate-from-the-container-to-the-host). ## Known issues ### podman: subsequent feature's `apt-get update` fails with `Couldn't create temporary file /tmp/apt.conf.XXX` -This is [containers/buildah#6747](https://github.com/containers/buildah/issues/6747): when a devcontainer feature is installed via the `RUN --mount=type=bind,target=/tmp/build-features-src/_0 ...` pattern the devcontainer CLI generates, buildah/podman can commit the resulting layer with `/tmp` reset from `1777 root:root` to `0755 root:root`. A later feature that calls `apt-get update` then fails because `_apt` (uid 42) can no longer write to `/tmp`. Docker is not affected. +This is [containers/buildah#6747](https://github.com/containers/buildah/issues/6747): when a Dev Container feature is installed via the `RUN --mount=type=bind,target=/tmp/build-features-src/_0 ...` pattern the devcontainer CLI generates, buildah/podman can commit the resulting layer with `/tmp` reset from `1777 root:root` to `0755 root:root`. A later feature that calls `apt-get update` then fails because `_apt` (uid 42) can no longer write to `/tmp`. Docker is not affected. This feature applies the workaround at the end of its own install script, so combinations like `devcontainer-dev-certs` + `azure-functions-core-tools` on podman work out of the box. If you hit the same class of failure with a *different* feature, copy the standalone [`examples/buildah-tmp-fix/`](examples/buildah-tmp-fix/) local feature into your `.devcontainer/` and place it in `devcontainer.json` between the offending feature and the consumer. +## Non-VS Code editors + +The companion extension pattern relies on VS Code's cross-host command routing, so JetBrains, Vim, and other editors can't use the automatic flow. For those, the repository ships a standalone helper script that performs the container-side install the remote extension would normally do: + +[`src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh`](src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh) + +**The feature does not copy this script into the container** — it's a manual fallback. Copy it into your image or bind-mount it, then invoke it yourself: + +```bash +# Single cert +setup-cert.sh + +# Multiple certs and/or extra destinations +setup-cert.sh --bundle-json +``` + +The bundle JSON form takes one or more certs plus optional extra destinations: + +```json +{ + "certs": [ + { + "name": "aspnetcore-dev", + "thumbprint": "ABC123...", + "pfxPath": "/abs/cert.pfx", + "pemPath": "/abs/cert.pem", + "pemKeyPath": "/abs/cert.key", + "rootPfxPath": "/abs/root.pfx", + "trustInContainer": true + } + ], + "extraDestinations": [ + { "path": "/etc/nginx/certs", "format": "pem" }, + { "path": "/var/app", "format": "pem-bundle" } + ] +} +``` + +`pemKeyPath` and `rootPfxPath` are optional — the root PFX is generated with `openssl` when omitted. The script honors `_REMOTE_USER` / `_REMOTE_USER_HOME` (defaulting to `vscode` / `/home/vscode`) to decide whose home directory receives the certs. + +**Requirements:** `openssl` must be installed for hash computation and PFX conversion; the `--bundle-json` form additionally requires `jq`. + +You still need to produce the certificate material yourself on the host (for example with `dotnet dev-certs https --export-path`) and get it into the container — the script handles placement and trust, not transfer. + ## Limitations - **Auto-generated dev cert matches .NET's format only.** The `generateDotNetCert` flow produces a cert identical to `dotnet dev-certs https` (specific OID marker, subject, SAN entries). To sync differently-shaped certs (corporate CAs, custom wildcard certs, etc.), add them via the `devcontainerDevCerts.userCertificates` VS 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.sh` fallback script (with a `--bundle-json` form for multi-cert bundles) for manual use. +- **VS Code only.** The companion extension pattern relies on VS Code's cross-host command routing. Other editors (JetBrains, Vim, etc.) are not supported automatically; see "[Non-VS Code editors](#non-vs-code-editors)" for the manual fallback. - **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. +- **Linux browser trust is a separate step.** Firefox and Chromium on Linux don't read the OS/OpenSSL trust stores; see "[Linux hosts: browser trust](#linux-hosts-browser-trust)". ## Supported Platforms @@ -362,3 +499,76 @@ This feature applies the workaround at the end of its own install script, so com | macOS | x64, ARM64 | | Linux (glibc) | x64, ARM64 | | Linux (musl/Alpine) | x64 | + +## Development + +### Prerequisites + +- Node.js 22+ +- Docker (for Dev Container testing) +- VS Code with the Dev Containers extension + +### Building + +Open the repo in VS Code and press F5. The `build-extensions` task will: + +1. Build both TypeScript extensions with esbuild +2. Hydrate a test project from the template into `.out/test-project/` +3. 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 Dev Container flow, reopen `.out/test-project/` in a container. + +### Repository Layout + +``` +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/paths.ts .NET store and OpenSSL trust directory paths + + devcontainer-feature/ Dev Container feature + src/devcontainer-dev-certs/ + devcontainer-feature.json Feature metadata, options, extension references + install.sh Container build-time setup (creates directories) + scripts/setup-cert.sh Manual fallback installer for non-VS Code editors + +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) +``` + +## Verifying Release Provenance + +Starting with **v1.0.0**, each release publishes [SLSA build provenance](https://slsa.dev/spec/v1.0/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`](https://cli.github.com/manual/gh_attestation_verify) command: + +- **Dev Container feature.** The provenance attestation is pushed to GHCR alongside the OCI artifact: + ```bash + gh attestation verify \ + oci://ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs: \ + --repo dnegstad/devcontainer-dev-certs + ``` +- **Extension VSIXes.** Both `dnegstad.devcontainer-dev-certs-host` and `dnegstad.devcontainer-dev-certs-remote` VSIXes are attached as assets to each [GitHub Release](https://github.com/dnegstad/devcontainer-dev-certs/releases) with attestations stored on GitHub: + ```bash + gh attestation verify .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. diff --git a/src/vscode-ui-extension/README.md b/src/vscode-ui-extension/README.md index ae8751f..e06d64c 100644 --- a/src/vscode-ui-extension/README.md +++ b/src/vscode-ui-extension/README.md @@ -4,6 +4,8 @@ Automatically generate, trust, and share HTTPS development certificates with Dev This is the **host companion** extension. It runs on your local machine and works together with [Dev Container Dev Certificates (Remote)](https://marketplace.visualstudio.com/items?itemName=dnegstad.devcontainer-dev-certs-remote) to enable trusted HTTPS across the host/remote boundary. +📖 **Full documentation:** [github.com/dnegstad/devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs) + ## The Problem When developing inside Dev Containers, HTTPS certificates are a persistent pain point: @@ -52,46 +54,15 @@ The feature declares both companion extensions and configures the container's tr 4. ASP.NET, Aspire, and other services discover the cert automatically — no environment variables or manual configuration needed 5. Your host browser trusts the cert on forwarded ports -## How It Works - -### Certificate Generation - -The extension generates certificates using Node's built-in `crypto` plus `@peculiar/x509` (X.509) and `pkijs` (PKCS#12) — no .NET installation required on your host. The auto-generated certificate is compatible with the [ASP.NET dev cert format](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs) and includes: - -- Subject: `CN=localhost` -- SAN: `localhost`, `*.dev.localhost`, `*.dev.internal`, `host.docker.internal`, `host.containers.internal`, `127.0.0.1`, `::1` -- The ASP.NET Core HTTPS development certificate OID marker (`1.3.6.1.4.1.311.84.1.1`, version 6) -- RSA 2048-bit key, SHA-256 signature, 365-day validity - -This means `dotnet dev-certs https --check` recognizes it as a valid dev cert, and [Aspire](https://aspire.dev/app-host/certificate-configuration/) uses it for orchestrated service-to-service HTTPS — including non-.NET services. - -### Host Trust - -The cert is trusted in your OS certificate store: - -| Platform | Trust Mechanism | -|----------|----------------| -| Windows | Added to `CurrentUser\Root` (triggers system dialog on first use) | -| macOS | Added to login keychain via `security add-trusted-cert` | -| Linux | Added to .NET Root store + OpenSSL trust directory + `SSL_CERT_DIR` in VS Code terminals | - -### Linux Trust Details - -On Linux, host trust involves three layers: - -1. **`.NET Root store`** (`~/.dotnet/corefx/cryptography/x509stores/root/`) — trusted automatically by the .NET runtime -2. **`OpenSSL trust directory`** (`~/.aspnet/dev-certs/trust/`) — PEM certificate with hash symlinks for OpenSSL-based tools -3. **`SSL_CERT_DIR` in VS Code terminals** — the extension prepends the trust directory to `SSL_CERT_DIR` in VS Code's integrated terminal environment, so `curl`, `wget`, and other CLI tools trust the cert automatically +**On a Linux host, step 5 needs one extra command** — see below. -For tools running **outside** VS Code integrated terminals (e.g., a system terminal), set `SSL_CERT_DIR` manually: +## Linux browser trust -```bash -export SSL_CERT_DIR="$HOME/.aspnet/dev-certs/trust${SSL_CERT_DIR:+:$SSL_CERT_DIR}" -``` +On Windows and macOS, trusting the certificate in the OS store is enough for browsers. You can skip this section. -#### Browser Trust (Firefox / Chromium) +On Linux, Firefox and Chromium use [NSS](https://firefox-source-docs.mozilla.org/security/nss/) for certificate trust — they do **not** read from OpenSSL trust directories or the .NET root store. Without this step, forwarded ports still show a certificate warning in the browser even though `curl` works. -Firefox and Chromium on Linux use [NSS](https://firefox-source-docs.mozilla.org/security/nss/) for certificate trust — they do **not** read from OpenSSL trust directories. This extension provides a **"Dev Certs: Trust Certificate in Browsers"** command (available from the Command Palette) that: +Run **Dev Certs: Trust Certificate in Browsers** from the Command Palette (`F1`). It: - Checks for `certutil` (from `libnss3-tools`) on the PATH - If available, automatically trusts the cert in Chromium (`~/.pki/nssdb/`) and Firefox profile NSS databases @@ -107,18 +78,30 @@ To install `certutil`: To import manually in Firefox: **Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import**, then select the PEM file from `~/.aspnet/dev-certs/trust/`. -> **Note:** The default trust directory is `~/.aspnet/dev-certs/trust/`. This can be overridden with the `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY` environment variable. +Restart the browser afterwards so it picks up the updated NSS database. -### Cross-Host Transfer +## Commands -Certificate material (PFX + PEM) is serialized as base64 and transferred via `vscode.commands.executeCommand()`, which VS Code routes transparently between extension hosts. No Docker commands, no file mounts — this works for any VS Code remote scenario. +Available from the Command Palette (`F1`): -### Container Trust +| Command | Command ID | Description | +|---------|------------|-------------| +| **Dev Certs: Trust Certificate in Browsers** | `devcontainer-dev-certs.trustInBrowsers` | Trust the dev cert in Firefox / Chromium NSS databases. Linux only — on Windows and macOS this reports that the OS handles browser trust automatically. | -Inside the container, the remote extension places the cert in two locations: +The remote companion extension contributes the in-container commands (**Dev Certs: Inject Certificate into Remote** and **Dev Certs: Clean Up Other Dev Certificates in Dev Container**); they appear in the Command Palette when a Dev Container window is focused. -- **`~/.dotnet/corefx/cryptography/x509stores/my/{thumbprint}.pfx`** — ASP.NET's X509Store fallback discovers it automatically -- **`~/.aspnet/dev-certs/trust/`** — PEM + OpenSSL hash symlinks, included in `SSL_CERT_DIR` so `curl`, `wget`, and other OpenSSL-based tools trust it +## Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `devcontainer-dev-certs.autoProvision` | `true` | Allow certificate provisioning when the workspace extension requests one. On first use, a consent prompt explains what will happen before any certificates are generated. Set to `false` to disable provisioning entirely (host-generation AND acceptance of container-pushed certs). | +| `devcontainerDevCerts.generateDotNetCert` | `true` | Auto-generate the ASP.NET / Aspire compatible HTTPS dev cert and trust it in the host OS store. When `false`, user-managed certificates (if any) are still synced, but no managed dev cert lives on the host — also implicitly disables acceptance of container-pushed dev certs. | +| `devcontainerDevCerts.userCertificates` | `[]` | Host-managed certificates to sync from the host into Dev Containers (see "User-managed certificates" below). | +| `devcontainerDevCerts.installUserCertsToDotNetStore` | `false` | When `true`, also copies every entry from `userCertificates` into the container's .NET X509Store. **Security note:** the on-disk PFX there is passwordless (Linux's `StoreName.My` enumeration can't accept per-file passwords), so opting in strips your user cert's password on the in-container copy. Per-entry exemption via `excludeFromDotNetStore: true`. The auto-generated dotnet-dev cert is always installed to the store regardless. | +| `devcontainerDevCerts.defaultKestrelCertificate` | `""` | Name of a `userCertificates` entry to use as the default Kestrel certificate inside Dev Containers. When set, the remote extension writes that cert's PFX to `~/.aspnet/dev-certs/https/kestrel-default.pfx` and exports `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` to processes launched from VS Code (terminals, debug, tasks). Leave empty to opt out — Kestrel will still discover the auto-generated dev cert via X509Store. | +| `devcontainerDevCerts.allowNonLocalContainerCertSans` | `false` | When accepting a Dev Container-managed dev certificate (via `syncContainerCert`), override the default SAN restriction that limits trusted certificates to localhost / loopback / private IPs / docker host names. Only enable when you fully understand the SAN entries the container will push. Has no effect when `generateDotNetCert` or `autoProvision` is `false`. | + +> **Note:** `devcontainerDevCerts.generateDotNetCert` (this host setting) and the Dev Container feature's `generateDotNetCert` option are different knobs. The host setting decides whether a managed dev cert exists on your machine at all; the feature option decides whether a given container pulls it in. ## User-managed certificates @@ -135,16 +118,16 @@ In addition to the auto-generated dev cert, you can sync arbitrary host-side cer Each entry supplies exactly one of `pfxPath` (+ optional `pfxPassword`) or `pemCertPath` (+ optional `pemKeyPath`). Omitting the key produces a CA-only entry. User-managed certificates are **never** added to the host OS trust store — the assumption is you already trust them on the host if you're syncing them in. PFX passwords are preserved end-to-end (no decrypt/re-encrypt round-trip strips them on the wire). -Per-entry options: +Per-entry fields: | Field | Required | Description | |-------|----------|-------------| -| `name` | yes | Filename stem used inside the container (`{name}.pem`, etc.). 1-64 chars from `[A-Za-z0-9._-]`, no leading dot. | +| `name` | yes | Filename stem used inside the container (`{name}.pem`, etc.). 1–64 chars from `[A-Za-z0-9._-]`, no leading dot. | | `pfxPath` | one of | Absolute path on the host to a PFX/PKCS#12 file. | | `pfxPassword` | optional | Password for the PFX (or, for `pemCertPath` sources, the password used when synthesizing the `.pfx` for extra destinations). | | `pemCertPath` | one of | Absolute path on the host to a PEM-encoded certificate. | | `pemKeyPath` | optional | Absolute path on the host to a PEM-encoded private key. Omit for CA-only entries. | -| `trustInContainer` | optional, default `true` | Plant the cert in the container's OpenSSL trust dir + .NET Root store. | +| `trustInContainer` | optional, default `true` | Plant the cert in the container's OpenSSL trust dir + .NET root store. | | `excludeFromDotNetStore` | optional, default `false` | When `installUserCertsToDotNetStore` is on globally, exempt this single cert from the `my/` write (avoids the password-stripping copy for sensitive entries). | ## Container-to-host sync (opt-in) @@ -159,7 +142,7 @@ The default flow is host-as-source. If you have a Dev Container that already pro 2. No new host setting is needed — the same `devcontainerDevCerts.generateDotNetCert` + `devcontainer-dev-certs.autoProvision` settings that gate host-generation also gate accepting a container-pushed cert. The host independently re-validates the incoming certificate (ASP.NET dev-cert format + local-only SAN entries by default) and prompts you once before trusting it. -The host only ever trusts the **public** certificate — the private key never leaves the container. The trust step goes through the same OS-level path host-generated certs use: on Linux that includes the .NET Root store, the OpenSSL trust directory, and NSS browser databases; on macOS the login keychain trust policy; on Windows `CurrentUser/Root` via `certutil`. The cert is NOT written to `CurrentUser/My`, the macOS keychain identity slot, or the .NET `my/` directory. +The host only ever trusts the **public** certificate — the private key never leaves the container. The trust step goes through the same OS-level path host-generated certs use: on Linux that includes the .NET root store, the OpenSSL trust directory, and NSS browser databases; on macOS the login keychain trust policy; on Windows `CurrentUser/Root` via `certutil`. The cert is NOT written to `CurrentUser/My`, the macOS keychain identity slot, or the .NET `my/` directory. By default the host rejects any cert whose SAN entries reach outside well-known local scopes: @@ -169,20 +152,9 @@ By default the host rejects any cert whose SAN entries reach outside well-known This matches the SAN set baked into the default ASP.NET dev cert. To allow a cert with SAN entries outside this set (rare; security-sensitive), set `devcontainerDevCerts.allowNonLocalContainerCertSans: true`. The consent prompt surfaces every non-local SAN entry when the override is enabled so you can see exactly what you're agreeing to trust. -## Settings - -| Setting | Default | Description | -|---------|---------|-------------| -| `devcontainer-dev-certs.autoProvision` | `true` | Allow certificate provisioning when the workspace extension requests one. On first use, a consent prompt explains what will happen before any certificates are generated. Set to `false` to disable provisioning entirely (host-generation AND acceptance of container-pushed certs). | -| `devcontainerDevCerts.generateDotNetCert` | `true` | Auto-generate the ASP.NET / Aspire compatible HTTPS dev cert and trust it in the host OS store. When `false`, user-managed certificates (if any) are still synced, but no managed dev cert lives on the host — also implicitly disables acceptance of container-pushed dev certs. | -| `devcontainerDevCerts.userCertificates` | `[]` | Host-managed certificates to sync from the host into Dev Containers (see "User-managed certificates" above). | -| `devcontainerDevCerts.installUserCertsToDotNetStore` | `false` | When `true`, also copies every entry from `userCertificates` into the container's .NET X509Store. **Security note:** the on-disk PFX there is passwordless (Linux's `StoreName.My` enumeration can't accept per-file passwords), so opting in strips your user cert's password on the in-container copy. Per-entry exemption via `excludeFromDotNetStore: true`. The auto-generated dotnet-dev cert is always installed to the store regardless. | -| `devcontainerDevCerts.defaultKestrelCertificate` | `""` | Name of a `userCertificates` entry to use as the default Kestrel certificate inside Dev Containers. When set, the remote extension writes that cert's PFX to `~/.aspnet/dev-certs/https/kestrel-default.pfx` and exports `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` to processes launched from VS Code (terminals, debug, tasks). Leave empty to opt out — Kestrel will still discover the auto-generated dev cert via X509Store. | -| `devcontainerDevCerts.allowNonLocalContainerCertSans` | `false` | When accepting a Dev Container-managed dev certificate (via `syncContainerCert`), override the default SAN restriction that limits trusted certificates to localhost / loopback / private IPs / docker host names. Only enable when you fully understand the SAN entries the container will push. Has no effect when `generateDotNetCert` or `autoProvision` is `false`. | - -## Dev Container Feature Options +## Dev Container feature options -These options are set on the feature in your project's `devcontainer.json` (not in VS Code settings) and configure the container side. The feature declares and installs both companion extensions, so you don't list them yourself. +Container-side behavior is configured on the feature in your project's `devcontainer.json` (not in VS Code settings). The feature declares and installs both companion extensions, so you don't list them yourself. ```json { @@ -194,19 +166,73 @@ These options are set on the feature in your project's `devcontainer.json` (not } ``` +These are the options that change what this extension does: + | 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 if your base image uses non-standard paths. | -| `pruneMissingCertDirs` | `true` | Filter out directories that don't exist on this image from `sslCertDirs` before writing `SSL_CERT_DIR` (some TLS stacks error on a missing entry). Set to `false` to use `sslCertDirs` verbatim — e.g. when a directory is created after install but before it's needed. | | `generateDotNetCert` | `true` | Pull the host-generated ASP.NET dev cert into this container. Set to `false` if you're only using user-managed certs in this container. | | `syncUserCertificates` | `true` | Per-container opt-out for syncing certs configured in the host `devcontainerDevCerts.userCertificates` setting. | | `syncContainerCert` | `false` | Opt in to pushing the container's own dev cert to the host (reverse-sync). When true, also implicitly overrides `generateDotNetCert` — you don't need to set both. | -| `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `[=]` where format is `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). Useful for non-.NET workloads (nginx, Java keystores, etc.). | -## Avoiding dotnet's first-run cert auto-generation race +The feature also accepts `trustNss`, `sslCertDirs`, `pruneMissingCertDirs`, and `extraCertDestinations`, which only affect the container side. See the [full option reference](https://github.com/dnegstad/devcontainer-dev-certs#dev-container-feature-options). + +When the host is the dev cert source (the default), the feature additionally sets `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` inside the container so dotnet's implicit first-run cert provisioning doesn't race the workspace extension's install. It's only applied when `generateDotNetCert: true` AND `syncContainerCert: false`, and explicit `dotnet dev-certs https` commands are unaffected either way. [Details](https://github.com/dnegstad/devcontainer-dev-certs#why-dotnet_generate_aspnet_certificatefalse). + +## How It Works + +### Certificate Generation + +The extension generates certificates using Node's built-in `crypto` plus `@peculiar/x509` (X.509) and `pkijs` (PKCS#12) — no .NET installation required on your host. The auto-generated certificate is compatible with the [ASP.NET dev cert format](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs) and includes: + +- Subject: `CN=localhost` +- SAN: `localhost`, `*.dev.localhost`, `*.dev.internal`, `host.docker.internal`, `host.containers.internal`, `127.0.0.1`, `::1` +- The ASP.NET Core HTTPS development certificate OID marker (`1.3.6.1.4.1.311.84.1.1`, version 6) +- RSA 2048-bit key, SHA-256 signature, 365-day validity + +This means `dotnet dev-certs https --check` recognizes it as a valid dev cert, and [Aspire](https://aspire.dev/app-host/certificate-configuration/) uses it for orchestrated service-to-service HTTPS — including non-.NET services. + +### Host Trust + +The cert is trusted in your OS certificate store: + +| Platform | Trust Mechanism | +|----------|----------------| +| Windows | Added to `CurrentUser\Root` (triggers system dialog on first use) | +| macOS | Added to login keychain via `security add-trusted-cert` | +| Linux | Added to .NET root store + OpenSSL trust directory + `SSL_CERT_DIR` in VS Code terminals | + +On Linux, host trust involves three layers: + +1. **.NET root store** (`~/.dotnet/corefx/cryptography/x509stores/root/`) — trusted automatically by the .NET runtime +2. **OpenSSL trust directory** (`~/.aspnet/dev-certs/trust/`) — PEM certificate with hash symlinks for OpenSSL-based tools +3. **`SSL_CERT_DIR` in VS Code terminals** — the extension prepends the trust directory to `SSL_CERT_DIR` in VS Code's integrated terminal environment, so `curl`, `wget`, and other CLI tools trust the cert automatically + +Browsers are a fourth, separate layer on Linux — see "[Linux browser trust](#linux-browser-trust)". + +For tools running **outside** VS Code integrated terminals (e.g., a system terminal), set `SSL_CERT_DIR` manually: + +```bash +export SSL_CERT_DIR="$HOME/.aspnet/dev-certs/trust${SSL_CERT_DIR:+:$SSL_CERT_DIR}" +``` + +> **Note:** The default trust directory is `~/.aspnet/dev-certs/trust/`. This can be overridden with the `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY` environment variable. + +### Cross-Host Transfer + +Certificate material (PFX + PEM) is serialized as base64 and transferred via `vscode.commands.executeCommand()`, which VS Code routes transparently between extension hosts. No Docker commands, no file mounts — this works for any VS Code remote scenario. + +### Container Trust + +Inside the container, the remote extension places the cert in two locations: + +- **`~/.dotnet/corefx/cryptography/x509stores/my/{thumbprint}.pfx`** — ASP.NET's X509Store fallback discovers it automatically +- **`~/.aspnet/dev-certs/trust/`** — PEM + OpenSSL hash symlinks, included in `SSL_CERT_DIR` so `curl`, `wget`, and other OpenSSL-based tools trust it + +## Troubleshooting + +This extension logs to the **Dev Container Dev Certs** output channel (**View → Output**, then pick it from the dropdown) — generation, host trust, and what was served to each container. The remote companion logs separately to **Dev Container Dev Certs (Remote)** in the Dev Container window. -When the host is the dev cert source (default), the devcontainer feature also sets `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` inside the container so dotnet's implicit first-run cert provisioning doesn't race the workspace extension's install of OUR cert. The override is gated — it's only applied when `generateDotNetCert: true` AND `syncContainerCert: false`, so it doesn't break the `syncContainerCert` flow when the container relies on dotnet's auto-gen as its source. Explicit `dotnet dev-certs https` commands are unaffected in either case. +See the [troubleshooting guide](https://github.com/dnegstad/devcontainer-dev-certs#troubleshooting) for the common failure modes. ## Requirements diff --git a/src/vscode-workspace-extension/README.md b/src/vscode-workspace-extension/README.md index 0b3344d..9e6002a 100644 --- a/src/vscode-workspace-extension/README.md +++ b/src/vscode-workspace-extension/README.md @@ -4,6 +4,8 @@ Automatically receive and install HTTPS development certificates inside Dev Cont This is the **remote companion** extension. It runs inside the remote environment (Dev Container, SSH, WSL) and works together with [Dev Container Dev Certificates (Host)](https://marketplace.visualstudio.com/items?itemName=dnegstad.devcontainer-dev-certs-host) to enable trusted HTTPS across the host/remote boundary. +📖 **Full documentation:** [github.com/dnegstad/devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs) + ## The Problem When developing inside Dev Containers, HTTPS certificates are a persistent pain point: @@ -55,20 +57,28 @@ The feature declares both companion extensions and configures the container's tr If the host companion extension is missing when this extension activates, you'll see an **Install Host Extension** prompt that installs it with one click. +### Verify it worked + +```bash +ls ~/.dotnet/corefx/cryptography/x509stores/my/ # expect {thumbprint}.pfx +ls ~/.aspnet/dev-certs/trust/ # expect the PEM plus a {hash}.0 symlink +dotnet dev-certs https --check # if the .NET SDK is present +``` + +Then call your app over HTTPS from inside the container without `-k`: `curl https://localhost:`. + ## What This Extension Does On activation in a remote context, this extension: 1. **Requests certificate material** from the host companion extension via `vscode.commands.executeCommand()` (routed transparently across VS Code extension hosts). Calls `getAllCertMaterialV3` for the modern multi-cert bundle and falls back to older single-cert endpoints if the host extension is on an older version. -2. **Installs each certificate in the bundle** — the auto-generated dotnet dev cert plus any user-managed certs the host is configured to sync. The PFX lands in `~/.dotnet/corefx/cryptography/x509stores/my/{thumbprint}.pfx` so ASP.NET's `GetDevelopmentCertificateFromStore()` discovers it; the PEM lands in `~/.aspnet/dev-certs/trust/` with OpenSSL hash symlinks (c_rehash implemented in pure TypeScript — no `openssl` binary required); a public-cert-only PFX lands in the .NET Root store so dotnet reports the cert as trusted. +2. **Installs each certificate in the bundle** — the auto-generated dotnet dev cert plus any user-managed certs the host is configured to sync. The PFX lands in `~/.dotnet/corefx/cryptography/x509stores/my/{thumbprint}.pfx` so ASP.NET's `GetDevelopmentCertificateFromStore()` discovers it; the PEM lands in `~/.aspnet/dev-certs/trust/` with OpenSSL hash symlinks (c_rehash implemented in pure TypeScript — no `openssl` binary required); a public-cert-only PFX lands in the .NET root store so dotnet reports the cert as trusted. 3. **Writes user-managed certs to extra destinations** when configured — each entry in `extraCertDestinations` becomes a directory inside the container the extension writes per-cert `{name}.pem` / `.key` / `.pfx` / `-bundle.pem` files to (useful for nginx, Java keystores, requests bundles, etc.). 4. **Pushes the container's own dev cert to the host** when `syncContainerCert` is enabled — the optional reverse-sync flow (see below). 5. **Surfaces a one-time cleanup prompt** when it detects other ASP.NET dev cert artifacts in the container's .NET store alongside the managed one, so leftover certs from previous installs don't confuse Kestrel's selection logic. OpenSSL-based tools (`curl`, `wget`, etc.) trust the cert because the Dev Container feature points `SSL_CERT_DIR` at the trust directory (see [OpenSSL Trust](#openssl-trust)). -If the host companion extension is not installed, this extension prompts you to install it with a single click. - ### ASP.NET and Aspire Certificate Discovery No `ASPNETCORE_Kestrel__Certificates__Default__Path` or other environment variables are needed by default. ASP.NET's startup chain falls back to reading the .NET X509Store, where it finds the PFX by its [ASP.NET dev cert OID marker](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs). This works for standalone ASP.NET services, [Aspire-orchestrated applications](https://aspire.dev/app-host/certificate-configuration/) (including non-.NET services), and any other workload that uses the ASP.NET dev cert format. @@ -83,9 +93,9 @@ This means any tool or service that uses OpenSSL for TLS verification — regard ### Container-to-host reverse sync (opt-in) -When `syncContainerCert: true` is set on the devcontainer feature, this extension runs an additional step **before** the standard pull: it scans `~/.dotnet/corefx/cryptography/x509stores/my/*.pfx` inside the container for a valid ASP.NET dev cert (same classify-and-select-best rules the host uses on its own stores), and if it finds one, pushes the **public** cert (PEM-encoded — no PFX, no private key) to the host extension to trust. Useful when the container itself produces the dev cert (e.g. baked into the image with `dotnet dev-certs https`, or generated on first run by Aspire AppHost). +When `syncContainerCert: true` is set on the Dev Container feature, this extension runs an additional step **before** the standard pull: it scans `~/.dotnet/corefx/cryptography/x509stores/my/*.pfx` inside the container for a valid ASP.NET dev cert (same classify-and-select-best rules the host uses on its own stores), and if it finds one, pushes the **public** cert (PEM-encoded — no PFX, no private key) to the host extension to trust. Useful when the container itself produces the dev cert (e.g. baked into the image with `dotnet dev-certs https`, or generated on first run by Aspire AppHost). -Enabling `syncContainerCert` implicitly overrides `generateDotNetCert` for that container — the workspace extension will skip pulling a host-generated dotnet dev cert, since the container is the source. User-managed certificates configured via `userCertificates` are still pulled normally. +Enabling `syncContainerCert` implicitly overrides `generateDotNetCert` for that container — this extension will skip pulling a host-generated dotnet dev cert, since the container is the source. User-managed certificates configured via `userCertificates` are still pulled normally. The host independently re-validates the pushed cert and rejects any SAN entry outside well-known local scopes (loopback / RFC1918 private IPs / docker host names / `.localhost` / `.dev.localhost` / `.dev.internal` / IPv6 ULA + link-local). An explicit `devcontainerDevCerts.allowNonLocalContainerCertSans` host setting is available for cases that legitimately need non-local SANs. @@ -95,10 +105,12 @@ If after install this extension finds dev cert PFXes in the container's .NET sto ## Commands -| Command | Description | -|---------|-------------| -| `devcontainer-dev-certs.injectCert` | Manually re-run the certificate injection flow (normally automatic on activation). | -| `devcontainer-dev-certs.cleanupStaleDevCerts` | Sweep dev cert artifacts in the container's .NET stores that aren't the extension-managed one, preserving the managed cert. | +Available from the Command Palette (`F1`) in a Dev Container window: + +| Command | Command ID | Description | +|---------|------------|-------------| +| **Dev Certs: Inject Certificate into Remote** | `devcontainer-dev-certs.injectCert` | Manually re-run the certificate injection flow (normally automatic on activation). Use it when `autoInject` is off, or to retry after a failure without rebuilding. | +| **Dev Certs: Clean Up Other Dev Certificates in Dev Container** | `devcontainer-dev-certs.cleanupStaleDevCerts` | Sweep dev cert artifacts in the container's .NET stores that aren't the extension-managed one, preserving the managed cert. | ## Settings @@ -107,9 +119,11 @@ If after install this extension finds dev cert PFXes in the container's .NET sto | `devcontainer-dev-certs.autoInject` | `true` | Automatically inject the cert when a remote session starts. Set to `false` to require manually invoking "Dev Certs: Inject Certificate into Remote". | | `devcontainer-dev-certs.warnOnStaleDevCerts` | `true` | Show the post-install warning when other dev certs are detected alongside the managed one. Set to `false` to silence the prompt; the cleanup command stays available from the Command Palette either way. | -## Dev Container Feature Options +Certificate *content* — which certs exist and get synced — is configured on the host side (`devcontainerDevCerts.userCertificates`, `defaultKestrelCertificate`, and friends). See the [host extension](https://marketplace.visualstudio.com/items?itemName=dnegstad.devcontainer-dev-certs-host) or the [configuration reference](https://github.com/dnegstad/devcontainer-dev-certs#configuration-reference). -When using the Dev Container feature, these options are available: +## Dev Container feature options + +Container-side behavior is configured on the feature in your project's `devcontainer.json`: ```json { @@ -121,28 +135,33 @@ When using the Dev Container feature, these options are available: } ``` +These are the options that change what this extension does: + | 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 if your base image uses non-standard paths. | -| `pruneMissingCertDirs` | `true` | Filter out directories that don't exist on this image from `sslCertDirs` before writing `SSL_CERT_DIR` (some TLS stacks error on a missing entry). Set to `false` to use `sslCertDirs` verbatim — e.g. when a directory is created after install but before it's needed. | | `generateDotNetCert` | `true` | Pull the host-generated ASP.NET dev cert into this container. Set to `false` if you're only using user-managed certs in this container. | | `syncUserCertificates` | `true` | Per-container opt-out for syncing certs configured in the host `devcontainerDevCerts.userCertificates` setting. | | `syncContainerCert` | `false` | Opt in to pushing the container's own dev cert to the host (reverse-sync). When true, also implicitly overrides `generateDotNetCert` — you don't need to set both. | | `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `[=]` where format is `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). Useful for non-.NET workloads (nginx, Java keystores, etc.). | -### dotnet first-run cert auto-generation suppression - -When the host is the dev cert source (`generateDotNetCert: true` and `syncContainerCert: false` — the default), the feature also sets `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` inside the container. This prevents the first `dotnet run` of an HTTPS-enabled project from racing this extension's install of OUR cert (which would otherwise leave a "partially valid certificate on first run" combo where TLS works for some clients and fails for others). The override is **only** set when the host is the source — `syncContainerCert: true` flows that might rely on dotnet's implicit auto-gen as their source are unaffected. Explicit `dotnet dev-certs https` commands always work regardless. +The feature also accepts `trustNss`, `sslCertDirs`, and `pruneMissingCertDirs`, which configure the container's trust infrastructure at build time. See the [full option reference](https://github.com/dnegstad/devcontainer-dev-certs#dev-container-feature-options). ## Environment Variables -This extension honors the following environment variables, matching the behavior of the official .NET `CertificateManager`: +The Dev Container feature sets `SSL_CERT_DIR` (plus `DEVCONTAINER_DEV_CERTS_*` vars describing the selected options) at build time. When the host is the dev cert source — `generateDotNetCert: true` and `syncContainerCert: false`, the default — it also sets `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` so the first `dotnet run` of an HTTPS-enabled project doesn't race this extension's install with a cert of its own. Explicit `dotnet dev-certs https` commands always work regardless. [Details](https://github.com/dnegstad/devcontainer-dev-certs#why-dotnet_generate_aspnet_certificatefalse). + +This extension additionally honors, matching the behavior of the official .NET `CertificateManager`: | Variable | Description | |----------|-------------| | `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY` | Override the default OpenSSL trust directory (`~/.aspnet/dev-certs/trust/`) | +## Troubleshooting + +This extension logs to the **Dev Container Dev Certs (Remote)** output channel (**View → Output**, then pick it from the dropdown) — what was received from the host, where each file was written, and which thumbprints were considered. The host companion logs separately to **Dev Container Dev Certs** in your local VS Code window; a failure to *obtain* certificates is usually explained there. + +If nothing happened at all, check that the host extension is installed, that `autoInject` is on (or run **Dev Certs: Inject Certificate into Remote**), and that `echo $SSL_CERT_DIR` in a container terminal starts with `~/.aspnet/dev-certs/trust`. See the [troubleshooting guide](https://github.com/dnegstad/devcontainer-dev-certs#troubleshooting) for the full list. + ## Requirements - VS Code 1.100 or later From 1f1b99b057569b98d2ac7d6b9186923ae4521f4a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 02:51:15 +0000 Subject: [PATCH 2/2] docs: drop the setup-cert.sh fallback section from the README The script isn't installed into the container by the feature and isn't reachable in a usable form today, so documenting it as a non-VS Code path promised support that doesn't currently exist. Removes the section, its Contents entry, and the Repository Layout line. Limitations now states plainly that VS Code is the only supported editor. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KjJpJFDYwZTq7oYF7zLAkH --- README.md | 48 +----------------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/README.md b/README.md index 6d4e7f8..d79241b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ No `dotnet dev-certs` commands, no manual PFX exports, no environment variable c - [Syncing a certificate from the container to the host](#syncing-a-certificate-from-the-container-to-the-host) — reverse sync (opt-in) - [Troubleshooting](#troubleshooting) — logs and the common failure modes - [Known issues](#known-issues) -- [Non-VS Code editors](#non-vs-code-editors) — the manual fallback script - [Limitations](#limitations) · [Supported Platforms](#supported-platforms) - [Development](#development) · [Verifying Release Provenance](#verifying-release-provenance) @@ -440,54 +439,10 @@ This is [containers/buildah#6747](https://github.com/containers/buildah/issues/6 This feature applies the workaround at the end of its own install script, so combinations like `devcontainer-dev-certs` + `azure-functions-core-tools` on podman work out of the box. If you hit the same class of failure with a *different* feature, copy the standalone [`examples/buildah-tmp-fix/`](examples/buildah-tmp-fix/) local feature into your `.devcontainer/` and place it in `devcontainer.json` between the offending feature and the consumer. -## Non-VS Code editors - -The companion extension pattern relies on VS Code's cross-host command routing, so JetBrains, Vim, and other editors can't use the automatic flow. For those, the repository ships a standalone helper script that performs the container-side install the remote extension would normally do: - -[`src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh`](src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh) - -**The feature does not copy this script into the container** — it's a manual fallback. Copy it into your image or bind-mount it, then invoke it yourself: - -```bash -# Single cert -setup-cert.sh - -# Multiple certs and/or extra destinations -setup-cert.sh --bundle-json -``` - -The bundle JSON form takes one or more certs plus optional extra destinations: - -```json -{ - "certs": [ - { - "name": "aspnetcore-dev", - "thumbprint": "ABC123...", - "pfxPath": "/abs/cert.pfx", - "pemPath": "/abs/cert.pem", - "pemKeyPath": "/abs/cert.key", - "rootPfxPath": "/abs/root.pfx", - "trustInContainer": true - } - ], - "extraDestinations": [ - { "path": "/etc/nginx/certs", "format": "pem" }, - { "path": "/var/app", "format": "pem-bundle" } - ] -} -``` - -`pemKeyPath` and `rootPfxPath` are optional — the root PFX is generated with `openssl` when omitted. The script honors `_REMOTE_USER` / `_REMOTE_USER_HOME` (defaulting to `vscode` / `/home/vscode`) to decide whose home directory receives the certs. - -**Requirements:** `openssl` must be installed for hash computation and PFX conversion; the `--bundle-json` form additionally requires `jq`. - -You still need to produce the certificate material yourself on the host (for example with `dotnet dev-certs https --export-path`) and get it into the container — the script handles placement and trust, not transfer. - ## Limitations - **Auto-generated dev cert matches .NET's format only.** The `generateDotNetCert` flow produces a cert identical to `dotnet dev-certs https` (specific OID marker, subject, SAN entries). To sync differently-shaped certs (corporate CAs, custom wildcard certs, etc.), add them via the `devcontainerDevCerts.userCertificates` VS 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 automatically; see "[Non-VS Code editors](#non-vs-code-editors)" for the manual fallback. +- **VS Code only.** The companion extension pattern relies on VS Code's cross-host command routing, so VS Code is currently the only supported editor. Other editors (JetBrains, Vim, etc.) can't use this flow today. - **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. - **Linux browser trust is a separate step.** Firefox and Chromium on Linux don't read the OS/OpenSSL trust stores; see "[Linux hosts: browser trust](#linux-hosts-browser-trust)". @@ -545,7 +500,6 @@ src/ src/devcontainer-dev-certs/ devcontainer-feature.json Feature metadata, options, extension references install.sh Container build-time setup (creates directories) - scripts/setup-cert.sh Manual fallback installer for non-VS Code editors test/ sample-project/ Test project template (hydrated into .out/ for testing)