Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2a6cab1
Add WSLC support
craigloewen-msft May 20, 2026
3864bda
Updates from PRs
craigloewen-msft May 21, 2026
f4a6363
Fixes
craigloewen-msft Jun 16, 2026
328335e
Downgrade runner
chrmarti Jun 23, 2026
0ffb221
nits
chrmarti Jun 24, 2026
6166357
0.88.0
chrmarti Jun 24, 2026
f63be01
Merge WSLC support (#1249)
chrmarti Jun 24, 2026
6e878c1
Bump shell-quote from 1.8.3 to 1.8.4 (#1250)
dependabot[bot] Jun 24, 2026
496c284
Bump esbuild from 0.27.3 to 0.28.1 (#1251)
dependabot[bot] Jun 24, 2026
f683c29
Bump tar from 7.5.11 to 7.5.16 (#1253)
dependabot[bot] Jun 25, 2026
58be970
Save uncommitted changes (#1270)
joshspicer Jul 23, 2026
657e33a
Fix flaky tests.
v-Kaniska244 Aug 11, 2026
d53bff6
Merge pull request #1277 from Kaniska244/fix-flaky-tests
sam-byng Aug 11, 2026
05249c9
Harden OCI registry authentication
chrmarti Aug 11, 2026
7b8e496
Pass OCI auth hosts as CLI state
chrmarti Aug 11, 2026
87adb63
Trust refresh tokens for mapped auth hosts
chrmarti Aug 11, 2026
146b161
Simplify bearer auth test setup
chrmarti Aug 13, 2026
c9783d0
Consolidate OCI auth realm policy
chrmarti Aug 13, 2026
5a7dc87
Clarify registry credential gate
chrmarti Aug 13, 2026
7c650d5
Make OCI auth hardening opt-in
chrmarti Aug 14, 2026
3ba63a5
Report OCI auth hardening impact
chrmarti Aug 14, 2026
422a92e
Prepare 0.89.0
chrmarti Aug 14, 2026
987bbc7
Refine OCI auth impact diagnostics
chrmarti Aug 14, 2026
98e182e
Fix rootless Podman tests on hosted runners
chrmarti Aug 15, 2026
0c9f4ca
Handle missing Podman user storage config
chrmarti Aug 15, 2026
3d2bed9
Revert "Handle missing Podman user storage config"
chrmarti Aug 17, 2026
5f49670
Revert "Fix rootless Podman tests on hosted runners"
chrmarti Aug 17, 2026
98c5265
Use Podman 5.5.2 for hosted integration tests (#1280)
chrmarti Aug 20, 2026
33073db
fix: set default base image in Dockerfiles and improve syntax handlin…
v-Kaniska244 Aug 20, 2026
4d46324
Merge origin/main into copilot/secure-oci-auth
chrmarti Aug 25, 2026
2a6de23
Honor HTTPS for localhost OCI auth realms
chrmarti Aug 25, 2026
1e47c10
Preserve localhost OCI registry transport
chrmarti Aug 25, 2026
aee6df1
Add schemes to OCI test references
chrmarti Aug 25, 2026
011f72f
Bump tar from 7.5.16 to 7.5.21 (#1292)
dependabot[bot] Aug 28, 2026
9b5e010
Bump brace-expansion to patched versions (#1294)
chrmarti Aug 28, 2026
53a7b99
Bump ip-address from 10.2.0 to 10.5.0 (#1295)
dependabot[bot] Aug 28, 2026
5d6f831
Bump js-yaml from 4.1.1 to 4.3.1 (#1296)
dependabot[bot] Aug 28, 2026
6083827
Bump shell-quote from 1.8.4 to 1.9.0 (#1297)
dependabot[bot] Aug 28, 2026
6360fbe
Bind OCI credentials to registry origins
chrmarti Aug 28, 2026
5c5e0c4
Honor OCI auth options in feature tests
chrmarti Aug 28, 2026
5dc7533
Merge pull request #1278 from devcontainers/copilot/secure-oci-auth
chrmarti Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"integrity": "sha256:ce078b7bf7d9ef3bcb9813b32103795d8d72172446890b64772cbe1dec6baafd"
}
}
}
}
16 changes: 15 additions & 1 deletion .github/workflows/dev-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ jobs:
"src/test/cli.podman.test.ts",
"src/test/cli.test.ts",
"src/test/cli.up.test.ts",
"src/test/httpOCIRegistry.test.ts",
"src/test/imageMetadata.test.ts",
"src/test/container-features/containerFeaturesOCIPush.test.ts",
# Run all except the above:
"--exclude src/test/container-features/containerFeaturesOrder.test.ts --exclude src/test/container-features/registryCompatibilityOCI.test.ts --exclude src/test/container-features/containerFeaturesOCIPush.test.ts --exclude src/test/container-features/e2e.test.ts --exclude src/test/container-features/featuresCLICommands.test.ts --exclude src/test/cli.build.test.ts --exclude src/test/cli.exec.buildKit.1.test.ts --exclude src/test/cli.exec.buildKit.2.test.ts --exclude src/test/cli.exec.nonBuildKit.1.test.ts --exclude src/test/cli.exec.nonBuildKit.2.test.ts --exclude src/test/cli.podman.test.ts --exclude src/test/cli.test.ts --exclude src/test/cli.up.test.ts --exclude src/test/imageMetadata.test.ts 'src/test/**/*.test.ts'",
"--exclude src/test/container-features/containerFeaturesOrder.test.ts --exclude src/test/container-features/registryCompatibilityOCI.test.ts --exclude src/test/container-features/containerFeaturesOCIPush.test.ts --exclude src/test/container-features/e2e.test.ts --exclude src/test/container-features/featuresCLICommands.test.ts --exclude src/test/cli.build.test.ts --exclude src/test/cli.exec.buildKit.1.test.ts --exclude src/test/cli.exec.buildKit.2.test.ts --exclude src/test/cli.exec.nonBuildKit.1.test.ts --exclude src/test/cli.exec.nonBuildKit.2.test.ts --exclude src/test/cli.podman.test.ts --exclude src/test/cli.test.ts --exclude src/test/cli.up.test.ts --exclude src/test/httpOCIRegistry.test.ts --exclude src/test/imageMetadata.test.ts 'src/test/**/*.test.ts'",
]
steps:
- name: Checkout
Expand All @@ -89,6 +90,19 @@ jobs:
fi
cat "$DAEMON_JSON"
sudo systemctl restart docker
- name: Install Podman 5.5.2
if: matrix.mocha-args == 'src/test/cli.podman.test.ts'
run: |
docker build \
--build-arg PODMAN_VERSION=v5.5.2 \
--target podman \
--tag podman-5.5.2-builder \
'https://github.com/mgoltzsche/podman-static.git#9ccf992536ecf0a4a3b82ac27fc708f00ab141ab'
container_id=$(docker create podman-5.5.2-builder)
docker cp "${container_id}:/usr/local/bin/podman" podman
docker rm "${container_id}"
sudo install -m 0755 podman /usr/local/bin/podman
podman --version
- name: Tools Info
run: |
docker info
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/publish-dev-containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: read
id-token: write

jobs:
main:
Expand All @@ -18,7 +19,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '20.x'
node-version: '24.x'
registry-url: 'https://registry.npmjs.org'
scope: '@devcontainers'
- name: Verify Versions
Expand Down Expand Up @@ -46,5 +47,3 @@ jobs:
path: .
- name: Publish TGZ
run: npm publish ${TGZ} --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permissions:
jobs:
tests-matrix:
name: Tests Matrix (Windows)
runs-on: windows-latest
runs-on: windows-2022
timeout-minutes: 15
strategy:
fail-fast: false
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ src/test/container-features/configs/temp_lifecycle-hooks-alternative-order
test-secrets-temp.json
src/test/container-*/**/src/**/README.md
!src/test/container-features/assets/*.tgz
src/test/fixtures/localhost-cert.pem
src/test/fixtures/localhost-key.pem
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Notable changes.

## August 2026

### [0.89.0]
- Add opt-in OCI authentication hardening with `--oci-auth-hardening`, trusted cross-origin authentication host mappings, and diagnostics for measuring compatibility impact. (https://github.com/devcontainers/cli/pull/1278)

## June 2026

### [0.88.0]
- Add WSLc support (https://github.com/devcontainers/cli/pull/1249)

## May 2026

### [0.87.0]
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@devcontainers/cli",
"description": "Dev Containers CLI",
"version": "0.87.0",
"version": "0.89.0",
"bin": {
"devcontainer": "devcontainer.js"
},
Expand Down Expand Up @@ -71,7 +71,7 @@
"@typescript-eslint/parser": "^8.56.1",
"chai": "^4.5.0",
"copyfiles": "^2.4.1",
"esbuild": "^0.27.3",
"esbuild": "^0.28.1",
"eslint": "^10.0.2",
"event-stream": "^4.0.1",
"minimatch": "^10.2.4",
Expand Down
2 changes: 1 addition & 1 deletion scripts/updateUID.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
ARG BASE_IMAGE
ARG BASE_IMAGE=placeholder
FROM $BASE_IMAGE

USER root
Expand Down
4 changes: 4 additions & 0 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { launch, ShellServer } from './shellServer';
import { ExecFunction, CLIHost, PtyExecFunction, isFile, Exec, PtyExec, getEntPasswdShellCommand } from './commonUtils';
import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event';
import { PackageConfiguration } from '../spec-utils/product';
import { OCIAuthDiagnostics } from './ociAuth';
import { URI } from 'vscode-uri';
import { containerSubstitute } from './variableSubstitution';
import { delay } from './async';
Expand Down Expand Up @@ -69,6 +70,9 @@ export interface ResolverParameters {
omitConfigRemotEnvFromMetadata?: boolean;
secretsP?: Promise<Record<string, string>>;
omitSyntaxDirective?: boolean;
allowedCrossOriginAuthHosts?: string[];
ociAuthHardening?: boolean;
ociAuthDiagnostics: OCIAuthDiagnostics;
}

export interface LifecycleHook {
Expand Down
18 changes: 18 additions & 0 deletions src/spec-common/ociAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export interface OCIAuthDiagnostics {
authLookupWouldBeBlocked: boolean;
registryRedirectWouldPreventCredentialForwarding: boolean;
authServerRedirect: boolean;
}

export function createOCIAuthDiagnostics(): OCIAuthDiagnostics {
return {
authLookupWouldBeBlocked: false,
registryRedirectWouldPreventCredentialForwarding: false,
authServerRedirect: false,
};
}
16 changes: 14 additions & 2 deletions src/spec-configuration/containerCollectionsOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Log, LogLevel } from '../spec-utils/log';
import { isLocalFile, mkdirpLocal, readLocalFile, writeLocalFile } from '../spec-utils/pfs';
import { requestEnsureAuthenticated } from './httpOCIRegistry';
import { GoARCH, GoOS, PlatformInfo } from '../spec-common/commonUtils';
import { OCIAuthDiagnostics } from '../spec-common/ociAuth';

export const DEVCONTAINER_MANIFEST_MEDIATYPE = 'application/vnd.devcontainers';
export const DEVCONTAINER_TAR_LAYER_MEDIATYPE = 'application/vnd.devcontainers.layer.v1+tar';
Expand All @@ -18,13 +19,17 @@ export interface CommonParams {
env: NodeJS.ProcessEnv;
output: Log;
cachedAuthHeader?: Record<string, string>; // <registry, authHeader>
allowedCrossOriginAuthHosts?: string[];
ociAuthHardening?: boolean;
ociAuthDiagnostics: OCIAuthDiagnostics;
}

// Represents the unique OCI identifier for a Feature or Template.
// eg: ghcr.io/devcontainers/features/go:1.0.0
// eg: ghcr.io/devcontainers/features/go@sha256:fe73f123927bd9ed1abda190d3009c4d51d0e17499154423c5913cf344af15a3
// Constructed by 'getRef()'
export interface OCIRef {
scheme: 'http' | 'https';
registry: string; // 'ghcr.io'
owner: string; // 'devcontainers'
namespace: string; // 'devcontainers/features'
Expand All @@ -41,6 +46,7 @@ export interface OCIRef {
// eg: ghcr.io/devcontainers/features:latest
// Constructed by 'getCollectionRef()'
export interface OCICollectionRef {
scheme: 'http' | 'https';
registry: string; // 'ghcr.io'
path: string; // 'devcontainers/features'
resource: string; // 'ghcr.io/devcontainers/features'
Expand Down Expand Up @@ -116,6 +122,10 @@ const regexForPath = /^[a-z0-9]+([._-][a-z0-9]+)*(\/[a-z0-9]+([._-][a-z0-9]+)*)*
// MUST be at most 128 characters in length and MUST match the following regular expression:
const regexForVersionOrDigest = /^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$/;

function getRegistryScheme(registry: string): OCIRef['scheme'] {
return new URL(`https://${registry}`).hostname.toLowerCase() === 'localhost' ? 'http' : 'https';
}

// https://go.dev/doc/install/source#environment
// Expected by OCI Spec as seen here: https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions
export function mapNodeArchitectureToGOARCH(arch: NodeJS.Architecture): GoARCH {
Expand Down Expand Up @@ -236,6 +246,7 @@ export function getRef(output: Log, input: string): OCIRef | undefined {
output.write(`> digest?: ${digest}`, LogLevel.Trace);

return {
scheme: getRegistryScheme(registry),
id,
owner,
namespace,
Expand Down Expand Up @@ -266,6 +277,7 @@ export function getCollectionRef(output: Log, registry: string, namespace: strin
}

return {
scheme: getRegistryScheme(registry),
registry,
path,
resource,
Expand All @@ -291,7 +303,7 @@ export async function fetchOCIManifestIfExists(params: CommonParams, ref: OCIRef
if (manifestDigest) {
reference = manifestDigest;
}
const manifestUrl = `https://${ref.registry}/v2/${ref.path}/manifests/${reference}`;
const manifestUrl = `${ref.scheme}://${ref.registry}/v2/${ref.path}/manifests/${reference}`;
output.write(`manifest url: ${manifestUrl}`, LogLevel.Trace);
const expectedDigest = manifestDigest || ('digest' in ref ? ref.digest : undefined);
const manifestContainer = await getManifest(params, manifestUrl, ref, undefined, expectedDigest);
Expand Down Expand Up @@ -467,7 +479,7 @@ export async function getVersionsStrictSorted(params: CommonParams, ref: OCIRef)
export async function getPublishedTags(params: CommonParams, ref: OCIRef): Promise<string[] | undefined> {
const { output } = params;
try {
const url = `https://${ref.registry}/v2/${ref.namespace}/${ref.id}/tags/list`;
const url = `${ref.scheme}://${ref.registry}/v2/${ref.namespace}/${ref.id}/tags/list`;

const headers = {
'Accept': 'application/json',
Expand Down
8 changes: 4 additions & 4 deletions src/spec-configuration/containerCollectionsOCIPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ async function putManifestWithTags(params: CommonParams, manifest: ManifestConta
const { manifestBuffer, contentDigest } = manifest;

for await (const tag of tags) {
const url = `https://${ociRef.registry}/v2/${ociRef.path}/manifests/${tag}`;
const url = `${ociRef.scheme}://${ociRef.registry}/v2/${ociRef.path}/manifests/${tag}`;
output.write(`PUT -> '${url}'`, LogLevel.Trace);

const httpOptions = {
Expand Down Expand Up @@ -232,7 +232,7 @@ async function putBlob(params: CommonParams, blobPutLocationUriPath: string, oci
if (blobPutLocationUriPath.startsWith('https://') || blobPutLocationUriPath.startsWith('http://')) {
url = blobPutLocationUriPath;
} else {
url = `https://${ociRef.registry}${blobPutLocationUriPath}`;
url = `${ociRef.scheme}://${ociRef.registry}${blobPutLocationUriPath}`;
}

// The <location> MAY contain critical query parameters.
Expand Down Expand Up @@ -332,7 +332,7 @@ export async function calculateDataLayer(output: Log, data: Buffer, basename: st
export async function checkIfBlobExists(params: CommonParams, ociRef: OCIRef | OCICollectionRef, digest: string): Promise<boolean> {
const { output } = params;

const url = `https://${ociRef.registry}/v2/${ociRef.path}/blobs/${digest}`;
const url = `${ociRef.scheme}://${ociRef.registry}/v2/${ociRef.path}/blobs/${digest}`;
const res = await requestEnsureAuthenticated(params, { type: 'HEAD', url, headers: {} }, ociRef);
if (!res) {
output.write('Request failed', LogLevel.Error);
Expand All @@ -349,7 +349,7 @@ export async function checkIfBlobExists(params: CommonParams, ociRef: OCIRef | O
async function postUploadSessionId(params: CommonParams, ociRef: OCIRef | OCICollectionRef): Promise<string | undefined> {
const { output } = params;

const url = `https://${ociRef.registry}/v2/${ociRef.path}/blobs/uploads/`;
const url = `${ociRef.scheme}://${ociRef.registry}/v2/${ociRef.path}/blobs/uploads/`;
output.write(`Generating Upload URL -> ${url}`, LogLevel.Trace);
const res = await requestEnsureAuthenticated(params, { type: 'POST', url, headers: {} }, ociRef);

Expand Down
10 changes: 8 additions & 2 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { request } from '../spec-utils/httpRequest';
import { fetchOCIFeature, tryGetOCIFeatureSet, fetchOCIFeatureManifestIfExistsFromUserIdentifier } from './containerFeaturesOCI';
import { uriToFsPath } from './configurationCommonUtils';
import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getRef, getVersionsStrictSorted } from './containerCollectionsOCI';
import { OCIAuthDiagnostics } from '../spec-common/ociAuth';
import { Lockfile, generateLockfile, readLockfile, writeLockfile } from './lockfile';
import { computeDependsOnInstallationOrder } from './containerFeaturesOrder';
import { logFeatureAdvisories } from './featureAdvisories';
Expand Down Expand Up @@ -195,6 +196,9 @@ export interface ContainerFeatureInternalParams {
platform: NodeJS.Platform;
noLockfile?: boolean;
frozenLockfile?: boolean;
allowedCrossOriginAuthHosts?: string[];
ociAuthHardening?: boolean;
ociAuthDiagnostics: OCIAuthDiagnostics;
}

// TODO: Move to node layer.
Expand All @@ -203,6 +207,8 @@ export function getContainerFeaturesBaseDockerFile(contentSourceRootPath: string

#{nonBuildKitFeatureContentFallback}

ARG _DEV_CONTAINERS_BASE_IMAGE=scratch

FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_feature_content_normalize
USER root
COPY --from=dev_containers_feature_content_source ${path.posix.join(contentSourceRootPath, 'devcontainer-features.builtin.env')} /tmp/build-features/
Expand Down Expand Up @@ -389,7 +395,7 @@ const cleanupIterationFetchAndMerge = async (tempTarballPath: string, output: Lo
}
};

function getRequestHeaders(params: CommonParams, sourceInformation: SourceInformation) {
function getRequestHeaders(params: { env: NodeJS.ProcessEnv; output: Log }, sourceInformation: SourceInformation) {
const { env, output } = params;
let headers: { 'user-agent': string; 'Authorization'?: string; 'Accept'?: string } = {
'user-agent': 'devcontainer'
Expand Down Expand Up @@ -955,7 +961,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
// throw new Error(`Unsupported feature source type: ${type}`);
}

async function fetchFeatures(params: { extensionPath: string; cwd: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, dstFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) {
async function fetchFeatures(params: ContainerFeatureInternalParams, featuresConfig: FeaturesConfig, dstFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) {
const featureSets = featuresConfig.featureSets;
for (let idx = 0; idx < featureSets.length; idx++) { // Index represents the previously computed installation order.
const featureSet = featureSets[idx];
Expand Down
2 changes: 1 addition & 1 deletion src/spec-configuration/containerFeaturesOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function fetchOCIFeature(params: CommonParams, featureSet: FeatureS
const { featureRef } = featureSet.sourceInformation;

const layerDigest = featureSet.sourceInformation.manifest?.layers[0].digest;
const blobUrl = `https://${featureSet.sourceInformation.featureRef.registry}/v2/${featureSet.sourceInformation.featureRef.path}/blobs/${layerDigest}`;
const blobUrl = `${featureRef.scheme}://${featureRef.registry}/v2/${featureRef.path}/blobs/${layerDigest}`;
output.write(`blob url: ${blobUrl}`, LogLevel.Trace);

const blobResult = await getBlob(params, blobUrl, ociCacheDir, featCachePath, featureRef, layerDigest, undefined, metadataFile);
Expand Down
2 changes: 1 addition & 1 deletion src/spec-configuration/containerTemplatesOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function fetchTemplate(params: CommonParams, selectedTemplate: Sele
return;
}

const blobUrl = `https://${templateRef.registry}/v2/${templateRef.path}/blobs/${blobDigest}`;
const blobUrl = `${templateRef.scheme}://${templateRef.registry}/v2/${templateRef.path}/blobs/${blobDigest}`;
output.write(`blob url: ${blobUrl}`, LogLevel.Trace);

const tmpDir = userProvidedTmpDir || path.join(os.tmpdir(), 'vsch-template-temp', `${Date.now()}`);
Expand Down
Loading