Harden AWS remote secret imports

This commit is contained in:
Dotta committed 2026-05-07 18:59:05 -05:00
1 parent 02a5f13044
commit ae538db38f
6 files changed
+413 -42

No files matched your search

@@ -269,7 +269,18 @@ describe("awsSecretsManagerProvider", () => {
});
it("stores linked external references as metadata-only provider material", async () => {
const provider = createAwsSecretsManagerProvider();
const provider = createAwsSecretsManagerProvider({
config: {
region: "us-east-1",
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
deploymentId: "prod-use1",
prefix: "paperclip",
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
environmentTag: "production",
providerOwnerTag: "paperclip",
deleteRecoveryWindowDays: 30,
},
});
const prepared = await provider.linkExternalSecret({
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external",
@@ -283,6 +294,29 @@ describe("awsSecretsManagerProvider", () => {
expect(prepared.valueSha256).toBeTruthy();
});
it("rejects linked external references under the Paperclip-managed namespace", async () => {
const provider = createAwsSecretsManagerProvider({
config: {
region: "us-east-1",
endpoint: "https://secretsmanager.us-east-1.amazonaws.com",
deploymentId: "prod-use1",
prefix: "paperclip",
kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test",
environmentTag: "production",
providerOwnerTag: "paperclip",
deleteRecoveryWindowDays: 30,
},
});
await expect(
provider.linkExternalSecret({
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key",
providerVersionRef: "linked-version-7",
}),
).rejects.toThrow(/Paperclip-managed namespace/i);
});
it("lists remote AWS secrets with metadata only and never resolves plaintext", async () => {
const calls: Array<{ op: string; input: Record<string, unknown> }> = [];
const provider = createAwsSecretsManagerProvider({
@@ -352,15 +386,16 @@ describe("awsSecretsManagerProvider", () => {
name: "prod/openai",
providerVersionRef: null,
metadata: expect.objectContaining({
arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
name: "prod/openai",
description: "OpenAI API key",
createdDate: "2026-05-06T00:00:00.000Z",
hasDescription: true,
tagCount: 1,
}),
},
],
});
expect(JSON.stringify(listed)).not.toContain("SecretString");
expect(JSON.stringify(listed)).not.toContain("OpenAI API key");
expect(JSON.stringify(listed)).not.toContain("team");
});
it("resolves AWS secret values by provider version reference", async () => {
@@ -3,6 +3,7 @@ import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { secretRoutes } from "../routes/secrets.js";
import { errorHandler } from "../middleware/error-handler.js";
import { unprocessable } from "../errors.js";
const mockSecretService = vi.hoisted(() => ({
listProviders: vi.fn(),
@@ -14,7 +15,9 @@ const mockSecretService = vi.hoisted(() => ({
disableProviderConfig: vi.fn(),
setDefaultProviderConfig: vi.fn(),
checkProviderConfigHealth: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
previewRemoteImport: vi.fn(),
importRemoteSecrets: vi.fn(),
}));
@@ -357,4 +360,35 @@ describe("secret routes", () => {
}));
expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("prod/openai");
});
it("surfaces update-route externalRef retarget rejection without logging raw refs", async () => {
mockSecretService.getById.mockResolvedValue({
id: "22222222-2222-4222-8222-222222222222",
companyId: "company-1",
name: "OpenAI API key",
key: "openai-api-key",
provider: "aws_secrets_manager",
managedMode: "external_reference",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original",
});
mockSecretService.update.mockRejectedValue(
unprocessable("External reference secrets cannot be retargeted through generic update"),
);
const res = await request(createApp())
.patch("/api/secrets/22222222-2222-4222-8222-222222222222")
.send({
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed",
});
expect(res.status).toBe(422);
expect(mockSecretService.update).toHaveBeenCalledWith(
"22222222-2222-4222-8222-222222222222",
expect.objectContaining({
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed",
}),
);
expect(mockLogActivity).not.toHaveBeenCalled();
expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("shared/repointed");
});
});
+144 -2
View File
@@ -206,17 +206,23 @@ describeEmbeddedPostgres("secretService", () => {
it("stores external references without requiring or persisting secret values", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
managedMode: "external_reference",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/test",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/test",
providerVersionRef: "version-1",
});
expect(secret.managedMode).toBe("external_reference");
expect(secret.externalRef).toBe("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/test");
expect(secret.externalRef).toBe("arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/test");
const versions = await db
.select()
@@ -439,6 +445,7 @@ describeEmbeddedPostgres("secretService", () => {
name: "prod/ready",
key: "prod-ready",
});
expect(preview.candidates[2]?.providerMetadata).toBeNull();
});
it("imports AWS remote references row-by-row without fetching plaintext", async () => {
@@ -500,6 +507,7 @@ describeEmbeddedPostgres("secretService", () => {
managedMode: "external_reference",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
createdByUserId: "user-1",
providerMetadata: null,
});
const versions = await db
@@ -511,6 +519,112 @@ describeEmbeddedPostgres("secretService", () => {
expect(JSON.stringify(versions[0])).not.toContain("sk-");
});
it("rejects Paperclip-managed AWS namespace refs during preview and import commit", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
vi.spyOn(awsSecretsManagerProvider, "listRemoteSecrets").mockResolvedValue({
secrets: [
{
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai",
name: "paperclip/prod-use1/company-b/openai",
providerVersionRef: null,
metadata: {
arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai",
description: "must not leak",
tags: [{ Key: "paperclip:company-id", Value: "company-b" }],
},
},
],
});
const preview = await svc.previewRemoteImport(companyId, {
providerConfigId: awsVault.id,
});
expect(preview.candidates[0]).toMatchObject({
status: "conflict",
importable: false,
conflicts: [expect.objectContaining({ type: "provider_guardrail" })],
providerMetadata: null,
});
expect(JSON.stringify(preview)).not.toContain("must not leak");
expect(JSON.stringify(preview)).not.toContain("paperclip:company-id");
const result = await svc.importRemoteSecrets(companyId, {
providerConfigId: awsVault.id,
secrets: [
{
externalRef:
"arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai",
name: "Foreign managed secret",
key: "foreign-managed-secret",
providerMetadata: {
description: "client-submitted metadata must not persist",
tags: [{ Key: "paperclip:company-id", Value: "company-b" }],
},
},
],
});
expect(result).toMatchObject({
importedCount: 0,
skippedCount: 0,
errorCount: 1,
results: [expect.objectContaining({ status: "error" })],
});
expect(result.results[0]?.reason).toMatch(/Paperclip-managed namespace/i);
const imported = await db.select().from(companySecrets).where(eq(companySecrets.key, "foreign-managed-secret"));
expect(imported).toHaveLength(0);
});
it("skips duplicate AWS remote imports for the same provider vault and canonical ref", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const first = await svc.importRemoteSecrets(companyId, {
providerConfigId: awsVault.id,
secrets: [
{
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
name: "OpenAI API key",
key: "openai-api-key",
},
],
});
const second = await svc.importRemoteSecrets(companyId, {
providerConfigId: awsVault.id,
secrets: [
{
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai",
name: "OpenAI API key duplicate",
key: "openai-api-key-duplicate",
},
],
});
expect(first.importedCount).toBe(1);
expect(second).toMatchObject({
importedCount: 0,
skippedCount: 1,
errorCount: 0,
results: [expect.objectContaining({ reason: "exact_reference_duplicate" })],
});
const imported = await db.select().from(companySecrets).where(eq(companySecrets.providerConfigId, awsVault.id));
expect(imported).toHaveLength(1);
});
it("rejects remote import for disabled or cross-company provider vaults", async () => {
const companyA = await seedCompany("A");
const companyB = await seedCompany("B");
@@ -558,6 +672,34 @@ describeEmbeddedPostgres("secretService", () => {
).rejects.toThrow(/Managed secrets cannot override externalRef/i);
});
it("rejects generic update retargeting for external reference secrets", async () => {
const companyId = await seedCompany();
const svc = secretService(db);
const awsVault = await svc.createProviderConfig(companyId, {
provider: "aws_secrets_manager",
displayName: "AWS production",
config: { region: "us-east-1", namespace: "prod-use1" },
});
const secret = await svc.create(companyId, {
name: `external-${randomUUID()}`,
provider: "aws_secrets_manager",
providerConfigId: awsVault.id,
managedMode: "external_reference",
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original",
});
await expect(
svc.update(secret.id, {
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed",
}),
).rejects.toThrow(/cannot be retargeted/i);
const persisted = await svc.getById(secret.id);
expect(persisted?.externalRef).toBe(
"arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original",
);
});
it("passes managed AWS secret context into provider delete during removal", async () => {
const companyId = await seedCompany();
const svc = secretService(db);