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

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}`;

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}`;

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 {

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({

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({

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({

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 { 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 {

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,
};
}

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;
}

View File

@@ -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);

View File

@@ -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;