Compare commits

...

7 Commits

Author SHA1 Message Date
Mauricio Siu
1f4f94042f fix: prevent registry password from appearing in error messages and shell commands (#4579) 2026-06-08 09:20:34 -06:00
Mauricio Siu
e9a0932b23 fix: correct git provider access check for existing deploys (#4570)
* fix: use canEditDeployGitSource for git provider access on existing deploys

Replaces the simple userId ownership check with a new canEditDeployGitSource
function that correctly handles all role/sharing scenarios. Owner always has
access; admin and member only if they own the provider or it is shared with
the org — being assigned via accessedGitProviders (enterprise) only grants
permission to connect new deploys, not to edit the git source of existing ones.

Adds 26 unit tests covering owner, admin, member (with/without enterprise
license), shared providers, and the key regression case from issue #4469.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-07 02:10:49 -06:00
Mauricio Siu
6b68fcab8c fix: strip credentials from gitProvider.getAll API response (#4569)
* fix: strip credentials from gitProvider.getAll API response

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-07 01:29:04 -06:00
Mauricio Siu
dfbae18557 fix: correct deriveCookieSecret test to validate 16-byte hex secret as per oauth2-proxy requirements 2026-06-07 01:25:05 -06:00
Mauricio Siu
c1c887d03c fix: update deriveCookieSecret to meet oauth2-proxy requirements 2026-06-07 00:50:20 -06:00
Mauricio Siu
0f77c40ee3 refactor: replace BETTER_AUTH_SECRET with betterAuthSecret in forward-auth setup 2026-06-07 00:28:57 -06:00
Mauricio Siu
a0288f83d5 fix: enforce docker:read on container start/stop/kill/restart mutations (#4568) 2026-06-07 00:18:40 -06:00
12 changed files with 531 additions and 71 deletions

View File

@@ -0,0 +1,369 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
canEditDeployGitSource,
getAccessibleGitProviderIds,
} from "@dokploy/server/services/git-provider";
const mockDb = vi.hoisted(() => ({
query: {
gitProvider: {
findMany: vi.fn(),
findFirst: vi.fn(),
},
member: {
findFirst: vi.fn(),
},
},
}));
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
const mockHasValidLicense = vi.hoisted(() => vi.fn());
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: mockHasValidLicense,
}));
const ORG_ID = "org-1";
const USER_OWNER = "user-owner";
const USER_ADMIN = "user-admin";
const USER_MEMBER = "user-member";
const USER_MEMBER_2 = "user-member-2";
const providerOwned = {
gitProviderId: "gp-owned",
userId: USER_MEMBER,
sharedWithOrganization: false,
};
const providerShared = {
gitProviderId: "gp-shared",
userId: USER_OWNER,
sharedWithOrganization: true,
};
const providerPrivate = {
gitProviderId: "gp-private",
userId: USER_OWNER,
sharedWithOrganization: false,
};
const providerOtherMember = {
gitProviderId: "gp-other",
userId: USER_MEMBER_2,
sharedWithOrganization: false,
};
const allProviders = [
providerOwned,
providerShared,
providerPrivate,
providerOtherMember,
];
function session(userId: string) {
return { userId, activeOrganizationId: ORG_ID };
}
beforeEach(() => {
vi.clearAllMocks();
mockDb.query.gitProvider.findMany.mockResolvedValue(allProviders);
mockHasValidLicense.mockResolvedValue(false);
});
describe("getAccessibleGitProviderIds", () => {
describe("owner", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "owner",
accessedGitProviders: [],
});
});
it("returns all org providers", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
});
it("includes providers owned by other members", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
});
});
describe("admin", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "admin",
accessedGitProviders: [],
});
});
it("returns all org providers", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
});
it("includes providers owned by other members — fixes issue #4469", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
});
});
describe("member without enterprise license", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [providerPrivate.gitProviderId],
});
mockHasValidLicense.mockResolvedValue(false);
});
it("can access their own provider", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
});
it("can access shared providers", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerShared.gitProviderId)).toBe(true);
});
it("cannot access private providers of other users even if assigned (no license)", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
});
it("cannot access providers of other members", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
});
});
describe("member with enterprise license", () => {
beforeEach(() => {
mockHasValidLicense.mockResolvedValue(true);
});
it("can access provider explicitly assigned to them", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [providerPrivate.gitProviderId],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
});
it("cannot access provider not assigned and not shared", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
});
it("can access shared provider even without explicit assignment", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerShared.gitProviderId)).toBe(true);
});
it("can access own provider regardless of assignments", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
});
it("cannot access provider of other member even with license but no assignment", async () => {
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
});
});
describe("member with no member record", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue(null);
mockHasValidLicense.mockResolvedValue(true);
});
it("only returns own providers and shared ones", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
expect(ids.has(providerShared.gitProviderId)).toBe(true);
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
});
});
describe("enterprise license — member assigned to a provider they do not own", () => {
// getAccessibleGitProviderIds still returns the provider (member can connect NEW deploys)
it("member assigned to owner's private provider can USE the provider for new deploys", async () => {
mockHasValidLicense.mockResolvedValue(true);
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [providerPrivate.gitProviderId],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
});
it("member NOT assigned to owner's private provider cannot use it at all", async () => {
mockHasValidLicense.mockResolvedValue(true);
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
});
});
describe("empty org", () => {
beforeEach(() => {
mockDb.query.gitProvider.findMany.mockResolvedValue([]);
mockDb.query.member.findFirst.mockResolvedValue({
role: "admin",
accessedGitProviders: [],
});
});
it("returns empty set when org has no providers", async () => {
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
expect(ids.size).toBe(0);
});
});
});
describe("canEditDeployGitSource", () => {
beforeEach(() => {
vi.clearAllMocks();
mockHasValidLicense.mockResolvedValue(true);
});
describe("owner", () => {
it("can edit deploy using any provider", async () => {
mockDb.query.member.findFirst.mockResolvedValue({ role: "owner" });
const result = await canEditDeployGitSource(
providerPrivate.gitProviderId,
session(USER_OWNER),
);
expect(result).toBe(true);
});
});
describe("admin", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue({ role: "admin" });
});
it("cannot edit deploy using owner's private provider (not shared)", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_OWNER,
sharedWithOrganization: false,
});
const result = await canEditDeployGitSource(
providerPrivate.gitProviderId,
session(USER_ADMIN),
);
expect(result).toBe(false);
});
it("can edit deploy using a provider shared with the org", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_OWNER,
sharedWithOrganization: true,
});
const result = await canEditDeployGitSource(
providerShared.gitProviderId,
session(USER_ADMIN),
);
expect(result).toBe(true);
});
it("can edit deploy using their own provider", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_ADMIN,
sharedWithOrganization: false,
});
const result = await canEditDeployGitSource(
"gp-admin-owned",
session(USER_ADMIN),
);
expect(result).toBe(true);
});
});
describe("member", () => {
beforeEach(() => {
mockDb.query.member.findFirst.mockResolvedValue({ role: "member" });
});
it("can edit deploy using their own provider", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_MEMBER,
sharedWithOrganization: false,
});
const result = await canEditDeployGitSource(
providerOwned.gitProviderId,
session(USER_MEMBER),
);
expect(result).toBe(true);
});
it("can edit deploy using a provider shared with the org", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_OWNER,
sharedWithOrganization: true,
});
const result = await canEditDeployGitSource(
providerShared.gitProviderId,
session(USER_MEMBER),
);
expect(result).toBe(true);
});
it("cannot edit deploy using owner's private provider even with enterprise license and assignment", async () => {
// This is the key case: enterprise, provider del owner, no compartido,
// member tiene accessedGitProviders asignado — pero NO puede cambiar la branch del deploy del owner
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_OWNER,
sharedWithOrganization: false,
});
const result = await canEditDeployGitSource(
providerPrivate.gitProviderId,
session(USER_MEMBER),
);
expect(result).toBe(false);
});
it("cannot edit deploy using another member's private provider", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_MEMBER_2,
sharedWithOrganization: false,
});
const result = await canEditDeployGitSource(
providerOtherMember.gitProviderId,
session(USER_MEMBER),
);
expect(result).toBe(false);
});
it("returns false if provider does not exist", async () => {
mockDb.query.gitProvider.findFirst.mockResolvedValue(null);
const result = await canEditDeployGitSource(
"nonexistent-id",
session(USER_MEMBER),
);
expect(result).toBe(false);
});
});
});

