mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 22:46:24 +00:00
Improvements on Azure ADCS PKI feature
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable no-case-declarations, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires, no-await-in-loop, no-continue */
|
||||
// @ts-expect-error: No types available for httpntlm
|
||||
import * as httpntlm from "httpntlm";
|
||||
import { NtlmClient } from "axios-ntlm";
|
||||
import https from "https";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@@ -13,23 +12,24 @@ import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
|
||||
import { TAzureADCSConnectionConfig } from "./azure-adcs-connection-types";
|
||||
|
||||
// Type definitions for httpntlm module
|
||||
interface HttpNtlmRequestOptions {
|
||||
// Type definitions for axios-ntlm
|
||||
interface AxiosNtlmConfig {
|
||||
ntlm: {
|
||||
domain: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
httpsAgent?: https.Agent;
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
domain: string;
|
||||
workstation?: string;
|
||||
method?: string;
|
||||
body?: string;
|
||||
data?: string;
|
||||
headers?: Record<string, string>;
|
||||
rejectUnauthorized?: boolean;
|
||||
}
|
||||
|
||||
interface HttpNtlmResponse {
|
||||
statusCode: number;
|
||||
body: string;
|
||||
headers: Record<string, string>;
|
||||
interface AxiosNtlmResponse {
|
||||
status: number;
|
||||
data: string;
|
||||
headers: unknown;
|
||||
}
|
||||
|
||||
// Types for credential parsing
|
||||
@@ -104,31 +104,65 @@ const normalizeAdcsUrl = (url: string): string => {
|
||||
};
|
||||
|
||||
// NTLM request wrapper
|
||||
const ntlmRequest = (options: HttpNtlmRequestOptions): Promise<HttpNtlmResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const method = options.method || "GET";
|
||||
const createHttpsAgent = (sslRejectUnauthorized: boolean, sslCertificate?: string): https.Agent => {
|
||||
const agentOptions: https.AgentOptions = {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
keepAlive: true // axios-ntlm needs keepAlive for NTLM handshake
|
||||
};
|
||||
|
||||
if (method.toLowerCase() === "get") {
|
||||
httpntlm.get(options, (err: Error | null, res: HttpNtlmResponse) => {
|
||||
if (err) reject(err);
|
||||
else resolve(res);
|
||||
});
|
||||
} else if (method.toLowerCase() === "post") {
|
||||
httpntlm.post(options, (err: Error | null, res: HttpNtlmResponse) => {
|
||||
if (err) reject(err);
|
||||
else resolve(res);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Unsupported HTTP method: ${method}`));
|
||||
}
|
||||
});
|
||||
if (sslCertificate && sslCertificate.trim()) {
|
||||
agentOptions.ca = [sslCertificate.trim()];
|
||||
}
|
||||
|
||||
return new https.Agent(agentOptions);
|
||||
};
|
||||
|
||||
const axiosNtlmRequest = async (config: AxiosNtlmConfig): Promise<AxiosNtlmResponse> => {
|
||||
const method = config.method || "GET";
|
||||
|
||||
const credentials = {
|
||||
username: config.ntlm.username,
|
||||
password: config.ntlm.password,
|
||||
domain: config.ntlm.domain || "",
|
||||
workstation: ""
|
||||
};
|
||||
|
||||
const axiosConfig = {
|
||||
httpsAgent: config.httpsAgent,
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const client = NtlmClient(credentials, axiosConfig);
|
||||
|
||||
const requestOptions: { url: string; method: string; data?: string; headers?: Record<string, string> } = {
|
||||
url: config.url,
|
||||
method
|
||||
};
|
||||
|
||||
if (config.data) {
|
||||
requestOptions.data = config.data;
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
requestOptions.headers = config.headers;
|
||||
}
|
||||
|
||||
const response = await client(requestOptions);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
headers: response.headers
|
||||
};
|
||||
};
|
||||
|
||||
// Test ADCS connectivity and authentication using NTLM
|
||||
const testAdcsConnection = async (
|
||||
credentials: ParsedCredentials,
|
||||
password: string,
|
||||
baseUrl: string
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
): Promise<boolean> => {
|
||||
// Test endpoints in order of preference
|
||||
const testEndpoints = [
|
||||
@@ -142,18 +176,24 @@ const testAdcsConnection = async (
|
||||
try {
|
||||
const testUrl = `${baseUrl}${endpoint}`;
|
||||
|
||||
const response = await ntlmRequest({
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
const response = await axiosNtlmRequest({
|
||||
url: testUrl,
|
||||
username: credentials.username,
|
||||
password,
|
||||
domain: credentials.domain,
|
||||
workstation: "",
|
||||
rejectUnauthorized: !getConfig().isDevelopmentMode // Only allow unauthorized SSL in development
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
ntlm: {
|
||||
domain: credentials.domain,
|
||||
username: credentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we got a successful response
|
||||
if (response.statusCode === 200) {
|
||||
const responseText = response.body;
|
||||
if (response.status === 200) {
|
||||
const responseText = response.data;
|
||||
|
||||
// Verify this is actually an ADCS server by checking content
|
||||
const adcsIndicators = [
|
||||
@@ -175,14 +215,13 @@ const testAdcsConnection = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle authentication failures
|
||||
if (response.statusCode === 401) {
|
||||
if (response.status === 401) {
|
||||
throw new BadRequestError({
|
||||
message: "Authentication failed. Please verify your username, password, and domain are correct."
|
||||
message: "Authentication failed. Please verify your credentials are correct."
|
||||
});
|
||||
}
|
||||
|
||||
if (response.statusCode === 403) {
|
||||
if (response.status === 403) {
|
||||
throw new BadRequestError({
|
||||
message: "Access denied. Your account may not have permission to access ADCS web enrollment."
|
||||
});
|
||||
@@ -204,11 +243,27 @@ const testAdcsConnection = async (
|
||||
message: "Connection refused by ADCS server. Please verify the server is running and accessible."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("ETIMEDOUT")) {
|
||||
if (error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) {
|
||||
throw new BadRequestError({
|
||||
message: "Connection timeout. Please verify the server is accessible and not blocked by firewall."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("certificate") || error.message.includes("SSL") || error.message.includes("TLS")) {
|
||||
throw new BadRequestError({
|
||||
message: `SSL/TLS certificate error: ${error.message}. This may indicate a certificate verification failure.`
|
||||
});
|
||||
}
|
||||
if (error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT")) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Self-signed certificate detected. Either provide the server's certificate or set 'sslRejectUnauthorized' to false."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to verify certificate signature. Please provide the correct CA certificate."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next endpoint for other errors
|
||||
@@ -223,37 +278,53 @@ const testAdcsConnection = async (
|
||||
};
|
||||
|
||||
// Create authenticated NTLM client for ADCS operations
|
||||
const createNtlmClient = (username: string, password: string, baseUrl: string) => {
|
||||
const createNtlmClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
const parsedCredentials = parseCredentials(username);
|
||||
const normalizedUrl = normalizeAdcsUrl(baseUrl);
|
||||
|
||||
return {
|
||||
get: (endpoint: string, additionalOptions: Partial<HttpNtlmRequestOptions> = {}) => {
|
||||
return ntlmRequest({
|
||||
get: async (endpoint: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
username: parsedCredentials.username,
|
||||
password,
|
||||
domain: parsedCredentials.domain,
|
||||
workstation: "",
|
||||
rejectUnauthorized: !getConfig().isDevelopmentMode, // Only allow unauthorized SSL in development,
|
||||
...additionalOptions
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
headers: additionalHeaders,
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
post: (endpoint: string, body: string, additionalOptions: Partial<HttpNtlmRequestOptions> = {}) => {
|
||||
return ntlmRequest({
|
||||
method: "POST",
|
||||
post: async (endpoint: string, body: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
username: parsedCredentials.username,
|
||||
password,
|
||||
domain: parsedCredentials.domain,
|
||||
workstation: "",
|
||||
rejectUnauthorized: !getConfig().isDevelopmentMode, // Only allow unauthorized SSL in development,
|
||||
body,
|
||||
method: "POST",
|
||||
httpsAgent,
|
||||
data: body,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
...additionalOptions.headers
|
||||
...additionalHeaders
|
||||
},
|
||||
...additionalOptions
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
baseUrl: normalizedUrl,
|
||||
@@ -282,12 +353,20 @@ export const getAzureADCSConnectionCredentials = async (
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as { username: string; password: string; adcsUrl: string };
|
||||
})) as {
|
||||
username: string;
|
||||
password: string;
|
||||
adcsUrl: string;
|
||||
sslRejectUnauthorized?: boolean;
|
||||
sslCertificate?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -309,13 +388,21 @@ export const validateAzureADCSConnectionCredentials = async (appConnection: TAzu
|
||||
await blockLocalAndPrivateIpAddresses(normalizedUrl);
|
||||
|
||||
// Test the connection using NTLM
|
||||
await testAdcsConnection(parsedCredentials, credentials.password, normalizedUrl);
|
||||
await testAdcsConnection(
|
||||
parsedCredentials,
|
||||
credentials.password,
|
||||
normalizedUrl,
|
||||
credentials.sslRejectUnauthorized ?? true,
|
||||
credentials.sslCertificate
|
||||
);
|
||||
|
||||
// If we get here, authentication was successful
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
@@ -334,9 +421,11 @@ export const validateAzureADCSConnectionCredentials = async (appConnection: TAzu
|
||||
} else if (
|
||||
error.message.includes("certificate") ||
|
||||
error.message.includes("SSL") ||
|
||||
error.message.includes("TLS")
|
||||
error.message.includes("TLS") ||
|
||||
error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT") ||
|
||||
error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")
|
||||
) {
|
||||
errorMessage = "SSL/TLS certificate error. The server certificate may be self-signed or invalid.";
|
||||
errorMessage = `SSL/TLS certificate error: ${error.message}. The server certificate may be self-signed or the CA certificate may be incorrect.`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +444,12 @@ export const getAzureADCSConnectionListItem = () => ({
|
||||
});
|
||||
|
||||
// Export helper functions for use in certificate ordering
|
||||
export const createAdcsHttpClient = (username: string, password: string, baseUrl: string) => {
|
||||
return createNtlmClient(username, password, baseUrl);
|
||||
export const createAdcsHttpClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
return createNtlmClient(username, password, baseUrl, sslRejectUnauthorized, sslCertificate);
|
||||
};
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
|
||||
|
||||
export const AzureADCSConnectionAccessTokenCredentialsSchema = z.object({
|
||||
export const AzureADCSUsernamePasswordCredentialsSchema = z.object({
|
||||
adcsUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "ADCS URL required")
|
||||
.max(255)
|
||||
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.adcsUrl),
|
||||
username: z
|
||||
.string()
|
||||
@@ -28,21 +29,31 @@ export const AzureADCSConnectionAccessTokenCredentialsSchema = z.object({
|
||||
.trim()
|
||||
.min(1, "Password required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.password)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseAzureADCSConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AzureADCS) });
|
||||
|
||||
export const AzureADCSConnectionSchema = BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSConnectionAccessTokenCredentialsSchema
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.pick({
|
||||
username: true
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.pick({
|
||||
username: true,
|
||||
adcsUrl: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
@@ -52,7 +63,7 @@ export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion
|
||||
method: z
|
||||
.literal(AzureADCSConnectionMethod.UsernamePassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureADCS).method),
|
||||
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.describe(
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
@@ -64,7 +75,7 @@ export const CreateAzureADCSConnectionSchema = ValidateAzureADCSConnectionCreden
|
||||
|
||||
export const UpdateAzureADCSConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user