diff --git a/app/registries/BaseRegistry.test.ts b/app/registries/BaseRegistry.test.ts index d506138f0..0ca3a07d1 100644 --- a/app/registries/BaseRegistry.test.ts +++ b/app/registries/BaseRegistry.test.ts @@ -134,6 +134,25 @@ test('authenticateBearer should attach CA from cafile when configured', async () } }); +test('authenticateBearer should attach mtls cert and key when configured', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'drydock-baseregistry-')); + const clientCertPath = path.join(tempDir, 'clientcert.pem'); + const clientKeyPath = path.join(tempDir, 'clientkey.pem'); + try { + fs.writeFileSync(clientCertPath, 'test-client-cert-content'); + fs.writeFileSync(clientKeyPath, 'test-client-key-content'); + baseRegistry.configuration = { clientcertfile: clientCertPath, clientkeyfile: clientKeyPath }; + const result = await baseRegistry.authenticateBearer({ headers: {} }, 'token-value'); + expect(result.headers.Authorization).toBe('Bearer token-value'); + expect(result.httpsAgent).toBeDefined(); + expect(result.httpsAgent.options.rejectUnauthorized).toBe(true); + expect(result.httpsAgent.options.cert.toString('utf-8')).toBe('test-client-cert-content'); + expect(result.httpsAgent.options.key.toString('utf-8')).toBe('test-client-key-content'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + test('getAuthCredentials should return auth when set', () => { baseRegistry.configuration = { auth: 'base64-auth' }; expect(baseRegistry.getAuthCredentials()).toBe('base64-auth'); diff --git a/app/registries/BaseRegistry.ts b/app/registries/BaseRegistry.ts index fd00601b6..fd4782dea 100644 --- a/app/registries/BaseRegistry.ts +++ b/app/registries/BaseRegistry.ts @@ -76,7 +76,8 @@ class BaseRegistry extends Registry { private getHttpsAgent() { const shouldDisableTlsVerification = this.configuration?.insecure === true; const hasCaFile = Boolean(this.configuration?.cafile); - if (!shouldDisableTlsVerification && !hasCaFile) { + const hasMutalTls = Boolean(this.configuration?.clientcertfile); + if (!shouldDisableTlsVerification && !hasCaFile && !hasMutalTls) { return undefined; } @@ -91,12 +92,25 @@ class BaseRegistry extends Registry { }); ca = fs.readFileSync(caPath); } + let clientCert, clientKey; + if (hasMutalTls) { + const clientCertPath = resolveConfiguredPath(this.configuration.clientcertfile, { + label: `registry ${this.getId()} client cert file path`, + }); + clientCert = fs.readFileSync(clientCertPath); + const clientKeyPath = resolveConfiguredPath(this.configuration.clientkeyfile, { + label: `registry ${this.getId()} client key file path`, + }); + clientKey = fs.readFileSync(clientKeyPath); + } // Intentional opt-in for self-hosted registries with private/self-signed cert chains. // lgtm[js/disabling-certificate-validation] this.httpsAgent = new https.Agent({ ca, rejectUnauthorized: !shouldDisableTlsVerification, + cert: clientCert, + key: clientKey, }); return this.httpsAgent; } diff --git a/app/registries/providers/shared/SelfHostedBasic.ts b/app/registries/providers/shared/SelfHostedBasic.ts index 71e4e03b0..b747a2080 100644 --- a/app/registries/providers/shared/SelfHostedBasic.ts +++ b/app/registries/providers/shared/SelfHostedBasic.ts @@ -16,12 +16,15 @@ class SelfHostedBasic extends BaseRegistry { login: this.joi.string(), password: this.joi.string(), auth: authSchema, - cafile: this.joi.string(), insecure: this.joi.boolean(), + cafile: this.joi.string(), + clientcertfile: this.joi.string(), + clientkeyfile: this.joi.string(), }) .and('login', 'password') .without('login', 'auth') - .without('password', 'auth'); + .without('password', 'auth') + .and('clientcertfile', 'clientkeyfile'); } maskConfiguration() { diff --git a/app/store/update-operation.test.ts b/app/store/update-operation.test.ts index 997f21a8a..8d327473a 100644 --- a/app/store/update-operation.test.ts +++ b/app/store/update-operation.test.ts @@ -282,7 +282,7 @@ describe('Update Operation Store', () => { }); for (let i = 0; i < 97; i += 1) { - vi.setSystemTime(new Date(2026, 1, 1, 0, 1, i)); + vi.setSystemTime(new Date(2026, 2, 1, 0, 1, i)); fresh.updateOperation(third.id, { lastError: `error-${i}`, }); diff --git a/content/docs/current/configuration/registries/artifactory/index.mdx b/content/docs/current/configuration/registries/artifactory/index.mdx index b938e83b5..cd408dfce 100644 --- a/content/docs/current/configuration/registries/artifactory/index.mdx +++ b/content/docs/current/configuration/registries/artifactory/index.mdx @@ -19,6 +19,8 @@ The `artifactory` registry lets you configure a [JFrog Artifactory](https://jfro | `DD_REGISTRY_ARTIFACTORY_{REGISTRY_NAME}_AUTH` | ⚪ | Base64 encoded `login:password` string | DD_REGISTRY_ARTIFACTORY_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_ARTIFACTORY_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_ARTIFACTORY_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_ARTIFACTORY_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_ARTIFACTORY_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples diff --git a/content/docs/current/configuration/registries/custom/index.mdx b/content/docs/current/configuration/registries/custom/index.mdx index 9782b3374..5af579803 100644 --- a/content/docs/current/configuration/registries/custom/index.mdx +++ b/content/docs/current/configuration/registries/custom/index.mdx @@ -19,6 +19,8 @@ The `custom` registry lets you configure a self-hosted [Docker Registry](https:/ | `DD_REGISTRY_CUSTOM_{REGISTRY_NAME}_AUTH` | ⚪ | Base64-encoded `login:password` string | DD_REGISTRY_CUSTOM_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_CUSTOM_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_CUSTOM_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_CUSTOM_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_CUSTOM_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples diff --git a/content/docs/current/configuration/registries/forgejo/index.mdx b/content/docs/current/configuration/registries/forgejo/index.mdx index 5ea624621..4642004c4 100644 --- a/content/docs/current/configuration/registries/forgejo/index.mdx +++ b/content/docs/current/configuration/registries/forgejo/index.mdx @@ -19,6 +19,8 @@ The `forgejo` registry lets you configure a self-hosted [Forgejo](https://forgej | `DD_REGISTRY_FORGEJO_{REGISTRY_NAME}_AUTH` | ⚪ | Base64-encoded `login:password` string | DD_REGISTRY_FORGEJO_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_FORGEJO_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_FORGEJO_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_FORGEJO_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_FORGEJO_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples diff --git a/content/docs/current/configuration/registries/gitea/index.mdx b/content/docs/current/configuration/registries/gitea/index.mdx index 6c8500b8f..3ad155eb9 100644 --- a/content/docs/current/configuration/registries/gitea/index.mdx +++ b/content/docs/current/configuration/registries/gitea/index.mdx @@ -19,6 +19,8 @@ The `gitea` registry lets you configure a self-hosted [Gitea](https://gitea.com) | `DD_REGISTRY_GITEA_{REGISTRY_NAME}_AUTH` | ⚪ | Base64-encoded `login:password` string | DD_REGISTRY_GITEA_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_GITEA_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_GITEA_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_GITEA_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_GITEA_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples diff --git a/content/docs/current/configuration/registries/harbor/index.mdx b/content/docs/current/configuration/registries/harbor/index.mdx index d674c49e9..8cfda40c0 100644 --- a/content/docs/current/configuration/registries/harbor/index.mdx +++ b/content/docs/current/configuration/registries/harbor/index.mdx @@ -19,6 +19,8 @@ The `harbor` registry lets you configure a self-hosted [Harbor](https://goharbor | `DD_REGISTRY_HARBOR_{REGISTRY_NAME}_AUTH` | ⚪ | Base64 encoded `login:password` string | DD_REGISTRY_HARBOR_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_HARBOR_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_HARBOR_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_HARBOR_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_HARBOR_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples diff --git a/content/docs/current/configuration/registries/nexus/index.mdx b/content/docs/current/configuration/registries/nexus/index.mdx index 8d30d4434..00f484a38 100644 --- a/content/docs/current/configuration/registries/nexus/index.mdx +++ b/content/docs/current/configuration/registries/nexus/index.mdx @@ -19,6 +19,8 @@ The `nexus` registry lets you configure a [Sonatype Nexus](https://www.sonatype. | `DD_REGISTRY_NEXUS_{REGISTRY_NAME}_AUTH` | ⚪ | Base64 encoded `login:password` string | DD_REGISTRY_NEXUS_\{REGISTRY_NAME\}_LOGIN/PASSWORD must not be defined | | | `DD_REGISTRY_NEXUS_{REGISTRY_NAME}_CAFILE` | ⚪ | Path to custom CA certificate file | | | | `DD_REGISTRY_NEXUS_{REGISTRY_NAME}_INSECURE` | ⚪ | Allow insecure (non-TLS) connections | `true`, `false` | `false` | +| `DD_REGISTRY_NEXUS_{REGISTRY_NAME}_CLIENTCERTFILE` | ⚪ | Path to mTls client certificate file | | | +| `DD_REGISTRY_NEXUS_{REGISTRY_NAME}_CLIENTKEYFILE` | ⚪ | Path to mTls client key file | | | ## Examples