fix: strip credentials from service-level API responses (#4564)

* fix: strip credentials from service-level API responses

Registry passwords and S3 destination credentials were being returned
in service `.one` tRPC endpoints to any user with service-level read
access. Reported by Nihon Kohden Corporation security team.

- Strip registry `password` from `findApplicationById` via Drizzle `columns: { password: false }`
- Strip destination `accessKey`/`secretAccessKey` from all DB service finders (postgres, mysql, mariadb, mongo, libsql, compose, backup, volume-backups)
- Add `findRegistryByIdWithCredentials` for internal use only
- Builders and upload utils now load registry credentials by ID at execution time
- `createRollback` enriches `fullContext` with registry credentials before persisting to DB so rollback execution has what it needs
- Remove `findApplicationByIdWithCredentials` and `ApplicationNestedWithCredentials` — no longer needed
- Backup execution utils load full destination via `findDestinationById` at runtime instead of reading from the joined relation

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Mauricio Siu
2026-06-06 17:45:24 -06:00
committed by GitHub
parent f35f3064e9
commit c968a2755e
22 changed files with 169 additions and 82 deletions
+4 -8
View File
@@ -95,26 +95,22 @@ export const findApplicationById = async (applicationId: string) => {
const application = await db.query.applications.findFirst({
where: eq(applications.applicationId, applicationId),
with: {
environment: {
with: {
project: true,
},
},
environment: { with: { project: true } },
domains: true,
deployments: true,
mounts: true,
redirects: true,
security: true,
ports: true,
registry: true,
gitlab: true,
github: true,
bitbucket: true,
gitea: true,
server: true,
previewDeployments: true,
buildRegistry: true,
rollbackRegistry: true,
registry: { columns: { password: false } },
buildRegistry: { columns: { password: false } },
rollbackRegistry: { columns: { password: false } },
},
});
if (!application) {
+12 -2
View File
@@ -34,7 +34,12 @@ export const findBackupById = async (backupId: string) => {
mariadb: true,
mongo: true,
libsql: true,
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
compose: true,
},
});
@@ -83,7 +88,12 @@ export const findBackupsByDbId = async (
mariadb: true,
mongo: true,
libsql: true,
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
},
});
return result || [];
+6 -1
View File
@@ -131,7 +131,12 @@ export const findComposeById = async (composeId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+6 -1
View File
@@ -63,7 +63,12 @@ export const findLibsqlById = async (libsqlId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+6 -1
View File
@@ -68,7 +68,12 @@ export const findMariadbById = async (mariadbId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+6 -1
View File
@@ -63,7 +63,12 @@ export const findMongoById = async (mongoId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+6 -1
View File
@@ -66,7 +66,12 @@ export const findMySqlById = async (mysqlId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+6 -1
View File
@@ -76,7 +76,12 @@ export const findPostgresById = async (postgresId: string) => {
server: true,
backups: {
with: {
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
deployments: true,
},
},
+13
View File
@@ -162,6 +162,19 @@ export const findRegistryById = async (registryId: string) => {
return registryResponse;
};
export const findRegistryByIdWithCredentials = async (registryId: string) => {
const registryResponse = await db.query.registry.findFirst({
where: eq(registry.registryId, registryId),
});
if (!registryResponse) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Registry not found",
});
}
return registryResponse;
};
export const findAllRegistryByOrganizationId = async (
organizationId: string,
) => {
+37 -16
View File
@@ -7,7 +7,6 @@ import {
deployments as deploymentsSchema,
rollbacks,
} from "../db/schema";
import type { ApplicationNested } from "../utils/builders";
import { getRegistryTag } from "../utils/cluster/upload";
import {
calculateResources,
@@ -23,7 +22,11 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import { type Registry, safeDockerLoginCommand } from "./registry";
import {
findRegistryByIdWithCredentials,
type Registry,
safeDockerLoginCommand,
} from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
@@ -56,11 +59,29 @@ export const createRollback = async (
...rest
} = await findApplicationById(deployment.applicationId);
const registry = rest.registryId
? await findRegistryByIdWithCredentials(rest.registryId)
: rest.registry;
const buildRegistry = rest.buildRegistryId
? await findRegistryByIdWithCredentials(rest.buildRegistryId)
: rest.buildRegistry;
const rollbackRegistry = rest.rollbackRegistryId
? await findRegistryByIdWithCredentials(rest.rollbackRegistryId)
: rest.rollbackRegistry;
const fullContextWithCredentials = {
...rest,
registry,
buildRegistry,
rollbackRegistry,
};
await tx
.update(rollbacks)
.set({
image: tagImage,
fullContext: rest,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fullContext: fullContextWithCredentials as any,
})
.where(eq(rollbacks.rollbackId, rollback.rollbackId));
@@ -162,7 +183,6 @@ export const rollback = async (rollbackId: string) => {
if (!result.fullContext) {
throw new Error("Rollback context not found");
}
// Use the full context for rollback
await rollbackApplication(
application.appName,
result.image || "",
@@ -198,24 +218,25 @@ const rollbackApplication = async (
};
mounts: Mount[];
ports: Port[];
rollbackRegistry?: Registry;
rollbackRegistry?: Registry | null;
},
) => {
if (!fullContext) {
throw new Error("Full context is required for rollback");
}
const rollbackRegistry = fullContext.rollbackRegistry ?? undefined;
// Ensure Docker daemon is authenticated with the rollback registry
// before updating the swarm service. The authconfig in CreateServiceOptions
// alone is not sufficient — Docker Swarm also relies on the daemon's
// cached credentials (~/.docker/config.json) to distribute auth to nodes.
if (fullContext.rollbackRegistry) {
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
if (rollbackRegistry) {
await dockerLoginForRegistry(rollbackRegistry, serverId);
}
const docker = await getRemoteDocker(serverId);
// Use the same configuration as mechanizeDockerContainer
const {
env,
mounts,
@@ -246,7 +267,9 @@ const rollbackApplication = async (
UpdateConfig,
Networks,
Ulimits,
} = generateConfigContainer(fullContext as ApplicationNested);
} = generateConfigContainer(
fullContext as Parameters<typeof generateConfigContainer>[0],
);
const bindsMount = generateBindMounts(mounts);
const envVariables = prepareEnvironmentVariables(
@@ -254,18 +277,16 @@ const rollbackApplication = async (
fullContext.environment.project.env,
);
// Build the full registry image path if rollbackRegistry is available
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
let rollbackImage = image;
if (fullContext.rollbackRegistry) {
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
if (rollbackRegistry) {
rollbackImage = getRegistryTag(rollbackRegistry, image);
}
const settings: CreateServiceOptions = {
authconfig: {
password: fullContext.rollbackRegistry?.password || "",
username: fullContext.rollbackRegistry?.username || "",
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
password: rollbackRegistry?.password || "",
username: rollbackRegistry?.username || "",
serveraddress: rollbackRegistry?.registryUrl || "",
},
Name: appName,
TaskTemplate: {
@@ -84,7 +84,12 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
},
},
},
destination: true,
destination: {
columns: {
accessKey: false,
secretAccessKey: false,
},
},
},
});
+2 -1
View File
@@ -4,6 +4,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
@@ -23,7 +24,7 @@ export const runComposeBackup = async (
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix, databaseType, serviceName } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
+4 -2
View File
@@ -1,6 +1,7 @@
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { findDestinationById } from "@dokploy/server/services/destination";
import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm";
@@ -131,9 +132,10 @@ export const keepLatestNBackups = async (
if (!backup.keepLatestCount) return;
try {
const rcloneFlags = getS3Credentials(backup.destination);
const destination = await findDestinationById(backup.destinationId);
const rcloneFlags = getS3Credentials(destination);
const appName = getServiceAppName(backup);
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
const backupFilesPath = `:s3:${destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
// --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`;
+2 -1
View File
@@ -3,6 +3,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Libsql } from "@dokploy/server/services/libsql";
import { findProjectById } from "@dokploy/server/services/project";
@@ -29,7 +30,7 @@ export const runLibsqlBackup = async (
description: "Initializing Backup",
});
const { prefix } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
+2 -1
View File
@@ -3,6 +3,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project";
@@ -23,7 +24,7 @@ export const runMariadbBackup = async (
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
+2 -1
View File
@@ -3,6 +3,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project";
@@ -20,7 +21,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.bson.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
+2 -1
View File
@@ -3,6 +3,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project";
@@ -20,7 +21,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
@@ -3,6 +3,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project";
@@ -29,7 +30,7 @@ export const runPostgresBackup = async (
description: "Initializing Backup",
});
const { prefix } = backup;
const destination = backup.destination;
const destination = await findDestinationById(backup.destinationId);
const backupFileName = `${getBackupTimestamp()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
+21 -22
View File
@@ -1,3 +1,4 @@
import { findRegistryByIdWithCredentials } from "@dokploy/server/services/registry";
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
@@ -28,9 +29,9 @@ export type ApplicationNested = InferResultType<
security: true;
redirects: true;
ports: true;
registry: true;
buildRegistry: true;
rollbackRegistry: true;
registry: { columns: { password: false } };
buildRegistry: { columns: { password: false } };
rollbackRegistry: { columns: { password: false } };
deployments: true;
environment: { with: { project: true } };
}
@@ -121,8 +122,8 @@ export const mechanizeDockerContainer = async (
application.environment.env,
);
const image = getImageName(application);
const authConfig = getAuthConfig(application);
const image = await getImageName(application);
const authConfig = await getAuthConfig(application);
const docker = await getRemoteDocker(application.serverId);
const settings: CreateServiceOptions = {
@@ -190,7 +191,7 @@ export const mechanizeDockerContainer = async (
}
};
const getImageName = (application: ApplicationNested) => {
const getImageName = async (application: ApplicationNested) => {
const { appName, sourceType, dockerImage, registry, buildRegistry } =
application;
const imageName = `${appName}:latest`;
@@ -199,18 +200,18 @@ const getImageName = (application: ApplicationNested) => {
}
if (registry) {
const registryTag = getRegistryTag(registry, imageName);
return registryTag;
const r = await findRegistryByIdWithCredentials(registry.registryId);
return getRegistryTag(r, imageName);
}
if (buildRegistry) {
const registryTag = getRegistryTag(buildRegistry, imageName);
return registryTag;
const r = await findRegistryByIdWithCredentials(buildRegistry.registryId);
return getRegistryTag(r, imageName);
}
return imageName;
};
export const getAuthConfig = (application: ApplicationNested) => {
export const getAuthConfig = async (application: ApplicationNested) => {
const {
registry,
buildRegistry,
@@ -222,23 +223,21 @@ export const getAuthConfig = (application: ApplicationNested) => {
if (sourceType === "docker") {
if (username && password) {
return {
password,
username,
serveraddress: registryUrl || "",
};
return { password, username, serveraddress: registryUrl || "" };
}
} else if (registry) {
const r = await findRegistryByIdWithCredentials(registry.registryId);
return {
password: registry.password,
username: registry.username,
serveraddress: registry.registryUrl,
password: r.password,
username: r.username,
serveraddress: r.registryUrl,
};
} else if (buildRegistry) {
const r = await findRegistryByIdWithCredentials(buildRegistry.registryId);
return {
password: buildRegistry.password,
username: buildRegistry.username,
serveraddress: buildRegistry.registryUrl,
password: r.password,
username: r.username,
serveraddress: r.registryUrl,
};
}
+18 -16
View File
@@ -1,5 +1,8 @@
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
import type { Registry } from "@dokploy/server/services/registry";
import {
findRegistryByIdWithCredentials,
type Registry,
} from "@dokploy/server/services/registry";
import { createRollback } from "@dokploy/server/services/rollbacks";
import type { ApplicationNested } from "../builders";
@@ -22,19 +25,19 @@ export const uploadImageRemoteCommand = async (
const commands: string[] = [];
if (registry) {
const registryTag = getRegistryTag(registry, imageName);
const r = await findRegistryByIdWithCredentials(registry.registryId);
const registryTag = getRegistryTag(r, imageName);
if (registryTag) {
commands.push(`echo "📦 [Enabled Registry Swarm]"`);
commands.push(getRegistryCommands(registry, imageName, registryTag));
commands.push(getRegistryCommands(r, imageName, registryTag));
}
}
if (buildRegistry) {
const buildRegistryTag = getRegistryTag(buildRegistry, imageName);
const r = await findRegistryByIdWithCredentials(buildRegistry.registryId);
const buildRegistryTag = getRegistryTag(r, imageName);
if (buildRegistryTag) {
commands.push(`echo "🔑 [Enabled Build Registry]"`);
commands.push(
getRegistryCommands(buildRegistry, imageName, buildRegistryTag),
);
commands.push(getRegistryCommands(r, imageName, buildRegistryTag));
commands.push(
`echo "⚠️ INFO: After the build is finished, you need to wait a few seconds for the server to download the image and run the container."`,
);
@@ -57,15 +60,13 @@ export const uploadImageRemoteCommand = async (
deploymentId: deploymentId,
});
const rollbackRegistryTag = getRegistryTag(
rollbackRegistry,
rollback?.image || "",
const r = await findRegistryByIdWithCredentials(
rollbackRegistry.registryId,
);
const rollbackRegistryTag = getRegistryTag(r, rollback?.image || "");
if (rollbackRegistryTag) {
commands.push(`echo "🔄 [Enabled Rollback Registry]"`);
commands.push(
getRegistryCommands(rollbackRegistry, imageName, rollbackRegistryTag),
);
commands.push(getRegistryCommands(r, imageName, rollbackRegistryTag));
}
}
try {
@@ -74,6 +75,7 @@ export const uploadImageRemoteCommand = async (
throw error;
}
};
/**
* Extract the repository name from imageName by taking the last part after '/'
* Examples:
@@ -117,17 +119,17 @@ const getRegistryCommands = (
): string => {
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}
echo "✅ Registry Login Success" ;
docker tag ${imageName} ${registryTag} || {
docker tag ${imageName} ${registryTag} || {
echo "❌ Error tagging image" ;
exit 1;
}
echo "✅ Image Tagged" ;
docker push ${registryTag} || {
docker push ${registryTag} || {
echo "❌ Error pushing image" ;
exit 1;
}
@@ -1,6 +1,7 @@
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose";
import { findDestinationById } from "@dokploy/server/services/destination";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import {
getBackupTimestamp,
@@ -31,14 +32,14 @@ export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
) => {
const { serviceType, volumeName, turnOff, prefix } = volumeBackup;
const destination = await findDestinationById(volumeBackup.destinationId);
const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
const destination = volumeBackup.destination;
const s3AppName = getVolumeServiceAppName(volumeBackup);
const backupFileName = `${volumeName}-${getBackupTimestamp()}.tar`;
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`;
const rcloneFlags = getS3Credentials(volumeBackup.destination);
const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
@@ -4,6 +4,7 @@ import {
createDeploymentVolumeBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import {
execAsync,
@@ -77,7 +78,8 @@ const cleanupOldVolumeBackups = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
serverId?: string | null,
) => {
const { keepLatestCount, destination, prefix, volumeName } = volumeBackup;
const { keepLatestCount, prefix, volumeName } = volumeBackup;
const destination = await findDestinationById(volumeBackup.destinationId);
if (!keepLatestCount) return;