View File

@@ -226,8 +226,8 @@ describe("deriveCookieSecret", () => {
);
});
test("produces a 32-byte base64 secret (oauth2-proxy requirement)", () => {
test("produces a 16-byte hex secret (oauth2-proxy requirement)", () => {
const secret = deriveCookieSecret(".acme.com");
expect(Buffer.from(secret, "base64")).toHaveLength(32);
expect(Buffer.from(secret, "hex")).toHaveLength(16);
});
});

View File

@@ -134,15 +134,10 @@ export const ShowGitProviders = () => {
const canManage = gitProvider.isOwner || isOrgAdmin;
const haveGithubRequirements =
isGithub &&
gitProvider.github?.githubPrivateKey &&
gitProvider.github?.githubAppId &&
gitProvider.github?.githubInstallationId;
isGithub && gitProvider.github?.isConfigured;
const haveGitlabRequirements =
isGitlab &&
gitProvider.gitlab?.accessToken &&
gitProvider.gitlab?.refreshToken;
isGitlab && gitProvider.gitlab?.isConfigured;
return (
<div
@@ -230,8 +225,7 @@ export const ShowGitProviders = () => {
)}
{isBitbucket &&
gitProvider.bitbucket?.appPassword &&
!gitProvider.bitbucket?.apiToken ? (
gitProvider.bitbucket?.isDeprecated ? (
<Badge variant="yellow">Deprecated</Badge>
) : null}
@@ -244,7 +238,7 @@ export const ShowGitProviders = () => {
Action Required
</Badge>
<Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github?.githubId}`}
className={buttonVariants({
size: "icon",
variant: "ghost",
@@ -280,7 +274,7 @@ export const ShowGitProviders = () => {
href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "",
gitProvider.gitlab?.gitlabId || "",
gitProvider.gitlab?.gitlabUrl,
gitProvider.gitlab?.gitlabUrl || "",
)}
target="_blank"
className={buttonVariants({
@@ -295,29 +289,33 @@ export const ShowGitProviders = () => {
{canManage && (
<>
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github?.githubId}
/>
)}
{isGithub &&
haveGithubRequirements &&
gitProvider.github?.githubId && (
<EditGithubProvider
githubId={gitProvider.github.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isGitlab &&
gitProvider.gitlab?.gitlabId && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isBitbucket &&
gitProvider.bitbucket?.bitbucketId && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket.bitbucketId
}
/>
)}
{isGitea && (
{isGitea && gitProvider.gitea?.giteaId && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
giteaId={gitProvider.gitea.giteaId}
/>
)}

View File

@@ -4,7 +4,6 @@ import {
deleteAllMiddlewares,
findApplicationById,
findEnvironmentById,
findGitProviderById,
findProjectById,
getAccessibleServerIds,
getApplicationStats,
@@ -31,6 +30,7 @@ import {
writeConfigRemote,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
import {
addNewService,
checkServiceAccess,
@@ -174,13 +174,11 @@ export const applicationRouter = createTRPCRouter({
const gitProviderId = getGitProviderId();
if (gitProviderId) {
try {
const gitProvider = await findGitProviderById(gitProviderId);
if (gitProvider.userId !== ctx.session.userId) {
hasGitProviderAccess = false;
unauthorizedProvider = application.sourceType;
}
} catch {
const canEdit = await canEditDeployGitSource(
gitProviderId,
ctx.session,
);
if (!canEdit) {
hasGitProviderAccess = false;
unauthorizedProvider = application.sourceType;
}

View File

@@ -13,7 +13,6 @@ import {
findComposeById,
findDomainsByComposeId,
findEnvironmentById,
findGitProviderById,
findProjectById,
findServerById,
getAccessibleServerIds,
@@ -34,6 +33,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
import {
addNewService,
checkServiceAccess,
@@ -173,13 +173,11 @@ export const composeRouter = createTRPCRouter({
const gitProviderId = getGitProviderId();
if (gitProviderId) {
try {
const gitProvider = await findGitProviderById(gitProviderId);
if (gitProvider.userId !== ctx.session.userId) {
hasGitProviderAccess = false;
unauthorizedProvider = compose.sourceType;
}
} catch {
const canEdit = await canEditDeployGitSource(
gitProviderId,
ctx.session,
);
if (!canEdit) {
hasGitProviderAccess = false;
unauthorizedProvider = compose.sourceType;
}

View File

@@ -38,7 +38,7 @@ export const dockerRouter = createTRPCRouter({
return await getContainers(input.serverId);
}),
restartContainer: withPermission("service", "read")
restartContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
@@ -64,7 +64,7 @@ export const dockerRouter = createTRPCRouter({
});
}),
startContainer: withPermission("service", "read")
startContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
@@ -90,7 +90,7 @@ export const dockerRouter = createTRPCRouter({
});
}),
stopContainer: withPermission("service", "read")
stopContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
@@ -116,7 +116,7 @@ export const dockerRouter = createTRPCRouter({
});
}),
killContainer: withPermission("service", "read")
killContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z

View File

@@ -42,6 +42,43 @@ export const gitProviderRouter = createTRPCRouter({
return results.map((r) => ({
...r,
isOwner: r.userId === ctx.session.userId,
github: r.github
? {
githubId: r.github.githubId,
githubAppName: r.github.githubAppName,
githubAppId: r.github.githubAppId,
githubInstallationId: r.github.githubInstallationId,
isConfigured: !!(
r.github.githubPrivateKey &&
r.github.githubAppId &&
r.github.githubInstallationId
),
}
: null,
gitlab: r.gitlab
? {
gitlabId: r.gitlab.gitlabId,
applicationId: r.gitlab.applicationId,
gitlabUrl: r.gitlab.gitlabUrl,
isConfigured: !!(r.gitlab.accessToken && r.gitlab.refreshToken),
}
: null,
bitbucket: r.bitbucket
? {
bitbucketId: r.bitbucket.bitbucketId,
bitbucketUsername: r.bitbucket.bitbucketUsername,
isConfigured: false,
isDeprecated: !!(r.bitbucket.appPassword && !r.bitbucket.apiToken),
}
: null,
gitea: r.gitea
? {
giteaId: r.gitea.giteaId,
giteaUrl: r.gitea.giteaUrl,
clientId: r.gitea.clientId,
isConfigured: !!(r.gitea.accessToken && r.gitea.refreshToken),
}
: null,
}));
}),

View File

@@ -43,6 +43,38 @@ export const updateGitProvider = async (
.then((response) => response[0]);
};
// Returns true if the user can edit the git source configuration of an existing
// deploy that is connected to the given provider.
// Owner/admin: always yes.
// Member: only if they own the provider or it's shared with the org.
// Being in accessedGitProviders only grants permission to connect NEW deploys,
// not to modify the git config of an existing deploy owned by someone else.
export const canEditDeployGitSource = async (
gitProviderId: string,
session: { userId: string; activeOrganizationId: string },
): Promise<boolean> => {
const { userId, activeOrganizationId } = session;
const memberRecord = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, activeOrganizationId),
),
columns: { role: true },
});
if (memberRecord?.role === "owner") return true;
const provider = await db.query.gitProvider.findFirst({
where: eq(gitProvider.gitProviderId, gitProviderId),
columns: { userId: true, sharedWithOrganization: true },
});
if (!provider) return false;
return provider.userId === userId || provider.sharedWithOrganization;
};
export const getAccessibleGitProviderIds = async (session: {
userId: string;
activeOrganizationId: string;

View File

@@ -27,6 +27,16 @@ export function safeDockerLoginCommand(
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}
function sanitizeRegistryError(
error: unknown,
password: string | null | undefined,
): string {
const message =
error instanceof Error ? error.message : "Error with registry login";
if (!password) return message;
return message.split(password).join("***");
}
export const createRegistry = async (
input: z.infer<typeof apiCreateRegistry>,
organizationId: string,
@@ -59,10 +69,15 @@ export const createRegistry = async (
input.username,
input.password,
);
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
await execAsync(loginCommand);
try {
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
await execAsync(loginCommand);
}
} catch (error) {
const sanitized = sanitizeRegistryError(error, input.password);
throw new TRPCError({ code: "BAD_REQUEST", message: sanitized });
}
return newRegistry;
@@ -129,16 +144,24 @@ export const updateRegistry = async (
});
}
if (registryData?.serverId && registryData?.serverId !== "none") {
await execAsyncRemote(registryData.serverId, loginCommand);
} else if (response?.registryType === "cloud") {
await execAsync(loginCommand);
try {
if (registryData?.serverId && registryData?.serverId !== "none") {
await execAsyncRemote(registryData.serverId, loginCommand);
} else if (response?.registryType === "cloud") {
await execAsync(loginCommand);
}
} catch (execError) {
throw new Error(sanitizeRegistryError(execError, response?.password));
}
return response;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating this registry";
error instanceof TRPCError
? error.message
: error instanceof Error
? error.message
: "Error updating this registry";
throw new TRPCError({
code: "BAD_REQUEST",
message,

View File

@@ -1,5 +1,6 @@
import { createHmac } from "node:crypto";
import type { CreateServiceOptions } from "dockerode";
import { betterAuthSecret } from "../lib/auth-secret";
import { getRemoteDocker } from "../utils/servers/remote-docker";
export const FORWARD_AUTH_SERVICE_NAME = "dokploy-forward-auth";
@@ -37,15 +38,12 @@ export const forwardAuthCallbackUrl = (
): string => `${https ? "https" : "http"}://${authDomain}/oauth2/callback`;
export const deriveCookieSecret = (salt: string): string => {
const rootSecret = process.env.BETTER_AUTH_SECRET;
if (!rootSecret) {
throw new Error(
"BETTER_AUTH_SECRET is required to derive the forward-auth cookie secret",
);
}
return createHmac("sha256", rootSecret)
// oauth2-proxy requires cookie_secret to be exactly 16, 24, or 32 bytes.
// Take the first 32 hex chars (= 16 bytes) to satisfy that constraint.
return createHmac("sha256", betterAuthSecret)
.update(`forward-auth:${salt}`)
.digest("base64");
.digest("hex")
.slice(0, 32);
};
export const buildForwardAuthEnv = (

View File

@@ -1,6 +1,7 @@
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
import {
findRegistryByIdWithCredentials,
safeDockerLoginCommand,
type Registry,
} from "@dokploy/server/services/registry";
import { createRollback } from "@dokploy/server/services/rollbacks";
@@ -117,9 +118,14 @@ const getRegistryCommands = (
imageName: string,
registryTag: string,
): string => {
const loginCmd = safeDockerLoginCommand(
registry.registryUrl,
registry.username,
registry.password,
);
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
${loginCmd} || {
echo "❌ DockerHub Failed" ;
exit 1;
}

View File

@@ -1,3 +1,4 @@
import { safeDockerLoginCommand } from "@dokploy/server/services/registry";
import type { ApplicationNested } from "../builders";
export const buildRemoteDocker = async (application: ApplicationNested) => {
@@ -13,7 +14,7 @@ echo "Pulling ${dockerImage}";
if (username && password) {
command += `
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then
if ! ${safeDockerLoginCommand(registryUrl || "", username, password)} 2>&1; then
echo "❌ Login failed";
exit 1;
fi