diff --git a/apps/dokploy/__test__/dns/ovh.test.ts b/apps/dokploy/__test__/dns/ovh.test.ts
index 38b968add..f41735682 100644
--- a/apps/dokploy/__test__/dns/ovh.test.ts
+++ b/apps/dokploy/__test__/dns/ovh.test.ts
@@ -600,3 +600,43 @@ describe("ovhClient.testConnection", () => {
expect(calls[0]?.[0]).toBe(`${cfg.baseUrl}/domain/zone/example.com`);
});
});
+
+describe("ovhClient with a configured zone", () => {
+ const record = {
+ type: "A" as const,
+ name: "app.other.org",
+ content: "1.2.3.4",
+ };
+
+ it("refuses record operations on any other zone without calling OVH", async () => {
+ const cfg = { ...freshConfig(), zone: "example.com" };
+ mockApi();
+
+ const message = /restricted to the zone "example.com"/;
+ await expect(ovhClient.listRecords(cfg, "other.org")).rejects.toThrow(
+ message,
+ );
+ await expect(
+ ovhClient.upsertRecord(cfg, { ...record, zoneId: "other.org" }),
+ ).rejects.toThrow(message);
+ await expect(
+ ovhClient.updateRecord(cfg, "other.org", "1", record),
+ ).rejects.toThrow(message);
+ await expect(ovhClient.deleteRecord(cfg, "other.org", "1")).rejects.toThrow(
+ message,
+ );
+ expect(apiCalls()).toHaveLength(0);
+ });
+
+ it("still allows operations on the configured zone", async () => {
+ const cfg = { ...freshConfig(), zone: "example.com" };
+ mockApi(ovhSuccess(null), ovhSuccess(null));
+
+ await expect(
+ ovhClient.deleteRecord(cfg, "example.com", "1"),
+ ).resolves.toBeUndefined();
+ expect(apiCalls()[0]?.[0]).toBe(
+ `${cfg.baseUrl}/domain/zone/example.com/record/1`,
+ );
+ });
+});
diff --git a/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx b/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx
index 0201999e8..66fdddc69 100644
--- a/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx
+++ b/apps/dokploy/components/dashboard/settings/dns/handle-dns-provider.tsx
@@ -559,9 +559,12 @@ export const HandleDnsProvider = ({ dnsProviderId }: Props) => {
- Restrict this provider to a single DNS zone. Leave empty
- to use every zone the credentials can access. When set,
- the GET /domain/zone right is not needed.
+ Restrict this provider to a single DNS zone: Dokploy
+ will only list and change records in it. Leave empty to
+ use every zone the credentials can access. When set, the{" "}
+ GET /domain/zone right is not needed. This
+ is enforced by Dokploy only, so also limit the OVH token
+ to that zone if you need a hard guarantee.
diff --git a/packages/server/src/utils/dns/ovh.ts b/packages/server/src/utils/dns/ovh.ts
index 8106d3e31..c5a02acc9 100644
--- a/packages/server/src/utils/dns/ovh.ts
+++ b/packages/server/src/utils/dns/ovh.ts
@@ -252,6 +252,14 @@ const listZoneNames = async (config: OvhConfig) => {
}
};
+const assertZoneAllowed = (config: OvhConfig, zoneId: string) => {
+ if (config.zone && zoneId !== config.zone) {
+ throw new Error(
+ `OVH: this provider is restricted to the zone "${config.zone}", not "${zoneId}"`,
+ );
+ }
+};
+
export const ovhClient: DnsClient = {
async listZones(config) {
if (config.zone) {
@@ -262,6 +270,7 @@ export const ovhClient: DnsClient = {
},
async listRecords(config, zoneId) {
+ assertZoneAllowed(config, zoneId);
const zone = encodeURIComponent(zoneId);
// The listing endpoint only returns ids, so each record is fetched on its own.
const ids = await ovhFetch(config, `/domain/zone/${zone}/record`);
@@ -279,6 +288,7 @@ export const ovhClient: DnsClient = {
},
async upsertRecord(config, record) {
+ assertZoneAllowed(config, record.zoneId);
const zone = encodeURIComponent(record.zoneId);
const subDomain = toSubDomain(record.name, record.zoneId);
const existing = await ovhFetch(
@@ -322,6 +332,7 @@ export const ovhClient: DnsClient = {
},
async updateRecord(config, zoneId, recordId, record) {
+ assertZoneAllowed(config, zoneId);
const zone = encodeURIComponent(zoneId);
const existing = await ovhFetch(
config,
@@ -366,6 +377,7 @@ export const ovhClient: DnsClient = {
},
async deleteRecord(config, zoneId, recordId) {
+ assertZoneAllowed(config, zoneId);
await ovhFetch(
config,
`/domain/zone/${encodeURIComponent(zoneId)}/record/${recordId}`,