Audit logs for challenges

This commit is contained in:
Fang-Pen Lin
2025-12-09 18:34:48 -08:00
parent 4ed0f1d0a7
commit 36ea60a44c
2 changed files with 84 additions and 25 deletions

View File

@@ -122,6 +122,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
const result = await (tx || db)(TableName.PkiAcmeChallenge)
.join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`)
.join(TableName.PkiAcmeAccount, `${TableName.PkiAcmeAuth}.accountId`, `${TableName.PkiAcmeAccount}.id`)
.join(
TableName.PkiCertificateProfile,
`${TableName.PkiAcmeAccount}.profileId`,
`${TableName.PkiCertificateProfile}.id`
)
.select(
selectAllTableCols(TableName.PkiAcmeChallenge),
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("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt"),
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
.forUpdate(TableName.PkiAcmeAuth)
@@ -149,6 +156,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
authExpiresAt,
accountId,
accountPublicKeyThumbprint,
profileId,
projectId,
...challenge
} = result;
return {
@@ -161,7 +170,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
expiresAt: authExpiresAt,
account: {
id: accountId,
publicKeyThumbprint: accountPublicKeyThumbprint
publicKeyThumbprint: accountPublicKeyThumbprint,
project: {
id: projectId
},
profileId
}
}
};

View File

@@ -5,7 +5,9 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange";
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 {
AcmeConnectionError,
@@ -25,10 +27,12 @@ type TPkiAcmeChallengeServiceFactoryDep = {
| "markAsInvalidCascadeById"
| "updateById"
>;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export const pkiAcmeChallengeServiceFactory = ({
acmeChallengeDAL
acmeChallengeDAL,
auditLogService
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig();
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");
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) {
let finalAttempt = false;
if (retryCount >= 2) {
logger.error(
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
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId);
finalAttempt = true;
}
// Properly type and inspect the error
if (axios.isAxiosError(exp)) {
const axiosError = exp as AxiosError;
const errorCode = axiosError.code;
const errorMessage = axiosError.message;
try {
// Properly type and inspect the error
if (axios.isAxiosError(exp)) {
const axiosError = exp as AxiosError;
const errorCode = axiosError.code;
const errorMessage = axiosError.message;
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
throw new AcmeConnectionError({ message: "Connection refused" });
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
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")) {
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" });
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" });
} 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" });
}
};