Audit logs for challenges

This commit is contained in:
Fang-Pen Lin
2025-12-11 14:38:03 -08:00
parent 4ed0f1d0a7
commit 36ea60a44c
2 changed files with 84 additions and 25 deletions
@@ -122,6 +122,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
const result = await (tx || db)(TableName.PkiAcmeChallenge) const result = await (tx || db)(TableName.PkiAcmeChallenge)
.join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`)
.join(TableName.PkiAcmeAccount, `${TableName.PkiAcmeAuth}.accountId`, `${TableName.PkiAcmeAccount}.id`) .join(TableName.PkiAcmeAccount, `${TableName.PkiAcmeAuth}.accountId`, `${TableName.PkiAcmeAccount}.id`)
.join(
TableName.PkiCertificateProfile,
`${TableName.PkiAcmeAccount}.profileId`,
`${TableName.PkiCertificateProfile}.id`
)
.select( .select(
selectAllTableCols(TableName.PkiAcmeChallenge), selectAllTableCols(TableName.PkiAcmeChallenge),
db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"), db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"),
@@ -131,7 +136,9 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("authIdentifierValue"), db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("authIdentifierValue"),
db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt"), db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt"),
db.ref("id").withSchema(TableName.PkiAcmeAccount).as("accountId"), db.ref("id").withSchema(TableName.PkiAcmeAccount).as("accountId"),
db.ref("publicKeyThumbprint").withSchema(TableName.PkiAcmeAccount).as("accountPublicKeyThumbprint") db.ref("publicKeyThumbprint").withSchema(TableName.PkiAcmeAccount).as("accountPublicKeyThumbprint"),
db.ref("profileId").withSchema(TableName.PkiAcmeAccount).as("profileId"),
db.ref("projectId").withSchema(TableName.PkiCertificateProfile).as("projectId")
) )
// For all challenges, acquire update lock on the auth to avoid race conditions // For all challenges, acquire update lock on the auth to avoid race conditions
.forUpdate(TableName.PkiAcmeAuth) .forUpdate(TableName.PkiAcmeAuth)
@@ -149,6 +156,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
authExpiresAt, authExpiresAt,
accountId, accountId,
accountPublicKeyThumbprint, accountPublicKeyThumbprint,
profileId,
projectId,
...challenge ...challenge
} = result; } = result;
return { return {
@@ -161,7 +170,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
expiresAt: authExpiresAt, expiresAt: authExpiresAt,
account: { account: {
id: accountId, id: accountId,
publicKeyThumbprint: accountPublicKeyThumbprint publicKeyThumbprint: accountPublicKeyThumbprint,
project: {
id: projectId
},
profileId
} }
} }
}; };
@@ -5,7 +5,9 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange"; import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
import { import {
AcmeConnectionError, AcmeConnectionError,
@@ -25,10 +27,12 @@ type TPkiAcmeChallengeServiceFactoryDep = {
| "markAsInvalidCascadeById" | "markAsInvalidCascadeById"
| "updateById" | "updateById"
>; >;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
}; };
export const pkiAcmeChallengeServiceFactory = ({ export const pkiAcmeChallengeServiceFactory = ({
acmeChallengeDAL acmeChallengeDAL,
auditLogService
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => { }: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig(); const appCfg = getConfig();
const markChallengeAsReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => { const markChallengeAsReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
@@ -113,7 +117,25 @@ export const pkiAcmeChallengeServiceFactory = ({
} }
logger.info({ challengeId }, "ACME challenge response is correct, marking challenge as valid"); logger.info({ challengeId }, "ACME challenge response is correct, marking challenge as valid");
await acmeChallengeDAL.markAsValidCascadeById(challengeId); await acmeChallengeDAL.markAsValidCascadeById(challengeId);
await auditLogService.createAuditLog({
projectId: challenge.auth.account.project.id,
actor: {
type: ActorType.ACME_ACCOUNT,
metadata: {
profileId: challenge.auth.account.profileId,
accountId: challenge.auth.account.id
}
},
event: {
type: EventType.PASS_ACME_CHALLENGE,
metadata: {
challengeId,
type: challenge.type as AcmeChallengeType
}
}
});
} catch (exp) { } catch (exp) {
let finalAttempt = false;
if (retryCount >= 2) { if (retryCount >= 2) {
logger.error( logger.error(
exp, exp,
@@ -121,35 +143,59 @@ export const pkiAcmeChallengeServiceFactory = ({
); );
// This is the last attempt to validate the challenge response, if it fails, we mark the challenge as invalid // This is the last attempt to validate the challenge response, if it fails, we mark the challenge as invalid
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId); await acmeChallengeDAL.markAsInvalidCascadeById(challengeId);
finalAttempt = true;
} }
// Properly type and inspect the error try {
if (axios.isAxiosError(exp)) { // Properly type and inspect the error
const axiosError = exp as AxiosError; if (axios.isAxiosError(exp)) {
const errorCode = axiosError.code; const axiosError = exp as AxiosError;
const errorMessage = axiosError.message; const errorCode = axiosError.code;
const errorMessage = axiosError.message;
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) { if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
throw new AcmeConnectionError({ message: "Connection refused" }); throw new AcmeConnectionError({ message: "Connection refused" });
}
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) {
throw new AcmeConnectionError({ message: "Connection reset by peer" });
}
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
logger.error(exp, "Connection timed out while validating ACME challenge response");
throw new AcmeConnectionError({ message: "Connection timed out" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} }
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) { if (exp instanceof Error) {
throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); logger.error(exp, "Error validating ACME challenge response");
} throw exp;
if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) {
throw new AcmeConnectionError({ message: "Connection reset by peer" });
}
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
logger.error(exp, "Connection timed out while validating ACME challenge response");
throw new AcmeConnectionError({ message: "Connection timed out" });
} }
logger.error(exp, "Unknown error validating ACME challenge response"); logger.error(exp, "Unknown error validating ACME challenge response");
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" }); throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} catch (outterExp) {
await auditLogService.createAuditLog({
projectId: challenge.auth.account.project.id,
actor: {
type: ActorType.ACME_ACCOUNT,
metadata: {
profileId: challenge.auth.account.profileId,
accountId: challenge.auth.account.id
}
},
event: {
type: finalAttempt ? EventType.FAIL_ACME_CHALLENGE : EventType.ATTEMPT_ACME_CHALLENGE,
metadata: {
challengeId,
type: challenge.type as AcmeChallengeType,
retryCount,
errorMessage: exp instanceof Error ? exp.message : "Unknown error"
}
}
});
throw outterExp;
} }
if (exp instanceof Error) {
logger.error(exp, "Error validating ACME challenge response");
throw exp;
}
logger.error(exp, "Unknown error validating ACME challenge response");
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
} }
}; };