diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/add-env-vars.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/add-env-vars.tsx index 54a3709750..b7f7cbf6eb 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/add-env-vars.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/env-variables-section/add-env-vars.tsx @@ -2,7 +2,7 @@ import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { Plus, Trash } from "@unkey/icons"; import { Button, Input, toast } from "@unkey/ui"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { EnvVarSecretSwitch } from "./components/env-var-secret-switch"; import { ENV_VAR_KEY_REGEX, type EnvVar, type EnvVarType } from "./types"; @@ -154,22 +154,25 @@ export function AddEnvVars({ } }; - const getErrors = (entry: EnvVarEntry): { key?: string; value?: string } => { - const errors: { key?: string; value?: string } = {}; - - if (entry.key && !ENV_VAR_KEY_REGEX.test(entry.key)) { - errors.key = "Must be UPPERCASE"; - } else if (entry.key && getExistingEnvVar(entry.key)) { - errors.key = "Already exists"; - } else if (entry.key) { - const duplicates = entries.filter((e) => e.key === entry.key); - if (duplicates.length > 1) { - errors.key = "Duplicate"; + const getErrors = useCallback( + (entry: EnvVarEntry): { key?: string; value?: string } => { + const errors: { key?: string; value?: string } = {}; + + if (entry.key && !ENV_VAR_KEY_REGEX.test(entry.key)) { + errors.key = "Must be UPPERCASE"; + } else if (entry.key && getExistingEnvVar(entry.key)) { + errors.key = "Already exists"; + } else if (entry.key) { + const duplicates = entries.filter((e) => e.key === entry.key); + if (duplicates.length > 1) { + errors.key = "Duplicate"; + } } - } - return errors; - }; + return errors; + }, + [entries, getExistingEnvVar], + ); const validEntries = useMemo( () => @@ -177,7 +180,7 @@ export function AddEnvVars({ const errors = getErrors(e); return e.key && e.value && !errors.key && !errors.value; }), - [entries, getExistingEnvVar], + [entries, getErrors], ); const handleSave = async () => { diff --git a/apps/dashboard/gen/proto/ctrl/v1/secrets_pb.ts b/apps/dashboard/gen/proto/ctrl/v1/secrets_pb.ts index 493806e854..65f4a72f2f 100644 --- a/apps/dashboard/gen/proto/ctrl/v1/secrets_pb.ts +++ b/apps/dashboard/gen/proto/ctrl/v1/secrets_pb.ts @@ -2,18 +2,15 @@ // @generated from file ctrl/v1/secrets.proto (package ctrl.v1, syntax proto3) /* eslint-disable */ -import type { Message } from "@bufbuild/protobuf"; import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; /** * Describes the file ctrl/v1/secrets.proto. */ -export const file_ctrl_v1_secrets: GenFile = - /*@__PURE__*/ - fileDesc( - "ChVjdHJsL3YxL3NlY3JldHMucHJvdG8SB2N0cmwudjEidQoNU2VjcmV0c0NvbmZpZxI0CgdzZWNyZXRzGAEgAygLMiMuY3RybC52MS5TZWNyZXRzQ29uZmlnLlNlY3JldHNFbnRyeRouCgxTZWNyZXRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUKOAQoLY29tLmN0cmwudjFCDFNlY3JldHNQcm90b1ABWjRnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ28vZ2VuL3Byb3RvL2N0cmwvdjE7Y3RybHYxogIDQ1hYqgIHQ3RybC5WMcoCB0N0cmxcVjHiAhNDdHJsXFYxXEdQQk1ldGFkYXRh6gIIQ3RybDo6VjFiBnByb3RvMw", - ); +export const file_ctrl_v1_secrets: GenFile = /*@__PURE__*/ + fileDesc("ChVjdHJsL3YxL3NlY3JldHMucHJvdG8SB2N0cmwudjEidQoNU2VjcmV0c0NvbmZpZxI0CgdzZWNyZXRzGAEgAygLMiMuY3RybC52MS5TZWNyZXRzQ29uZmlnLlNlY3JldHNFbnRyeRouCgxTZWNyZXRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUKOAQoLY29tLmN0cmwudjFCDFNlY3JldHNQcm90b1ABWjRnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ28vZ2VuL3Byb3RvL2N0cmwvdjE7Y3RybHYxogIDQ1hYqgIHQ3RybC5WMcoCB0N0cmxcVjHiAhNDdHJsXFYxXEdQQk1ldGFkYXRh6gIIQ3RybDo6VjFiBnByb3RvMw"); /** * SecretsConfig is stored in the deployments table @@ -34,6 +31,6 @@ export type SecretsConfig = Message<"ctrl.v1.SecretsConfig"> & { * Describes the message ctrl.v1.SecretsConfig. * Use `create(SecretsConfigSchema)` to create a new message. */ -export const SecretsConfigSchema: GenMessage = - /*@__PURE__*/ +export const SecretsConfigSchema: GenMessage = /*@__PURE__*/ messageDesc(file_ctrl_v1_secrets, 0); + diff --git a/apps/dashboard/gen/proto/krane/v1/deployment_pb.ts b/apps/dashboard/gen/proto/krane/v1/deployment_pb.ts index f921f410d3..5741b95d2a 100644 --- a/apps/dashboard/gen/proto/krane/v1/deployment_pb.ts +++ b/apps/dashboard/gen/proto/krane/v1/deployment_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file krane/v1/deployment.proto. */ export const file_krane_v1_deployment: GenFile = /*@__PURE__*/ - fileDesc("ChlrcmFuZS92MS9kZXBsb3ltZW50LnByb3RvEghrcmFuZS52MSL7AQoRRGVwbG95bWVudFJlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEhUKDWRlcGxveW1lbnRfaWQYAiABKAkSDQoFaW1hZ2UYAyABKAkSEAoIcmVwbGljYXMYBCABKA0SFgoOY3B1X21pbGxpY29yZXMYBSABKA0SFwoPbWVtb3J5X3NpemVfbWliGAYgASgEEjoKCGVudl92YXJzGAcgAygLMigua3JhbmUudjEuRGVwbG95bWVudFJlcXVlc3QuRW52VmFyc0VudHJ5Gi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIkoKF0NyZWF0ZURlcGxveW1lbnRSZXF1ZXN0Ei8KCmRlcGxveW1lbnQYASABKAsyGy5rcmFuZS52MS5EZXBsb3ltZW50UmVxdWVzdCJGChhDcmVhdGVEZXBsb3ltZW50UmVzcG9uc2USKgoGc3RhdHVzGAEgASgOMhoua3JhbmUudjEuRGVwbG95bWVudFN0YXR1cyJKChdVcGRhdGVEZXBsb3ltZW50UmVxdWVzdBIvCgpkZXBsb3ltZW50GAEgASgLMhsua3JhbmUudjEuRGVwbG95bWVudFJlcXVlc3QiKwoYVXBkYXRlRGVwbG95bWVudFJlc3BvbnNlEg8KB3BvZF9pZHMYASADKAkiQwoXRGVsZXRlRGVwbG95bWVudFJlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEhUKDWRlcGxveW1lbnRfaWQYAiABKAkiGgoYRGVsZXRlRGVwbG95bWVudFJlc3BvbnNlIkAKFEdldERlcGxveW1lbnRSZXF1ZXN0EhEKCW5hbWVzcGFjZRgBIAEoCRIVCg1kZXBsb3ltZW50X2lkGAIgASgJIj4KFUdldERlcGxveW1lbnRSZXNwb25zZRIlCglpbnN0YW5jZXMYAiADKAsyEi5rcmFuZS52MS5JbnN0YW5jZSJTCghJbnN0YW5jZRIKCgJpZBgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEioKBnN0YXR1cxgDIAEoDjIaLmtyYW5lLnYxLkRlcGxveW1lbnRTdGF0dXMqlgEKEERlcGxveW1lbnRTdGF0dXMSIQodREVQTE9ZTUVOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIdChlERVBMT1lNRU5UX1NUQVRVU19QRU5ESU5HEAESHQoZREVQTE9ZTUVOVF9TVEFUVVNfUlVOTklORxACEiEKHURFUExPWU1FTlRfU1RBVFVTX1RFUk1JTkFUSU5HEAMymwIKEURlcGxveW1lbnRTZXJ2aWNlElkKEENyZWF0ZURlcGxveW1lbnQSIS5rcmFuZS52MS5DcmVhdGVEZXBsb3ltZW50UmVxdWVzdBoiLmtyYW5lLnYxLkNyZWF0ZURlcGxveW1lbnRSZXNwb25zZRJQCg1HZXREZXBsb3ltZW50Eh4ua3JhbmUudjEuR2V0RGVwbG95bWVudFJlcXVlc3QaHy5rcmFuZS52MS5HZXREZXBsb3ltZW50UmVzcG9uc2USWQoQRGVsZXRlRGVwbG95bWVudBIhLmtyYW5lLnYxLkRlbGV0ZURlcGxveW1lbnRSZXF1ZXN0GiIua3JhbmUudjEuRGVsZXRlRGVwbG95bWVudFJlc3BvbnNlQpgBCgxjb20ua3JhbmUudjFCD0RlcGxveW1lbnRQcm90b1ABWjZnaXRodWIuY29tL3Vua2V5ZWQvdW5rZXkvZ28vZ2VuL3Byb3RvL2tyYW5lL3YxO2tyYW5ldjGiAgNLWFiqAghLcmFuZS5WMcoCCEtyYW5lXFYx4gIUS3JhbmVcVjFcR1BCTWV0YWRhdGHqAglLcmFuZTo6VjFiBnByb3RvMw"); + fileDesc("ChlrcmFuZS92MS9kZXBsb3ltZW50LnByb3RvEghrcmFuZS52MSLhAQoRRGVwbG95bWVudFJlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEhUKDWRlcGxveW1lbnRfaWQYAiABKAkSDQoFaW1hZ2UYAyABKAkSEAoIcmVwbGljYXMYBCABKA0SFgoOY3B1X21pbGxpY29yZXMYBSABKA0SFwoPbWVtb3J5X3NpemVfbWliGAYgASgEEhgKEGVudmlyb25tZW50X3NsdWcYByABKAkSHgoWZW5jcnlwdGVkX3NlY3JldHNfYmxvYhgIIAEoDBIWCg5lbnZpcm9ubWVudF9pZBgJIAEoCSJKChdDcmVhdGVEZXBsb3ltZW50UmVxdWVzdBIvCgpkZXBsb3ltZW50GAEgASgLMhsua3JhbmUudjEuRGVwbG95bWVudFJlcXVlc3QiRgoYQ3JlYXRlRGVwbG95bWVudFJlc3BvbnNlEioKBnN0YXR1cxgBIAEoDjIaLmtyYW5lLnYxLkRlcGxveW1lbnRTdGF0dXMiSgoXVXBkYXRlRGVwbG95bWVudFJlcXVlc3QSLwoKZGVwbG95bWVudBgBIAEoCzIbLmtyYW5lLnYxLkRlcGxveW1lbnRSZXF1ZXN0IisKGFVwZGF0ZURlcGxveW1lbnRSZXNwb25zZRIPCgdwb2RfaWRzGAEgAygJIkMKF0RlbGV0ZURlcGxveW1lbnRSZXF1ZXN0EhEKCW5hbWVzcGFjZRgBIAEoCRIVCg1kZXBsb3ltZW50X2lkGAIgASgJIhoKGERlbGV0ZURlcGxveW1lbnRSZXNwb25zZSJAChRHZXREZXBsb3ltZW50UmVxdWVzdBIRCgluYW1lc3BhY2UYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCSI+ChVHZXREZXBsb3ltZW50UmVzcG9uc2USJQoJaW5zdGFuY2VzGAIgAygLMhIua3JhbmUudjEuSW5zdGFuY2UiUwoISW5zdGFuY2USCgoCaWQYASABKAkSDwoHYWRkcmVzcxgCIAEoCRIqCgZzdGF0dXMYAyABKA4yGi5rcmFuZS52MS5EZXBsb3ltZW50U3RhdHVzKpYBChBEZXBsb3ltZW50U3RhdHVzEiEKHURFUExPWU1FTlRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHQoZREVQTE9ZTUVOVF9TVEFUVVNfUEVORElORxABEh0KGURFUExPWU1FTlRfU1RBVFVTX1JVTk5JTkcQAhIhCh1ERVBMT1lNRU5UX1NUQVRVU19URVJNSU5BVElORxADMpsCChFEZXBsb3ltZW50U2VydmljZRJZChBDcmVhdGVEZXBsb3ltZW50EiEua3JhbmUudjEuQ3JlYXRlRGVwbG95bWVudFJlcXVlc3QaIi5rcmFuZS52MS5DcmVhdGVEZXBsb3ltZW50UmVzcG9uc2USUAoNR2V0RGVwbG95bWVudBIeLmtyYW5lLnYxLkdldERlcGxveW1lbnRSZXF1ZXN0Gh8ua3JhbmUudjEuR2V0RGVwbG95bWVudFJlc3BvbnNlElkKEERlbGV0ZURlcGxveW1lbnQSIS5rcmFuZS52MS5EZWxldGVEZXBsb3ltZW50UmVxdWVzdBoiLmtyYW5lLnYxLkRlbGV0ZURlcGxveW1lbnRSZXNwb25zZUKYAQoMY29tLmtyYW5lLnYxQg9EZXBsb3ltZW50UHJvdG9QAVo2Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dvL2dlbi9wcm90by9rcmFuZS92MTtrcmFuZXYxogIDS1hYqgIIS3JhbmUuVjHKAghLcmFuZVxWMeICFEtyYW5lXFYxXEdQQk1ldGFkYXRh6gIJS3JhbmU6OlYxYgZwcm90bzM"); /** * @generated from message krane.v1.DeploymentRequest @@ -47,12 +47,27 @@ export type DeploymentRequest = Message<"krane.v1.DeploymentRequest"> & { memorySizeMib: bigint; /** - * Environment variables to inject into the container. - * Keys are variable names, values are the (decrypted) values. + * Environment slug (e.g., production, staging). * - * @generated from field: map env_vars = 7; + * @generated from field: string environment_slug = 7; */ - envVars: { [key: string]: string }; + environmentSlug: string; + + /** + * Encrypted secrets blob to be decrypted at runtime by unkey-env. + * This is set as UNKEY_SECRETS_BLOB env var in the container. + * unkey-env calls krane's DecryptSecretsBlob RPC to decrypt. + * + * @generated from field: bytes encrypted_secrets_blob = 8; + */ + encryptedSecretsBlob: Uint8Array; + + /** + * Environment ID for secrets decryption (keyring identifier). + * + * @generated from field: string environment_id = 9; + */ + environmentId: string; }; /** diff --git a/apps/dashboard/gen/proto/krane/v1/secrets_pb.ts b/apps/dashboard/gen/proto/krane/v1/secrets_pb.ts new file mode 100644 index 0000000000..ca40d9a21a --- /dev/null +++ b/apps/dashboard/gen/proto/krane/v1/secrets_pb.ts @@ -0,0 +1,96 @@ +// @generated by protoc-gen-es v2.8.0 with parameter "target=ts" +// @generated from file krane/v1/secrets.proto (package krane.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file krane/v1/secrets.proto. + */ +export const file_krane_v1_secrets: GenFile = /*@__PURE__*/ + fileDesc("ChZrcmFuZS92MS9zZWNyZXRzLnByb3RvEghrcmFuZS52MSJxChlEZWNyeXB0U2VjcmV0c0Jsb2JSZXF1ZXN0EhYKDmVuY3J5cHRlZF9ibG9iGAEgASgMEhYKDmVudmlyb25tZW50X2lkGAIgASgJEg0KBXRva2VuGAMgASgJEhUKDWRlcGxveW1lbnRfaWQYBCABKAkikQEKGkRlY3J5cHRTZWNyZXRzQmxvYlJlc3BvbnNlEkMKCGVudl92YXJzGAEgAygLMjEua3JhbmUudjEuRGVjcnlwdFNlY3JldHNCbG9iUmVzcG9uc2UuRW52VmFyc0VudHJ5Gi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBMnEKDlNlY3JldHNTZXJ2aWNlEl8KEkRlY3J5cHRTZWNyZXRzQmxvYhIjLmtyYW5lLnYxLkRlY3J5cHRTZWNyZXRzQmxvYlJlcXVlc3QaJC5rcmFuZS52MS5EZWNyeXB0U2VjcmV0c0Jsb2JSZXNwb25zZUKVAQoMY29tLmtyYW5lLnYxQgxTZWNyZXRzUHJvdG9QAVo2Z2l0aHViLmNvbS91bmtleWVkL3Vua2V5L2dvL2dlbi9wcm90by9rcmFuZS92MTtrcmFuZXYxogIDS1hYqgIIS3JhbmUuVjHKAghLcmFuZVxWMeICFEtyYW5lXFYxXEdQQk1ldGFkYXRh6gIJS3JhbmU6OlYxYgZwcm90bzM"); + +/** + * @generated from message krane.v1.DecryptSecretsBlobRequest + */ +export type DecryptSecretsBlobRequest = Message<"krane.v1.DecryptSecretsBlobRequest"> & { + /** + * The encrypted secrets blob from the pod spec (UNKEY_SECRETS_BLOB env var). + * This is the SecretsConfig proto, encrypted with the environment's vault keyring. + * + * @generated from field: bytes encrypted_blob = 1; + */ + encryptedBlob: Uint8Array; + + /** + * Environment ID (keyring) to use for decryption. + * + * @generated from field: string environment_id = 2; + */ + environmentId: string; + + /** + * Token for authentication (K8s service account token or DB-stored token). + * + * @generated from field: string token = 3; + */ + token: string; + + /** + * Deployment ID for token validation. + * + * @generated from field: string deployment_id = 4; + */ + deploymentId: string; +}; + +/** + * Describes the message krane.v1.DecryptSecretsBlobRequest. + * Use `create(DecryptSecretsBlobRequestSchema)` to create a new message. + */ +export const DecryptSecretsBlobRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_krane_v1_secrets, 0); + +/** + * @generated from message krane.v1.DecryptSecretsBlobResponse + */ +export type DecryptSecretsBlobResponse = Message<"krane.v1.DecryptSecretsBlobResponse"> & { + /** + * Decrypted environment variables (key -> plaintext value) + * + * @generated from field: map env_vars = 1; + */ + envVars: { [key: string]: string }; +}; + +/** + * Describes the message krane.v1.DecryptSecretsBlobResponse. + * Use `create(DecryptSecretsBlobResponseSchema)` to create a new message. + */ +export const DecryptSecretsBlobResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_krane_v1_secrets, 1); + +/** + * SecretsService provides decrypted secrets to running workloads. + * Called by the unkey-env binary injected into customer pods/containers. + * + * @generated from service krane.v1.SecretsService + */ +export const SecretsService: GenService<{ + /** + * DecryptSecretsBlob decrypts an encrypted secrets blob passed in the pod spec. + * This avoids DB lookups - the encrypted blob travels with the pod. + * Authentication is via K8s service account token or DB-stored token. + * + * @generated from rpc krane.v1.SecretsService.DecryptSecretsBlob + */ + decryptSecretsBlob: { + methodKind: "unary"; + input: typeof DecryptSecretsBlobRequestSchema; + output: typeof DecryptSecretsBlobResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_krane_v1_secrets, 0); + diff --git a/apps/dashboard/lib/trpc/routers/deploy/env-vars/create.ts b/apps/dashboard/lib/trpc/routers/deploy/env-vars/create.ts index e19bc8d9c2..06a1ff6385 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/env-vars/create.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/env-vars/create.ts @@ -50,7 +50,7 @@ export const createEnvVars = t.procedure const encryptedVars = await Promise.all( input.variables.map(async (v) => { const { encrypted } = await vault.encrypt({ - keyring: ctx.workspace.id, + keyring: input.environmentId, data: v.value, }); diff --git a/apps/dashboard/lib/trpc/routers/deploy/env-vars/decrypt.ts b/apps/dashboard/lib/trpc/routers/deploy/env-vars/decrypt.ts index 291e03ec83..f76594c32c 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/env-vars/decrypt.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/env-vars/decrypt.ts @@ -35,6 +35,7 @@ export const decryptEnvVar = t.procedure id: true, value: true, type: true, + environmentId: true, }, }); @@ -53,7 +54,7 @@ export const decryptEnvVar = t.procedure } const { plaintext } = await vault.decrypt({ - keyring: ctx.workspace.id, + keyring: envVar.environmentId, encrypted: envVar.value, }); diff --git a/apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts b/apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts index 03981520ee..4dc22f11e5 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts @@ -34,6 +34,7 @@ export const updateEnvVar = t.procedure id: true, type: true, key: true, + environmentId: true, }, }); @@ -59,7 +60,7 @@ export const updateEnvVar = t.procedure } const { encrypted } = await vault.encrypt({ - keyring: ctx.workspace.id, + keyring: envVar.environmentId, data: input.value, }); diff --git a/deployment/docker-compose.yaml b/deployment/docker-compose.yaml index 7a886ce58d..e73b098712 100644 --- a/deployment/docker-compose.yaml +++ b/deployment/docker-compose.yaml @@ -381,6 +381,7 @@ services: UNKEY_VAULT_S3_ACCESS_KEY_SECRET: "minio_root_password" UNKEY_VAULT_MASTER_KEYS: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" # ACME Vault - Let's Encrypt certificates + UNKEY_ACME_VAULT_MASTER_KEYS: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" UNKEY_ACME_VAULT_S3_URL: "http://s3:3902" UNKEY_ACME_VAULT_S3_BUCKET: "acme-vault" UNKEY_ACME_VAULT_S3_ACCESS_KEY_ID: "minio_root_user" diff --git a/go/.gitignore b/go/.gitignore index eb6a2ad173..96530c8822 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -1,3 +1,4 @@ -unkey +/unkey +/unkey-env # Added by goreleaser init: dist/ diff --git a/go/Dockerfile.unkey-env b/go/Dockerfile.unkey-env new file mode 100644 index 0000000000..a88e084fea --- /dev/null +++ b/go/Dockerfile.unkey-env @@ -0,0 +1,10 @@ +# Minimal image for unkey-env binary +# This image is injected as an init container into customer pods +FROM alpine:3.19 + +# Add ca-certificates for HTTPS calls to krane +RUN apk --no-cache add ca-certificates + +COPY bin/unkey-env /unkey-env + +ENTRYPOINT ["/unkey-env"] diff --git a/go/Tiltfile b/go/Tiltfile index e9df8baa9a..deabf2e4ed 100644 --- a/go/Tiltfile +++ b/go/Tiltfile @@ -15,6 +15,9 @@ debug_mode = cfg.get('debug', False) print("Tilt starting with services: %s" % services) +# Suppress warnings for images used indirectly (injected into pods by webhook) +update_settings(suppress_unused_image_warnings=["unkey-env:latest"]) + # Create namespace using the extension with allow_duplicates namespace_create('unkey', allow_duplicates=True) @@ -30,6 +33,7 @@ start_ctrl = 'all' in services or 'ctrl' in services start_krane = 'all' in services or 'krane' in services start_dashboard = 'all' in services or 'dashboard' in services start_agent = 'all' in services or 'agent' in services +start_secrets_webhook = 'all' in services or 'secrets-webhook' in services # Apply RBAC k8s_yaml('k8s/manifests/rbac.yaml') @@ -141,7 +145,7 @@ if start_observability: ) # Build Unkey binary locally (independent of infrastructure) -if start_api or start_ctrl or start_krane: +if start_api or start_ctrl or start_krane or start_secrets_webhook: print("Building Unkey binary...") # Build locally first for faster updates local_resource( @@ -254,6 +258,57 @@ if start_krane: trigger_mode=TRIGGER_MODE_AUTO ) +# Secrets Webhook (mutating admission controller for secrets injection) +if start_secrets_webhook: + print("Setting up Secrets Webhook...") + + # Generate TLS certs for the webhook (mkcert must be installed) + local_resource( + 'secrets-webhook-tls', + ''' + mkcert -cert-file /tmp/secrets-webhook-tls.crt -key-file /tmp/secrets-webhook-tls.key \ + secrets-webhook.unkey.svc secrets-webhook.unkey.svc.cluster.local && \ + kubectl delete secret secrets-webhook-tls -n unkey --ignore-not-found && \ + kubectl create secret tls secrets-webhook-tls -n unkey \ + --cert=/tmp/secrets-webhook-tls.crt --key=/tmp/secrets-webhook-tls.key && \ + rm /tmp/secrets-webhook-tls.crt /tmp/secrets-webhook-tls.key + ''', + labels=['unkey'], + ) + + # Build unkey-env image for injection into customer pods + # Uses local_resource so it shows up in Tilt UI and can be triggered + local_resource( + 'unkey-env', + 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/unkey-env ./cmd/unkey-env && docker build -t unkey-env:latest -f Dockerfile.unkey-env .', + deps=['./cmd/unkey-env', './pkg/secrets', './pkg/cli', './Dockerfile.unkey-env'], + labels=['unkey'], + ) + + docker_build_with_restart( + 'unkey-secrets-webhook:latest', + '.', + dockerfile='Dockerfile.tilt', + entrypoint=['/unkey', 'run', 'secrets-webhook'], + only=['./bin'], + live_update=[ + sync('./bin/unkey', '/unkey'), + ], + ) + + k8s_yaml('k8s/manifests/secrets-webhook.yaml') + + secrets_webhook_deps = ['unkey-compile', 'secrets-webhook-tls', 'unkey-env'] + if start_krane: secrets_webhook_deps.append('krane') + + k8s_resource( + 'secrets-webhook', + resource_deps=secrets_webhook_deps, + labels=['unkey'], + auto_init=True, + trigger_mode=TRIGGER_MODE_AUTO + ) + # Agent service if start_agent: print("Setting up Agent service...") @@ -319,6 +374,8 @@ if start_observability: active_services.extend(['prometheus', 'otel-collector']) if start_restate: active_services.append('restate') if start_api: active_services.append('api') if start_ctrl: active_services.append('ctrl') +if start_krane: active_services.append('krane') +if start_secrets_webhook: active_services.append('secrets-webhook') if start_dashboard: active_services.append('dashboard') if start_agent: active_services.append('agent') diff --git a/go/apps/ctrl/workflows/deploy/deploy_handler.go b/go/apps/ctrl/workflows/deploy/deploy_handler.go index 53e5d926e1..575b67ddf7 100644 --- a/go/apps/ctrl/workflows/deploy/deploy_handler.go +++ b/go/apps/ctrl/workflows/deploy/deploy_handler.go @@ -31,7 +31,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) return nil, err } - // If anything goes wrong, we need to update the deployment status to failed defer func() { if finishedSuccessfully { return @@ -65,7 +64,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) var dockerImage string if req.GetBuildContextPath() != "" { - // Build Docker image from uploaded context if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusBuilding); err != nil { return nil, err } @@ -109,46 +107,40 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) return nil, fmt.Errorf("either build_context_path or docker_image must be specified") } - // Update version status to deploying if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusDeploying); err != nil { return nil, err } - // Unmarshal secrets config to get environment variables - var secretsConfig ctrlv1.SecretsConfig - if len(deployment.SecretsConfig) > 0 { + var encryptedSecretsBlob []byte + + if len(deployment.SecretsConfig) > 0 && w.vault != nil { + var secretsConfig ctrlv1.SecretsConfig if err = protojson.Unmarshal(deployment.SecretsConfig, &secretsConfig); err != nil { - return nil, fmt.Errorf("failed to unmarshal secrets config: %w", err) + return nil, fmt.Errorf("invalid secrets config: %w", err) } - } - // Decrypt environment variables using the workspace's keyring - decryptedEnvVars := make(map[string]string, len(secretsConfig.GetSecrets())) - if w.vault != nil { - for key, encryptedValue := range secretsConfig.GetSecrets() { - decrypted, decryptErr := w.vault.Decrypt(ctx, &vaultv1.DecryptRequest{ - Keyring: deployment.WorkspaceID, - Encrypted: encryptedValue, - }) - if decryptErr != nil { - return nil, fmt.Errorf("failed to decrypt env var %s: %w", key, decryptErr) - } - decryptedEnvVars[key] = decrypted.GetPlaintext() + encrypted, encryptErr := w.vault.Encrypt(ctx, &vaultv1.EncryptRequest{ + Keyring: deployment.EnvironmentID, + Data: string(deployment.SecretsConfig), + }) + if encryptErr != nil { + return nil, fmt.Errorf("failed to encrypt secrets blob: %w", encryptErr) } + encryptedSecretsBlob = []byte(encrypted.GetEncrypted()) } _, err = restate.Run(ctx, func(stepCtx restate.RunContext) (restate.Void, error) { - // Create deployment request - _, err = w.krane.CreateDeployment(stepCtx, connect.NewRequest(&kranev1.CreateDeploymentRequest{ Deployment: &kranev1.DeploymentRequest{ - Namespace: hardcodedNamespace, - DeploymentId: deployment.ID, - Image: dockerImage, - Replicas: 1, - CpuMillicores: 512, - MemorySizeMib: 512, - EnvVars: decryptedEnvVars, + Namespace: hardcodedNamespace, + DeploymentId: deployment.ID, + Image: dockerImage, + Replicas: 1, + CpuMillicores: 512, + MemorySizeMib: 512, + EnvironmentSlug: environment.Slug, + EncryptedSecretsBlob: encryptedSecretsBlob, + EnvironmentId: deployment.EnvironmentID, }, })) if err != nil { @@ -164,14 +156,11 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) w.logger.Info("deployment created", "deployment_id", deployment.ID) createdInstances, err := restate.Run(ctx, func(stepCtx restate.RunContext) ([]*kranev1.Instance, error) { - // prevent updating the db unnecessarily - - // instanceID -> status to track if an instance is already in the db and if we need to update it storedInstances := map[string]db.InstancesStatus{} for i := range 300 { time.Sleep(time.Second) - if i%10 == 0 { // Log every 10 seconds instead of every second + if i%10 == 0 { w.logger.Info("polling deployment status", "deployment_id", deployment.ID, "iteration", i) } @@ -238,8 +227,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) return resp.Msg.GetInstances(), nil } } - // next loop - } return nil, fmt.Errorf("deployment never became ready") @@ -266,7 +253,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) continue } - // Read the OpenAPI spec var specBytes []byte specBytes, err = io.ReadAll(resp.Body) if err != nil { @@ -277,7 +263,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) w.logger.Info("openapi spec scraped successfully", "host_addr", instance.GetAddress(), "deployment_id", deployment.ID, "spec_size", len(specBytes)) return base64.StdEncoding.EncodeToString(specBytes), nil } - // not an error really, just no OpenAPI spec found return "", nil }, restate.WithName("scrape openapi spec")) if err != nil { @@ -301,11 +286,9 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) deployment.GitCommitSha.String, deployment.GitBranch.String, w.defaultDomain, - ctrlv1.SourceType_SOURCE_TYPE_CLI_UPLOAD, // hardcoded for now cause I really need to move on + ctrlv1.SourceType_SOURCE_TYPE_CLI_UPLOAD, ) - // Build domain assignment requests - existingRouteIDs := make([]string, 0) for _, hostname := range allHostnames { @@ -324,7 +307,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) CreatedAt: time.Now().UnixMilli(), UpdatedAt: sql.NullInt64{Valid: false, Int64: 0}, }) - // return empty string cause this ingress is already updated since we just created it return "", err } @@ -342,7 +324,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) } } - // Call RoutingService to assign domains atomically _, err = hydrav1.NewRoutingServiceClient(ctx, project.ID). AssignIngressRoutes().Request(&hydrav1.AssignIngressRoutesRequest{ DeploymentId: deployment.ID, @@ -352,13 +333,11 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) return nil, fmt.Errorf("failed to assign domains: %w", err) } - // Update deployment status to ready if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusReady); err != nil { return nil, err } if !project.IsRolledBack && environment.Slug == "production" { - // only update this if the deployment is not rolled back _, err = restate.Run(ctx, func(stepCtx restate.RunContext) (restate.Void, error) { return restate.Void{}, db.Query.UpdateProjectDeployments(stepCtx, w.db.RW(), db.UpdateProjectDeploymentsParams{ IsRolledBack: false, @@ -372,7 +351,6 @@ func (w *Workflow) Deploy(ctx restate.ObjectContext, req *hydrav1.DeployRequest) } } - // Log deployment completed _, err = restate.Run(ctx, func(stepCtx restate.RunContext) (restate.Void, error) { return restate.Void{}, db.Query.InsertDeploymentStep(stepCtx, w.db.RW(), db.InsertDeploymentStepParams{ ProjectID: deployment.ProjectID, diff --git a/go/apps/krane/backend/docker/deployment_create.go b/go/apps/krane/backend/docker/deployment_create.go index e75bb9f611..da27eb9b9b 100644 --- a/go/apps/krane/backend/docker/deployment_create.go +++ b/go/apps/krane/backend/docker/deployment_create.go @@ -8,7 +8,10 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" + ctrlv1 "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1" kranev1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1" + vaultv1 "github.com/unkeyed/unkey/go/gen/proto/vault/v1" + "google.golang.org/protobuf/encoding/protojson" ) // CreateDeployment creates containers for a deployment with the specified replica count. @@ -49,10 +52,23 @@ func (d *docker) CreateDeployment(ctx context.Context, req *connect.Request[kran // Build environment variables list env := []string{ - fmt.Sprintf("DEPLOYMENT_ID=%s", deployment.GetDeploymentId()), + // Unkey-provided environment variables + fmt.Sprintf("UNKEY_DEPLOYMENT_ID=%s", deployment.GetDeploymentId()), + fmt.Sprintf("UNKEY_ENVIRONMENT_ID=%s", deployment.GetEnvironmentId()), + fmt.Sprintf("UNKEY_REGION=%s", d.region), + fmt.Sprintf("UNKEY_ENVIRONMENT_SLUG=%s", deployment.GetEnvironmentSlug()), + // UNKEY_INSTANCE_ID is set per-container below } - for k, v := range deployment.GetEnvVars() { - env = append(env, fmt.Sprintf("%s=%s", k, v)) + + // Decrypt and inject secrets directly as environment variables + if len(deployment.GetEncryptedSecretsBlob()) > 0 && d.vault != nil { + decryptedEnvVars, err := d.decryptSecrets(ctx, deployment.GetEnvironmentId(), deployment.GetEncryptedSecretsBlob()) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decrypt secrets: %w", err)) + } + for key, value := range decryptedEnvVars { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } } //nolint:exhaustruct // Docker SDK types have many optional fields @@ -63,7 +79,7 @@ func (d *docker) CreateDeployment(ctx context.Context, req *connect.Request[kran "unkey.managed.by": "krane", }, ExposedPorts: exposedPorts, - Env: env, + // Env is set per-instance below with UNKEY_INSTANCE_ID } //nolint:exhaustruct // Docker SDK types have many optional fields @@ -81,17 +97,29 @@ func (d *docker) CreateDeployment(ctx context.Context, req *connect.Request[kran //nolint:exhaustruct // Docker SDK types have many optional fields networkConfig := &network.NetworkingConfig{} - // Create container - + // Create containers for each replica for i := range req.Msg.GetDeployment().GetReplicas() { + instanceID := fmt.Sprintf("%s-%d", deployment.GetDeploymentId(), i) + + // Add instance-specific env var + instanceEnv := append(env, fmt.Sprintf("UNKEY_INSTANCE_ID=%s", instanceID)) //nolint:gocritic // intentional append to new slice + + //nolint:exhaustruct // Docker SDK types have many optional fields + instanceConfig := &container.Config{ + Image: containerConfig.Image, + Labels: containerConfig.Labels, + ExposedPorts: containerConfig.ExposedPorts, + Env: instanceEnv, + } + //nolint:exhaustruct // Docker SDK types have many optional fields resp, err := d.client.ContainerCreate( ctx, - containerConfig, + instanceConfig, hostConfig, networkConfig, nil, - fmt.Sprintf("%s-%d", deployment.GetDeploymentId(), i), + instanceID, ) if err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to create container: %w", err)) @@ -108,3 +136,36 @@ func (d *docker) CreateDeployment(ctx context.Context, req *connect.Request[kran Status: kranev1.DeploymentStatus_DEPLOYMENT_STATUS_PENDING, }), nil } + +// decryptSecrets decrypts an encrypted secrets blob and returns the plain env vars. +func (d *docker) decryptSecrets(ctx context.Context, environmentID string, encryptedBlob []byte) (map[string]string, error) { + // Step 1: Decrypt outer layer to get SecretsConfig JSON + decryptedBlobResp, err := d.vault.Decrypt(ctx, &vaultv1.DecryptRequest{ + Keyring: environmentID, + Encrypted: string(encryptedBlob), + }) + if err != nil { + return nil, fmt.Errorf("failed to decrypt secrets blob: %w", err) + } + + // Step 2: Parse the SecretsConfig (values inside are still encrypted) + var secretsConfig ctrlv1.SecretsConfig + if err = protojson.Unmarshal([]byte(decryptedBlobResp.GetPlaintext()), &secretsConfig); err != nil { + return nil, fmt.Errorf("failed to parse secrets config: %w", err) + } + + // Step 3: Decrypt each value individually + envVars := make(map[string]string, len(secretsConfig.GetSecrets())) + for key, encryptedValue := range secretsConfig.GetSecrets() { + decrypted, decryptErr := d.vault.Decrypt(ctx, &vaultv1.DecryptRequest{ + Keyring: environmentID, + Encrypted: encryptedValue, + }) + if decryptErr != nil { + return nil, fmt.Errorf("failed to decrypt env var %s: %w", key, decryptErr) + } + envVars[key] = decrypted.GetPlaintext() + } + + return envVars, nil +} diff --git a/go/apps/krane/backend/docker/service.go b/go/apps/krane/backend/docker/service.go index 9372831fd8..1648d8d4bd 100644 --- a/go/apps/krane/backend/docker/service.go +++ b/go/apps/krane/backend/docker/service.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/unkeyed/unkey/go/gen/proto/krane/v1/kranev1connect" "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/vault" ) // docker implements kranev1connect.DeploymentServiceHandler using Docker Engine API. @@ -22,7 +23,9 @@ import ( type docker struct { logger logging.Logger client *client.Client + vault *vault.Service registryAuth string // base64 encoded auth for pulls + region string kranev1connect.UnimplementedDeploymentServiceHandler kranev1connect.UnimplementedGatewayServiceHandler @@ -37,6 +40,8 @@ type Config struct { RegistryURL string RegistryUsername string RegistryPassword string + Region string + Vault *vault.Service } // New creates a Docker backend instance and validates daemon connectivity. @@ -71,6 +76,8 @@ func New(logger logging.Logger, cfg Config) (*docker, error) { d := &docker{ registryAuth: "", + region: cfg.Region, + vault: cfg.Vault, UnimplementedDeploymentServiceHandler: kranev1connect.UnimplementedDeploymentServiceHandler{}, UnimplementedGatewayServiceHandler: kranev1connect.UnimplementedGatewayServiceHandler{}, logger: logger, diff --git a/go/apps/krane/backend/kubernetes/deployment_create.go b/go/apps/krane/backend/kubernetes/deployment_create.go index a35ba29f6d..e0cf8457ce 100644 --- a/go/apps/krane/backend/kubernetes/deployment_create.go +++ b/go/apps/krane/backend/kubernetes/deployment_create.go @@ -2,6 +2,7 @@ package kubernetes import ( "context" + "encoding/base64" "fmt" "strings" @@ -15,57 +16,9 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) -// CreateDeployment creates a new deployment using Kubernetes StatefulSets and Services. -// -// This method implements deployment creation using a two-resource approach: -// a headless Service for DNS-based service discovery and a StatefulSet for -// managing pod replicas with stable network identities. This design choice -// supports the existing microVM abstraction where each instance requires -// predictable DNS names for database connections and gateway routing. -// -// Resource Creation Sequence: -// 1. Create headless Service (ClusterIP: None) for stable DNS resolution -// 2. Create StatefulSet with pod template and resource specifications -// 3. Set Service ownership to StatefulSet for automatic cleanup -// -// StatefulSet vs Deployment Choice: -// -// The implementation uses StatefulSets instead of standard Deployments -// because krane requires stable network identities for each replica. -// This represents a departure from typical stateless application patterns -// but aligns with the existing system architecture expectations. -// -// Note: This design decision may be reconsidered in future versions as -// it may not align with cloud-native best practices for stateless services. -// -// Resource Configuration: -// - Containers: Single container per pod with specified image and resources -// - CPU/Memory: Both requests and limits set to same values for predictable scheduling -// - Networking: Container port 8080 exposed, Service provides cluster-wide access -// - Labels: Applied for krane management tracking and resource grouping -// - Restart Policy: Always restart containers on failure -// -// Service Discovery: -// -// Each pod receives a stable DNS name following the pattern: -// {pod-name}-{index}.{service-name}.{namespace}.svc.cluster.local -// Example: myapp-0.myapp.unkey.svc.cluster.local -// -// Resource Ownership: -// -// The Service is configured as owned by the StatefulSet through Kubernetes -// owner references, ensuring automatic cleanup when the StatefulSet is deleted. -// This prevents orphaned Services from accumulating in the cluster. -// -// Error Handling: -// -// Service or StatefulSet creation failures return CodeInternal errors. -// The method ensures transactional behavior - if StatefulSet creation fails -// after Service creation, the Service remains (and will be cleaned up by -// Kubernetes garbage collection if properly configured). -// -// Returns DEPLOYMENT_STATUS_PENDING as pods may not be immediately scheduled -// and ready for traffic after creation. +// CreateDeployment creates a StatefulSet with a headless Service for stable DNS-based discovery. +// Uses StatefulSets (not Deployments) because each replica needs a predictable DNS name +// (e.g., myapp-0.myapp.unkey.svc.cluster.local) for gateway routing. func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1.CreateDeploymentRequest]) (*connect.Response[kranev1.CreateDeploymentResponse], error) { k8sDeploymentID := safeIDForK8s(req.Msg.GetDeployment().GetDeploymentId()) namespace := safeIDForK8s(req.Msg.GetDeployment().GetNamespace()) @@ -78,13 +31,6 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 service, err := k.clientset.CoreV1(). Services(namespace). Create(ctx, - // This implementation of using stateful sets is very likely not what we want to - // use in v1. - // - // It's simply what fits our existing abstraction of microVMs best, because it gives us - // stable dns addresses for each pod and that's what our database and gatway expect. - // - // I believe going forward we need to re-evaluate that cause it's the wrong abstraction. //nolint:exhaustruct &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -102,7 +48,7 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 //nolint:exhaustruct Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, // Use ClusterIP for internal communication + Type: corev1.ServiceTypeClusterIP, Selector: map[string]string{ "unkey.deployment.id": k8sDeploymentID, }, @@ -152,16 +98,16 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 Labels: map[string]string{ "unkey.deployment.id": k8sDeploymentID, "unkey.managed.by": "krane", + "unkey.com/inject": "true", + }, + Annotations: map[string]string{ + "unkey.com/deployment-id": req.Msg.GetDeployment().GetDeploymentId(), }, - Annotations: map[string]string{}, }, Spec: corev1.PodSpec{ - // Use a restricted service account with no API access ServiceAccountName: "customer-workload", - AutomountServiceAccountToken: ptr.P(false), - + AutomountServiceAccountToken: ptr.P(true), ImagePullSecrets: func() []corev1.LocalObjectReference { - // Only add imagePullSecrets if using Depot registry if strings.HasPrefix(req.Msg.GetDeployment().GetImage(), "registry.depot.dev/") { return []corev1.LocalObjectReference{ { @@ -183,17 +129,43 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 }, }, Env: func() []corev1.EnvVar { - envVars := req.Msg.GetDeployment().GetEnvVars() - if len(envVars) == 0 { - return nil + deployment := req.Msg.GetDeployment() + env := []corev1.EnvVar{ + { + Name: "UNKEY_DEPLOYMENT_ID", + Value: deployment.GetDeploymentId(), + }, + { + Name: "UNKEY_ENVIRONMENT_ID", + Value: deployment.GetEnvironmentId(), + }, + { + Name: "UNKEY_REGION", + Value: k.region, + }, + { + Name: "UNKEY_ENVIRONMENT_SLUG", + Value: deployment.GetEnvironmentSlug(), + }, + { + Name: "UNKEY_INSTANCE_ID", + //nolint:exhaustruct + ValueFrom: &corev1.EnvVarSource{ + //nolint:exhaustruct + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, } - env := make([]corev1.EnvVar, 0, len(envVars)) - for k, v := range envVars { + + if len(deployment.GetEncryptedSecretsBlob()) > 0 { env = append(env, corev1.EnvVar{ - Name: k, - Value: v, + Name: "UNKEY_SECRETS_BLOB", + Value: base64.StdEncoding.EncodeToString(deployment.GetEncryptedSecretsBlob()), }) } + return env }(), Resources: corev1.ResourceRequirements{ @@ -220,8 +192,6 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 }, }, metav1.CreateOptions{}) if err != nil { - k.logger.Info("Deleting service, because deployment creation failed") - // Delete service // nolint: exhaustruct if rollbackErr := k.clientset.CoreV1().Services(namespace).Delete(ctx, service.Name, metav1.DeleteOptions{}); rollbackErr != nil { k.logger.Error("Failed to delete service", "error", rollbackErr.Error()) @@ -231,7 +201,6 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1 } service.OwnerReferences = []metav1.OwnerReference{ - // Automatically clean up the service, when the deployment gets deleted //nolint:exhaustruct { APIVersion: "apps/v1", diff --git a/go/apps/krane/backend/kubernetes/service.go b/go/apps/krane/backend/kubernetes/service.go index ae83e5ce42..ea1ac0d554 100644 --- a/go/apps/krane/backend/kubernetes/service.go +++ b/go/apps/krane/backend/kubernetes/service.go @@ -15,6 +15,7 @@ import ( type k8s struct { logger logging.Logger clientset *kubernetes.Clientset + region string kranev1connect.UnimplementedDeploymentServiceHandler kranev1connect.UnimplementedGatewayServiceHandler } @@ -27,6 +28,9 @@ type Config struct { // Logger for Kubernetes operations. Logger logging.Logger + // Region where this krane instance is deployed. + Region string + // DeploymentEvictionTTL for automatic cleanup of old deployments. // Set to 0 to to disable automatic eviction. DeploymentEvictionTTL time.Duration @@ -53,7 +57,14 @@ func New(cfg Config) (*k8s, error) { UnimplementedGatewayServiceHandler: kranev1connect.UnimplementedGatewayServiceHandler{}, logger: cfg.Logger, clientset: clientset, + region: cfg.Region, } return k, nil } + +// GetClientset returns the Kubernetes clientset for use by other services +// such as the token validator for service account authentication. +func (k *k8s) GetClientset() kubernetes.Interface { + return k.clientset +} diff --git a/go/apps/krane/config.go b/go/apps/krane/config.go index 4777107a96..b000c47793 100644 --- a/go/apps/krane/config.go +++ b/go/apps/krane/config.go @@ -7,6 +7,14 @@ import ( "github.com/unkeyed/unkey/go/pkg/clock" ) +// S3Config holds S3 configuration for vault storage +type S3Config struct { + URL string + Bucket string + AccessKeyID string + AccessKeySecret string +} + // Backend represents the container orchestration backend type. type Backend string @@ -95,6 +103,13 @@ type Config struct { // Clock provides time operations for testing and time zone handling. // Use clock.RealClock{} for production, mock clocks for testing. Clock clock.Clock + + // VaultMasterKeys are the encryption keys for vault operations. + // Required for decrypting environment variable secrets. + VaultMasterKeys []string + + // VaultS3 configures S3 storage for encrypted vault data. + VaultS3 S3Config } func (c Config) Validate() error { diff --git a/go/apps/krane/run.go b/go/apps/krane/run.go index 3d115734c5..88741aad9c 100644 --- a/go/apps/krane/run.go +++ b/go/apps/krane/run.go @@ -7,12 +7,18 @@ import ( "net/http" "time" + k8s "k8s.io/client-go/kubernetes" + "github.com/unkeyed/unkey/go/apps/krane/backend/docker" "github.com/unkeyed/unkey/go/apps/krane/backend/kubernetes" + "github.com/unkeyed/unkey/go/apps/krane/secrets" + "github.com/unkeyed/unkey/go/apps/krane/secrets/token" "github.com/unkeyed/unkey/go/gen/proto/krane/v1/kranev1connect" "github.com/unkeyed/unkey/go/pkg/otel" "github.com/unkeyed/unkey/go/pkg/otel/logging" "github.com/unkeyed/unkey/go/pkg/shutdown" + "github.com/unkeyed/unkey/go/pkg/vault" + "github.com/unkeyed/unkey/go/pkg/vault/storage" pkgversion "github.com/unkeyed/unkey/go/pkg/version" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -63,34 +69,71 @@ func Run(ctx context.Context, cfg Config) error { logger = logger.With(slog.String("version", pkgversion.Version)) } + // Create vault service for secrets decryption + var vaultSvc *vault.Service + if len(cfg.VaultMasterKeys) > 0 && cfg.VaultS3.URL != "" { + vaultStorage, vaultStorageErr := storage.NewS3(storage.S3Config{ + Logger: logger, + S3URL: cfg.VaultS3.URL, + S3Bucket: cfg.VaultS3.Bucket, + S3AccessKeyID: cfg.VaultS3.AccessKeyID, + S3AccessKeySecret: cfg.VaultS3.AccessKeySecret, + }) + if vaultStorageErr != nil { + return fmt.Errorf("unable to create vault storage: %w", vaultStorageErr) + } + + vaultSvc, err = vault.New(vault.Config{ + Logger: logger, + Storage: vaultStorage, + MasterKeys: cfg.VaultMasterKeys, + }) + if err != nil { + return fmt.Errorf("unable to create vault service: %w", err) + } + logger.Info("Vault service initialized", "bucket", cfg.VaultS3.Bucket) + } + // Create the connect handler mux := http.NewServeMux() var svc Svc + var tokenValidator token.Validator + var k8sClientset k8s.Interface switch cfg.Backend { case Kubernetes: { - - svc, err = kubernetes.New(kubernetes.Config{ + k8sBackend, k8sErr := kubernetes.New(kubernetes.Config{ Logger: logger, + Region: cfg.Region, DeploymentEvictionTTL: cfg.DeploymentEvictionTTL, }) - if err != nil { - return fmt.Errorf("unable to init kubernetes backend: %w", err) + if k8sErr != nil { + return fmt.Errorf("unable to init kubernetes backend: %w", k8sErr) } - break + svc = k8sBackend + k8sClientset = k8sBackend.GetClientset() + + // For K8s backend, use K8s service account token validator + tokenValidator = token.NewK8sValidator(token.K8sValidatorConfig{ + Clientset: k8sClientset, + }) + logger.Info("Using K8s service account token validator") } case Docker: { - svc, err = docker.New(logger, docker.Config{ + dockerBackend, dockerErr := docker.New(logger, docker.Config{ SocketPath: cfg.DockerSocketPath, RegistryURL: cfg.RegistryURL, RegistryUsername: cfg.RegistryUsername, RegistryPassword: cfg.RegistryPassword, + Region: cfg.Region, + Vault: vaultSvc, }) - if err != nil { - return fmt.Errorf("unable to init docker backend: %w", err) + if dockerErr != nil { + return fmt.Errorf("unable to init docker backend: %w", dockerErr) } + svc = dockerBackend } default: return fmt.Errorf("unsupported backend: %s", cfg.Backend) @@ -100,6 +143,19 @@ func Run(ctx context.Context, cfg Config) error { mux.Handle(kranev1connect.NewDeploymentServiceHandler(svc)) mux.Handle(kranev1connect.NewGatewayServiceHandler(svc)) + // Register secrets service if vault is configured + if vaultSvc != nil && tokenValidator != nil { + secretsSvc := secrets.New(secrets.Config{ + Logger: logger, + Vault: vaultSvc, + TokenValidator: tokenValidator, + }) + mux.Handle(kranev1connect.NewSecretsServiceHandler(secretsSvc)) + logger.Info("Secrets service registered") + } else { + logger.Info("Secrets service not enabled (missing vault or token validator configuration)") + } + // Configure server addr := fmt.Sprintf(":%d", cfg.HttpPort) diff --git a/go/apps/krane/secrets/service.go b/go/apps/krane/secrets/service.go new file mode 100644 index 0000000000..3633a16c6d --- /dev/null +++ b/go/apps/krane/secrets/service.go @@ -0,0 +1,116 @@ +package secrets + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + ctrlv1 "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1" + kranev1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1" + "github.com/unkeyed/unkey/go/gen/proto/krane/v1/kranev1connect" + vaultv1 "github.com/unkeyed/unkey/go/gen/proto/vault/v1" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/vault" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/unkeyed/unkey/go/apps/krane/secrets/token" +) + +type Config struct { + Logger logging.Logger + Vault *vault.Service + TokenValidator token.Validator +} + +type Service struct { + kranev1connect.UnimplementedSecretsServiceHandler + logger logging.Logger + vault *vault.Service + tokenValidator token.Validator +} + +func New(cfg Config) *Service { + return &Service{ + UnimplementedSecretsServiceHandler: kranev1connect.UnimplementedSecretsServiceHandler{}, + logger: cfg.Logger, + vault: cfg.Vault, + tokenValidator: cfg.TokenValidator, + } +} + +func (s *Service) DecryptSecretsBlob( + ctx context.Context, + req *connect.Request[kranev1.DecryptSecretsBlobRequest], +) (*connect.Response[kranev1.DecryptSecretsBlobResponse], error) { + deploymentID := req.Msg.GetDeploymentId() + environmentID := req.Msg.GetEnvironmentId() + requestToken := req.Msg.GetToken() + encryptedBlob := req.Msg.GetEncryptedBlob() + + s.logger.Info("DecryptSecretsBlob request", + "deployment_id", deploymentID, + "environment_id", environmentID, + ) + + _, err := s.tokenValidator.Validate(ctx, requestToken, deploymentID) + if err != nil { + s.logger.Warn("token validation failed", + "deployment_id", deploymentID, + "error", err, + ) + return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("invalid or expired token: %w", err)) + } + + if len(encryptedBlob) == 0 { + return connect.NewResponse(&kranev1.DecryptSecretsBlobResponse{ + EnvVars: map[string]string{}, + }), nil + } + + decryptedBlobResp, err := s.vault.Decrypt(ctx, &vaultv1.DecryptRequest{ + Keyring: environmentID, + Encrypted: string(encryptedBlob), + }) + if err != nil { + s.logger.Error("failed to decrypt secrets blob", + "deployment_id", deploymentID, + "error", err, + ) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decrypt secrets blob")) + } + + var secretsConfig ctrlv1.SecretsConfig + if err = protojson.Unmarshal([]byte(decryptedBlobResp.GetPlaintext()), &secretsConfig); err != nil { + s.logger.Error("failed to unmarshal secrets config", + "deployment_id", deploymentID, + "error", err, + ) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to parse secrets config")) + } + + envVars := make(map[string]string, len(secretsConfig.GetSecrets())) + for key, encryptedValue := range secretsConfig.GetSecrets() { + decrypted, decryptErr := s.vault.Decrypt(ctx, &vaultv1.DecryptRequest{ + Keyring: environmentID, + Encrypted: encryptedValue, + }) + if decryptErr != nil { + s.logger.Error("failed to decrypt env var", + "deployment_id", deploymentID, + "key", key, + "error", decryptErr, + ) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to decrypt env var %s", key)) + } + envVars[key] = decrypted.GetPlaintext() + } + + s.logger.Info("decrypted secrets blob", + "deployment_id", deploymentID, + "num_secrets", len(envVars), + ) + + return connect.NewResponse(&kranev1.DecryptSecretsBlobResponse{ + EnvVars: envVars, + }), nil +} diff --git a/go/apps/krane/secrets/token/k8s_validator.go b/go/apps/krane/secrets/token/k8s_validator.go new file mode 100644 index 0000000000..c4ea99734f --- /dev/null +++ b/go/apps/krane/secrets/token/k8s_validator.go @@ -0,0 +1,75 @@ +package token + +import ( + "context" + "fmt" + "strings" + + authv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type K8sValidatorConfig struct { + Clientset kubernetes.Interface +} + +type K8sValidator struct { + clientset kubernetes.Interface +} + +func NewK8sValidator(cfg K8sValidatorConfig) *K8sValidator { + return &K8sValidator{clientset: cfg.Clientset} +} + +func (v *K8sValidator) Validate(ctx context.Context, token string, deploymentID string) (*ValidationResult, error) { + //nolint:exhaustruct // k8s API types have many optional fields + tokenReview := &authv1.TokenReview{ + Spec: authv1.TokenReviewSpec{Token: token}, + } + + result, err := v.clientset.AuthenticationV1().TokenReviews().Create(ctx, tokenReview, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to validate token via TokenReview: %w", err) + } + + if !result.Status.Authenticated { + return nil, fmt.Errorf("token not authenticated: %s", result.Status.Error) + } + + username := result.Status.User.Username + podName := "" + + if podNames, ok := result.Status.User.Extra["authentication.kubernetes.io/pod-name"]; ok && len(podNames) > 0 { + podName = podNames[0] + } + + if podName == "" { + return nil, fmt.Errorf("could not determine pod name from token (username: %s)", username) + } + + parts := strings.Split(username, ":") + if len(parts) != 4 { + return nil, fmt.Errorf("invalid service account username format (expected 4 parts): %s", username) + } + if parts[0] != "system" || parts[1] != "serviceaccount" { + return nil, fmt.Errorf("username is not a service account (expected system:serviceaccount:...): %s", username) + } + tokenNamespace := parts[2] + + pod, err := v.clientset.CoreV1().Pods(tokenNamespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get pod %s: %w", podName, err) + } + + podDeploymentID := pod.Annotations["unkey.com/deployment-id"] + if podDeploymentID == "" { + return nil, fmt.Errorf("pod %s missing unkey.com/deployment-id annotation", podName) + } + + if podDeploymentID != deploymentID { + return nil, fmt.Errorf("pod %s belongs to deployment %s, not %s", podName, podDeploymentID, deploymentID) + } + + return &ValidationResult{DeploymentID: deploymentID}, nil +} diff --git a/go/apps/krane/secrets/token/validator.go b/go/apps/krane/secrets/token/validator.go new file mode 100644 index 0000000000..5d98027f66 --- /dev/null +++ b/go/apps/krane/secrets/token/validator.go @@ -0,0 +1,11 @@ +package token + +import "context" + +type ValidationResult struct { + DeploymentID string +} + +type Validator interface { + Validate(ctx context.Context, token string, deploymentID string) (*ValidationResult, error) +} diff --git a/go/apps/secrets-webhook/config.go b/go/apps/secrets-webhook/config.go new file mode 100644 index 0000000000..bf6f1afc92 --- /dev/null +++ b/go/apps/secrets-webhook/config.go @@ -0,0 +1,29 @@ +package webhook + +import "github.com/unkeyed/unkey/go/pkg/assert" + +type Config struct { + HttpPort int + TLSCertFile string + TLSKeyFile string + UnkeyEnvImage string + UnkeyEnvImagePullPolicy string + KraneEndpoint string + AnnotationPrefix string +} + +func (c *Config) Validate() error { + if c.HttpPort == 0 { + c.HttpPort = 8443 + } + if c.AnnotationPrefix == "" { + c.AnnotationPrefix = "unkey.com" + } + + return assert.All( + assert.NotEmpty(c.TLSCertFile, "tls-cert-file is required"), + assert.NotEmpty(c.TLSKeyFile, "tls-key-file is required"), + assert.NotEmpty(c.UnkeyEnvImage, "unkey-env-image is required"), + assert.NotEmpty(c.KraneEndpoint, "krane-endpoint is required"), + ) +} diff --git a/go/apps/secrets-webhook/internal/services/mutator/config.go b/go/apps/secrets-webhook/internal/services/mutator/config.go new file mode 100644 index 0000000000..3af715aee9 --- /dev/null +++ b/go/apps/secrets-webhook/internal/services/mutator/config.go @@ -0,0 +1,52 @@ +package mutator + +import "fmt" + +const ( + unkeyEnvVolumeName = "unkey-env-bin" + unkeyEnvMountPath = "/unkey" + unkeyEnvBinary = "/unkey/unkey-env" + //nolint:gosec // G101: This is a file path, not credentials + ServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" +) + +const ( + AnnotationDeploymentID = "deployment-id" + AnnotationProviderEndpoint = "provider-endpoint" +) + +type Config struct { + UnkeyEnvImage string + UnkeyEnvImagePullPolicy string + AnnotationPrefix string + DefaultProviderEndpoint string +} + +func (c *Config) GetAnnotation(suffix string) string { + return fmt.Sprintf("%s/%s", c.AnnotationPrefix, suffix) +} + +type podConfig struct { + DeploymentID string + ProviderEndpoint string +} + +func (m *Mutator) loadPodConfig(annotations map[string]string) (*podConfig, error) { + cfg := &podConfig{ + DeploymentID: "", + ProviderEndpoint: "", + } + + cfg.DeploymentID = annotations[m.cfg.GetAnnotation(AnnotationDeploymentID)] + if cfg.DeploymentID == "" { + return nil, fmt.Errorf("missing required annotation: %s", m.cfg.GetAnnotation(AnnotationDeploymentID)) + } + + if val, ok := annotations[m.cfg.GetAnnotation(AnnotationProviderEndpoint)]; ok && val != "" { + cfg.ProviderEndpoint = val + } else { + cfg.ProviderEndpoint = m.cfg.DefaultProviderEndpoint + } + + return cfg, nil +} diff --git a/go/apps/secrets-webhook/internal/services/mutator/mutator.go b/go/apps/secrets-webhook/internal/services/mutator/mutator.go new file mode 100644 index 0000000000..2a3ecd96a8 --- /dev/null +++ b/go/apps/secrets-webhook/internal/services/mutator/mutator.go @@ -0,0 +1,109 @@ +package mutator + +import ( + "context" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + + "github.com/unkeyed/unkey/go/apps/secrets-webhook/internal/services/registry" + "github.com/unkeyed/unkey/go/pkg/otel/logging" +) + +type Mutator struct { + cfg *Config + logger logging.Logger + registry *registry.Registry +} + +func New(cfg *Config, logger logging.Logger, reg *registry.Registry) *Mutator { + return &Mutator{ + cfg: cfg, + logger: logger, + registry: reg, + } +} + +type Result struct { + Mutated bool + Patch []byte + Message string +} + +func (m *Mutator) ShouldMutate(pod *corev1.Pod) bool { + annotations := pod.GetAnnotations() + if annotations == nil { + return false + } + return annotations[m.cfg.GetAnnotation(AnnotationDeploymentID)] != "" +} + +func (m *Mutator) Mutate(ctx context.Context, pod *corev1.Pod, namespace string) (*Result, error) { + if !m.ShouldMutate(pod) { + return &Result{Mutated: false, Patch: nil, Message: "pod not annotated for injection"}, nil + } + + annotations := pod.GetAnnotations() + + podCfg, err := m.loadPodConfig(annotations) + if err != nil { + return nil, err + } + + m.logger.Info("loaded pod config from annotations", + "deployment_id", podCfg.DeploymentID, + "provider_endpoint", podCfg.ProviderEndpoint, + ) + + var patches []map[string]interface{} + + initContainer := m.buildInitContainer() + if len(pod.Spec.InitContainers) == 0 { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/spec/initContainers", + "value": []corev1.Container{initContainer}, + }) + } else { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/spec/initContainers/-", + "value": initContainer, + }) + } + + volume := m.buildVolume() + if len(pod.Spec.Volumes) == 0 { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/spec/volumes", + "value": []corev1.Volume{volume}, + }) + } else { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/spec/volumes/-", + "value": volume, + }) + } + + for i, container := range pod.Spec.Containers { + containerPatches, patchErr := m.buildContainerPatches(ctx, i, &container, &pod.Spec, namespace, podCfg) + if patchErr != nil { + return nil, fmt.Errorf("failed to build patches for container %s: %w", container.Name, patchErr) + } + patches = append(patches, containerPatches...) + } + + patchBytes, err := json.Marshal(patches) + if err != nil { + return nil, fmt.Errorf("failed to marshal patches: %w", err) + } + + return &Result{ + Mutated: true, + Patch: patchBytes, + Message: fmt.Sprintf("injected unkey-env for deployment %s", podCfg.DeploymentID), + }, nil +} diff --git a/go/apps/secrets-webhook/internal/services/mutator/patch.go b/go/apps/secrets-webhook/internal/services/mutator/patch.go new file mode 100644 index 0000000000..eec1224f07 --- /dev/null +++ b/go/apps/secrets-webhook/internal/services/mutator/patch.go @@ -0,0 +1,137 @@ +package mutator + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" +) + +func (m *Mutator) buildInitContainer() corev1.Container { + pullPolicy := corev1.PullIfNotPresent + if m.cfg.UnkeyEnvImagePullPolicy != "" { + pullPolicy = corev1.PullPolicy(m.cfg.UnkeyEnvImagePullPolicy) + } + + return corev1.Container{ + Name: "copy-unkey-env", + Image: m.cfg.UnkeyEnvImage, + ImagePullPolicy: pullPolicy, + Command: []string{"cp", "/unkey-env", unkeyEnvBinary}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: unkeyEnvVolumeName, + MountPath: unkeyEnvMountPath, + }, + }, + } +} + +func (m *Mutator) buildVolume() corev1.Volume { + return corev1.Volume{ + Name: unkeyEnvVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }, + }, + } +} + +func (m *Mutator) buildContainerPatches( + ctx context.Context, + containerIndex int, + container *corev1.Container, + podSpec *corev1.PodSpec, + namespace string, + podCfg *podConfig, +) ([]map[string]interface{}, error) { + var patches []map[string]interface{} + basePath := fmt.Sprintf("/spec/containers/%d", containerIndex) + + volumeMount := corev1.VolumeMount{ + Name: unkeyEnvVolumeName, + MountPath: unkeyEnvMountPath, + ReadOnly: true, + } + + if len(container.VolumeMounts) == 0 { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/volumeMounts", basePath), + "value": []corev1.VolumeMount{volumeMount}, + }) + } else { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/volumeMounts/-", basePath), + "value": volumeMount, + }) + } + + envVars := m.buildEnvVars(podCfg) + if len(container.Env) == 0 { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/env", basePath), + "value": envVars, + }) + } else { + for _, env := range envVars { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/env/-", basePath), + "value": env, + }) + } + } + + // We replace the container's command with unkey-env, which decrypts secrets and then + // exec's the original entrypoint. If the pod spec doesn't define a command, we need to + // fetch the image's ENTRYPOINT/CMD from the registry so we know what to exec into. + var args []string + if len(container.Command) == 0 { + m.logger.Info("container has no command, fetching from registry", + "container", container.Name, + "image", container.Image, + ) + + imageConfig, err := m.registry.GetImageConfig(ctx, namespace, container, podSpec) + if err != nil { + return nil, fmt.Errorf("failed to get image config for %s: %w", container.Image, err) + } + + args = append(args, imageConfig.Entrypoint...) + if len(container.Args) == 0 { + args = append(args, imageConfig.Cmd...) + } + } else { + args = append(args, container.Command...) + } + + args = append(args, container.Args...) + + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/command", basePath), + "value": []string{unkeyEnvBinary}, + }) + + if len(args) > 0 { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": fmt.Sprintf("%s/args", basePath), + "value": args, + }) + } + + return patches, nil +} + +func (m *Mutator) buildEnvVars(podCfg *podConfig) []corev1.EnvVar { + return []corev1.EnvVar{ + {Name: "UNKEY_PROVIDER_ENDPOINT", Value: podCfg.ProviderEndpoint}, + {Name: "UNKEY_DEPLOYMENT_ID", Value: podCfg.DeploymentID}, + {Name: "UNKEY_TOKEN_PATH", Value: ServiceAccountTokenPath}, + } +} diff --git a/go/apps/secrets-webhook/internal/services/registry/registry.go b/go/apps/secrets-webhook/internal/services/registry/registry.go new file mode 100644 index 0000000000..6b41146b6f --- /dev/null +++ b/go/apps/secrets-webhook/internal/services/registry/registry.go @@ -0,0 +1,152 @@ +package registry + +import ( + "context" + "fmt" + "runtime" + + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + + "github.com/unkeyed/unkey/go/pkg/otel/logging" +) + +const defaultOS = "linux" + +type ImageConfig struct { + Entrypoint []string + Cmd []string +} + +type Registry struct { + logger logging.Logger + clientset kubernetes.Interface +} + +func New(logger logging.Logger, clientset kubernetes.Interface) *Registry { + return &Registry{ + logger: logger, + clientset: clientset, + } +} + +func (r *Registry) GetImageConfig( + ctx context.Context, + namespace string, + container *corev1.Container, + podSpec *corev1.PodSpec, +) (*ImageConfig, error) { + //nolint:exhaustruct // k8schain has many optional fields + chainOpts := k8schain.Options{ + Namespace: namespace, + ServiceAccountName: podSpec.ServiceAccountName, + } + for _, secret := range podSpec.ImagePullSecrets { + chainOpts.ImagePullSecrets = append(chainOpts.ImagePullSecrets, secret.Name) + } + + authChain, err := k8schain.New(ctx, r.clientset, chainOpts) + if err != nil { + return nil, fmt.Errorf("failed to create k8s auth chain: %w", err) + } + + ref, err := name.ParseReference(container.Image) + if err != nil { + return nil, fmt.Errorf("failed to parse image reference %q: %w", container.Image, err) + } + + options := []remote.Option{ + remote.WithAuthFromKeychain(authChain), + remote.WithContext(ctx), + } + + descriptor, err := remote.Get(ref, options...) + if err != nil { + return nil, fmt.Errorf("failed to fetch image descriptor for %q: %w", container.Image, err) + } + + var image v1.Image + if descriptor.MediaType.IsIndex() { + index, indexErr := descriptor.ImageIndex() + if indexErr != nil { + return nil, fmt.Errorf("failed to get image index: %w", indexErr) + } + + manifest, manifestErr := index.IndexManifest() + if manifestErr != nil { + return nil, fmt.Errorf("failed to get index manifest: %w", manifestErr) + } + + if len(manifest.Manifests) == 0 { + return nil, fmt.Errorf("no manifests found in image index for %q", container.Image) + } + + digest, found := r.findPlatformManifest(manifest.Manifests) + if !found { + r.logger.Warn("no matching platform found in image index, using first manifest", + "image", container.Image, + "wanted_os", targetOS(), + "wanted_arch", targetArch(), + ) + digest = manifest.Manifests[0].Digest + } + + image, err = index.Image(digest) + if err != nil { + return nil, fmt.Errorf("failed to get image from manifest: %w", err) + } + } else { + image, err = descriptor.Image() + if err != nil { + return nil, fmt.Errorf("failed to convert descriptor to image: %w", err) + } + } + + configFile, err := image.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get image config: %w", err) + } + + r.logger.Debug("fetched image config", + "image", container.Image, + "entrypoint", configFile.Config.Entrypoint, + "cmd", configFile.Config.Cmd, + ) + + return &ImageConfig{ + Entrypoint: configFile.Config.Entrypoint, + Cmd: configFile.Config.Cmd, + }, nil +} + +func (r *Registry) findPlatformManifest(manifests []v1.Descriptor) (v1.Hash, bool) { + wantOS := targetOS() + wantArch := targetArch() + + for _, m := range manifests { + if m.Platform == nil { + continue + } + if m.Platform.OS == wantOS && m.Platform.Architecture == wantArch { + return m.Digest, true + } + } + return v1.Hash{}, false //nolint:exhaustruct // zero value for not-found case +} + +// targetOS returns the OS to look for in multi-arch image manifests. +// Containers always run linux, even when the webhook is built/tested on darwin. +func targetOS() string { + return defaultOS +} + +// targetArch returns the architecture to look for in multi-arch image manifests. +// We assume the webhook runs on the same architecture as the workloads it mutates. +// If this assumption changes, adjust this function accordingly. +func targetArch() string { + return runtime.GOARCH +} diff --git a/go/apps/secrets-webhook/routes/healthz/handler.go b/go/apps/secrets-webhook/routes/healthz/handler.go new file mode 100644 index 0000000000..5a798cc4ef --- /dev/null +++ b/go/apps/secrets-webhook/routes/healthz/handler.go @@ -0,0 +1,22 @@ +package healthz + +import ( + "context" + "net/http" + + "github.com/unkeyed/unkey/go/pkg/zen" +) + +type Handler struct{} + +func (h *Handler) Method() string { + return "GET" +} + +func (h *Handler) Path() string { + return "/healthz" +} + +func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { + return s.Plain(http.StatusOK, []byte("ok")) +} diff --git a/go/apps/secrets-webhook/routes/mutate/handler.go b/go/apps/secrets-webhook/routes/mutate/handler.go new file mode 100644 index 0000000000..d2ee7353e4 --- /dev/null +++ b/go/apps/secrets-webhook/routes/mutate/handler.go @@ -0,0 +1,96 @@ +package mutate + +import ( + "context" + "encoding/json" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/unkeyed/unkey/go/apps/secrets-webhook/internal/services/mutator" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +type Handler struct { + Logger logging.Logger + Mutator *mutator.Mutator +} + +func (h *Handler) Method() string { return "POST" } +func (h *Handler) Path() string { return "/mutate" } + +func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { + admissionReview, err := zen.BindBody[admissionv1.AdmissionReview](s) + if err != nil { + h.Logger.Error("failed to parse admission review", "error", err) + return s.JSON(http.StatusBadRequest, map[string]string{"error": "failed to parse admission review"}) + } + + if admissionReview.Request == nil { + h.Logger.Error("missing admission request") + return h.sendResponse(s, "", false, "missing admission request") + } + + var pod corev1.Pod + if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil { + h.Logger.Error("failed to parse pod", "error", err) + return h.sendResponse(s, admissionReview.Request.UID, false, "failed to parse pod") + } + + namespace := admissionReview.Request.Namespace + + h.Logger.Info("received admission request", + "pod", pod.Name, + "namespace", namespace, + "uid", admissionReview.Request.UID, + ) + + result, err := h.Mutator.Mutate(ctx, &pod, namespace) + if err != nil { + h.Logger.Error("failed to mutate pod", "pod", pod.Name, "namespace", namespace, "error", err) + return h.sendResponse(s, admissionReview.Request.UID, false, err.Error()) + } + + if result.Mutated { + h.Logger.Info("mutated pod", "pod", pod.Name, "namespace", pod.Namespace, "message", result.Message) + return h.sendResponseWithPatch(s, admissionReview.Request.UID, result.Patch) + } + + h.Logger.Info("skipped pod mutation", "pod", pod.Name, "namespace", pod.Namespace, "message", result.Message) + return h.sendResponse(s, admissionReview.Request.UID, true, result.Message) +} + +func (h *Handler) sendResponse(s *zen.Session, uid types.UID, allowed bool, message string) error { + //nolint:exhaustruct // k8s admission API types have many optional fields + response := admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, + Response: &admissionv1.AdmissionResponse{UID: uid, Allowed: allowed}, + } + + if message != "" && !allowed { + response.Response.Result = &metav1.Status{Message: message} + } + + return s.JSON(http.StatusOK, response) +} + +func (h *Handler) sendResponseWithPatch(s *zen.Session, uid types.UID, patch []byte) error { + patchType := admissionv1.PatchTypeJSONPatch + + //nolint:exhaustruct // k8s admission API types have many optional fields + response := admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, + Response: &admissionv1.AdmissionResponse{ + UID: uid, + Allowed: true, + Patch: patch, + PatchType: &patchType, + }, + } + + return s.JSON(http.StatusOK, response) +} diff --git a/go/apps/secrets-webhook/run.go b/go/apps/secrets-webhook/run.go new file mode 100644 index 0000000000..248435e1e2 --- /dev/null +++ b/go/apps/secrets-webhook/run.go @@ -0,0 +1,80 @@ +package webhook + +import ( + "context" + "fmt" + "log/slog" + "net" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + "github.com/unkeyed/unkey/go/apps/secrets-webhook/internal/services/mutator" + "github.com/unkeyed/unkey/go/apps/secrets-webhook/internal/services/registry" + "github.com/unkeyed/unkey/go/apps/secrets-webhook/routes/healthz" + "github.com/unkeyed/unkey/go/apps/secrets-webhook/routes/mutate" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/tls" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +func Run(ctx context.Context, cfg Config) error { + if err := cfg.Validate(); err != nil { + return fmt.Errorf("bad config: %w", err) + } + + logger := logging.New().With(slog.String("service", "secrets-webhook")) + + inClusterConfig, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to create in-cluster config: %w", err) + } + + clientset, err := kubernetes.NewForConfig(inClusterConfig) + if err != nil { + return fmt.Errorf("failed to create Kubernetes clientset: %w", err) + } + + reg := registry.New(logger, clientset) + + tlsConfig, err := tls.NewFromFiles(cfg.TLSCertFile, cfg.TLSKeyFile) + if err != nil { + return fmt.Errorf("failed to load TLS certificates: %w", err) + } + + //nolint:exhaustruct // zen.Config has many optional fields with sensible defaults + server, err := zen.New(zen.Config{ + Logger: logger, + TLS: tlsConfig, + }) + if err != nil { + return fmt.Errorf("failed to create server: %w", err) + } + + m := mutator.New(&mutator.Config{ + UnkeyEnvImage: cfg.UnkeyEnvImage, + UnkeyEnvImagePullPolicy: cfg.UnkeyEnvImagePullPolicy, + AnnotationPrefix: cfg.AnnotationPrefix, + DefaultProviderEndpoint: cfg.KraneEndpoint, + }, logger, reg) + + middlewares := []zen.Middleware{ + zen.WithPanicRecovery(logger), + zen.WithLogging(logger), + } + + server.RegisterRoute(middlewares, &healthz.Handler{}) + server.RegisterRoute(middlewares, &mutate.Handler{ + Logger: logger, + Mutator: m, + }) + + addr := fmt.Sprintf(":%d", cfg.HttpPort) + ln, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", addr, err) + } + + logger.Info("starting secrets webhook server", "addr", addr) + return server.Serve(ctx, ln) +} diff --git a/go/cmd/ctrl/main.go b/go/cmd/ctrl/main.go index be9be1f347..8b47d7cb74 100644 --- a/go/cmd/ctrl/main.go +++ b/go/cmd/ctrl/main.go @@ -186,6 +186,7 @@ func action(ctx context.Context, cmd *cli.Command) error { Bucket: cmd.String("vault-s3-bucket"), AccessKeyID: cmd.String("vault-s3-access-key-id"), AccessKeySecret: cmd.String("vault-s3-access-key-secret"), + ExternalURL: "", }, // ACME Vault configuration - Let's Encrypt certificates AcmeVaultMasterKeys: cmd.StringSlice("acme-vault-master-keys"), @@ -194,6 +195,7 @@ func action(ctx context.Context, cmd *cli.Command) error { Bucket: cmd.String("acme-vault-s3-bucket"), AccessKeyID: cmd.String("acme-vault-s3-access-key-id"), AccessKeySecret: cmd.String("acme-vault-s3-access-key-secret"), + ExternalURL: "", }, // Build configuration diff --git a/go/cmd/dev/seed/ingress.go b/go/cmd/dev/seed/ingress.go index fab6d4d42b..13eab7c202 100644 --- a/go/cmd/dev/seed/ingress.go +++ b/go/cmd/dev/seed/ingress.go @@ -112,6 +112,7 @@ func seedIngress(ctx context.Context, cmd *cli.Command) error { GitCommitAuthorAvatarUrl: sql.NullString{}, GitCommitTimestamp: sql.NullInt64{Int64: now, Valid: true}, OpenapiSpec: sql.NullString{}, + SecretsConfig: nil, Status: db.DeploymentsStatusReady, CreatedAt: now, UpdatedAt: sql.NullInt64{}, diff --git a/go/cmd/healthcheck/main.go b/go/cmd/healthcheck/main.go index 41edf624b4..45de2181c3 100644 --- a/go/cmd/healthcheck/main.go +++ b/go/cmd/healthcheck/main.go @@ -9,12 +9,13 @@ import ( ) var Cmd = &cli.Command{ - Aliases: []string{}, - Version: "", - Commands: []*cli.Command{}, - Flags: []cli.Flag{}, - Name: "healthcheck", - Usage: "Perform an HTTP healthcheck against a given URL", + Aliases: []string{}, + Version: "", + Commands: []*cli.Command{}, + Flags: []cli.Flag{}, + AcceptsArgs: true, + Name: "healthcheck", + Usage: "Perform an HTTP healthcheck against a given URL", Description: `This command sends an HTTP GET request to the specified URL and validates the response. It exits with code 0 if the server returns a 200 status code, otherwise exits with code 1. USE CASES: diff --git a/go/cmd/krane/main.go b/go/cmd/krane/main.go index 8f84f9f7fb..b9b9ae0d17 100644 --- a/go/cmd/krane/main.go +++ b/go/cmd/krane/main.go @@ -46,6 +46,18 @@ unkey run krane # Run with default configurati // This has no use outside of our demo cluster and will be removed soon cli.Duration("deployment-eviction-ttl", "Automatically delete deployments after some time. Use go duration formats such as 2h30m", cli.EnvVar("UNKEY_DEPLOYMENT_EVICTION_TTL")), + + // Vault Configuration + cli.StringSlice("vault-master-keys", "Master keys for vault encryption (base64 encoded)", + cli.EnvVar("UNKEY_VAULT_MASTER_KEYS")), + cli.String("vault-s3-url", "S3 URL for vault storage", + cli.EnvVar("UNKEY_VAULT_S3_URL")), + cli.String("vault-s3-bucket", "S3 bucket for vault storage", + cli.EnvVar("UNKEY_VAULT_S3_BUCKET")), + cli.String("vault-s3-access-key-id", "S3 access key ID for vault storage", + cli.EnvVar("UNKEY_VAULT_S3_ACCESS_KEY_ID")), + cli.String("vault-s3-access-key-secret", "S3 access key secret for vault storage", + cli.EnvVar("UNKEY_VAULT_S3_ACCESS_KEY_SECRET")), }, Action: action, } @@ -71,6 +83,13 @@ func action(ctx context.Context, cmd *cli.Command) error { RegistryUsername: cmd.String("registry-username"), RegistryPassword: cmd.String("registry-password"), DeploymentEvictionTTL: cmd.Duration("deployment-eviction-ttl"), + VaultMasterKeys: cmd.StringSlice("vault-master-keys"), + VaultS3: krane.S3Config{ + URL: cmd.String("vault-s3-url"), + Bucket: cmd.String("vault-s3-bucket"), + AccessKeyID: cmd.String("vault-s3-access-key-id"), + AccessKeySecret: cmd.String("vault-s3-access-key-secret"), + }, } // Validate configuration diff --git a/go/cmd/run/main.go b/go/cmd/run/main.go index 4343ad452a..b160623475 100644 --- a/go/cmd/run/main.go +++ b/go/cmd/run/main.go @@ -9,6 +9,7 @@ import ( "github.com/unkeyed/unkey/go/cmd/gateway" "github.com/unkeyed/unkey/go/cmd/ingress" "github.com/unkeyed/unkey/go/cmd/krane" + secretswebhook "github.com/unkeyed/unkey/go/cmd/secrets-webhook" "github.com/unkeyed/unkey/go/pkg/cli" ) @@ -28,6 +29,7 @@ AVAILABLE SERVICES: - krane: The VM management service for infrastructure - ingress: Multi-tenant ingress service for TLS termination and routing - gateway: Environment tenant gateway service for routing requests to the actual instances +- secrets-webhook: Kubernetes mutating webhook for secrets injection EXAMPLES: unkey run api # Run the API server @@ -42,17 +44,19 @@ unkey run api --port 8080 --env production # Run API server with custom con krane.Cmd, ingress.Cmd, gateway.Cmd, + secretswebhook.Cmd, }, Action: runAction, } func runAction(ctx context.Context, cmd *cli.Command) error { fmt.Println("Available services:") - fmt.Println(" api - The main API server for validating and managing API keys") - fmt.Println(" ctrl - The control plane service for managing infrastructure") - fmt.Println(" krane - Manage containers and deployments in docker or kubernetes") - fmt.Println(" ingress - Multi-tenant ingress service for TLS termination and routing") - fmt.Println(" gateway - Environment tenant gateway service for routing requests to the actual instances") + fmt.Println(" api - The main API server for validating and managing API keys") + fmt.Println(" ctrl - The control plane service for managing infrastructure") + fmt.Println(" krane - Manage containers and deployments in docker or kubernetes") + fmt.Println(" ingress - Multi-tenant ingress service for TLS termination and routing") + fmt.Println(" gateway - Environment tenant gateway service for routing requests to the actual instances") + fmt.Println(" secrets-webhook - Kubernetes mutating webhook for secrets injection") fmt.Println() fmt.Println("Use 'unkey run ' to start a specific service") fmt.Println("Use 'unkey run --help' for service-specific options") diff --git a/go/cmd/secrets-webhook/main.go b/go/cmd/secrets-webhook/main.go new file mode 100644 index 0000000000..8590a3b919 --- /dev/null +++ b/go/cmd/secrets-webhook/main.go @@ -0,0 +1,48 @@ +package secretswebhook + +import ( + "context" + + webhook "github.com/unkeyed/unkey/go/apps/secrets-webhook" + "github.com/unkeyed/unkey/go/pkg/cli" +) + +var Cmd = &cli.Command{ + Name: "secrets-webhook", + Usage: "Run the secrets injection webhook", + Flags: []cli.Flag{ + cli.Int("port", "Port for the webhook server", + cli.Default(8443), cli.EnvVar("WEBHOOK_PORT")), + cli.String("tls-cert-file", "Path to TLS certificate file", + cli.Required(), cli.EnvVar("WEBHOOK_TLS_CERT_FILE")), + cli.String("tls-key-file", "Path to TLS private key file", + cli.Required(), cli.EnvVar("WEBHOOK_TLS_KEY_FILE")), + cli.String("unkey-env-image", "Container image for unkey-env binary", + cli.Default("unkey-env:latest"), cli.EnvVar("UNKEY_ENV_IMAGE")), + cli.String("unkey-env-image-pull-policy", "Image pull policy (Always, IfNotPresent, Never)", + cli.Default("IfNotPresent"), cli.EnvVar("UNKEY_ENV_IMAGE_PULL_POLICY")), + cli.String("krane-endpoint", "Endpoint for Krane secrets service", + cli.Default("http://krane.unkey.svc.cluster.local:8080"), cli.EnvVar("KRANE_ENDPOINT")), + cli.String("annotation-prefix", "Annotation prefix for pod configuration", + cli.Default("unkey.com"), cli.EnvVar("ANNOTATION_PREFIX")), + }, + Action: action, +} + +func action(ctx context.Context, cmd *cli.Command) error { + config := webhook.Config{ + HttpPort: cmd.Int("port"), + TLSCertFile: cmd.String("tls-cert-file"), + TLSKeyFile: cmd.String("tls-key-file"), + UnkeyEnvImage: cmd.String("unkey-env-image"), + UnkeyEnvImagePullPolicy: cmd.String("unkey-env-image-pull-policy"), + KraneEndpoint: cmd.String("krane-endpoint"), + AnnotationPrefix: cmd.String("annotation-prefix"), + } + + if err := config.Validate(); err != nil { + return cli.Exit("Invalid configuration: "+err.Error(), 1) + } + + return webhook.Run(ctx, config) +} diff --git a/go/cmd/unkey-env/Dockerfile b/go/cmd/unkey-env/Dockerfile new file mode 100644 index 0000000000..543eda1be3 --- /dev/null +++ b/go/cmd/unkey-env/Dockerfile @@ -0,0 +1,23 @@ +# Build stage +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /unkey-env ./cmd/unkey-env + +# Final stage - scratch image with just the binary +FROM scratch + +# Copy the binary +COPY --from=builder /unkey-env /unkey-env + +# The binary will be copied to pods by the init container +ENTRYPOINT ["/unkey-env"] diff --git a/go/cmd/unkey-env/main.go b/go/cmd/unkey-env/main.go new file mode 100644 index 0000000000..9563c3b111 --- /dev/null +++ b/go/cmd/unkey-env/main.go @@ -0,0 +1,176 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "log/slog" + "os" + "os/exec" + "strings" + "syscall" + "time" + + "github.com/unkeyed/unkey/go/pkg/assert" + "github.com/unkeyed/unkey/go/pkg/cli" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/secrets/provider" +) + +var allowedUnkeyVars = map[string]bool{ + "UNKEY_DEPLOYMENT_ID": true, + "UNKEY_ENVIRONMENT_ID": true, + "UNKEY_REGION": true, + "UNKEY_INSTANCE_ID": true, +} + +type Config struct { + Provider provider.Type + Endpoint string + DeploymentID string + EnvironmentID string + EncryptedBlob string + Token string + TokenPath string + Debug bool + Args []string +} + +func (c *Config) Validate() error { + return assert.All( + assert.NotEmpty(c.Endpoint, "endpoint is required"), + assert.NotEmpty(c.DeploymentID, "deployment-id is required"), + assert.True(c.Token != "" || c.TokenPath != "", "either token or token-path is required"), + assert.True(len(c.Args) > 0, "command is required"), + ) +} + +var Cmd = &cli.Command{ + Name: "unkey-env", + Usage: "Fetch secrets and exec the given command", + AcceptsArgs: true, + Flags: []cli.Flag{ + cli.String("provider", "Secrets provider type", + cli.Default(string(provider.KraneVault)), + cli.EnvVar("UNKEY_PROVIDER")), + cli.String("endpoint", "Provider API endpoint", + cli.Required(), + cli.EnvVar("UNKEY_PROVIDER_ENDPOINT")), + cli.String("deployment-id", "Deployment ID", + cli.Required(), + cli.EnvVar("UNKEY_DEPLOYMENT_ID")), + cli.String("environment-id", "Environment ID for decryption", + cli.EnvVar("UNKEY_ENVIRONMENT_ID")), + cli.String("secrets-blob", "Base64-encoded encrypted secrets blob", + cli.EnvVar("UNKEY_SECRETS_BLOB")), + cli.String("token", "Authentication token", + cli.EnvVar("UNKEY_TOKEN")), + cli.String("token-path", "Path to token file", + cli.EnvVar("UNKEY_TOKEN_PATH")), + cli.Bool("debug", "Enable debug logging", + cli.EnvVar("UNKEY_DEBUG")), + }, + Action: action, +} + +func main() { + if err := Cmd.Run(context.Background(), os.Args); err != nil { + fmt.Fprintf(os.Stderr, "unkey-env: %v\n", err) + os.Exit(1) + } +} + +func action(ctx context.Context, cmd *cli.Command) error { + cfg := Config{ + Provider: provider.Type(cmd.String("provider")), + Endpoint: cmd.String("endpoint"), + DeploymentID: cmd.String("deployment-id"), + EnvironmentID: cmd.String("environment-id"), + EncryptedBlob: cmd.String("secrets-blob"), + Token: cmd.String("token"), + TokenPath: cmd.String("token-path"), + Debug: cmd.Bool("debug"), + Args: cmd.Args(), + } + + if err := cfg.Validate(); err != nil { + return cli.Exit(err.Error(), 1) + } + + return run(ctx, cfg) +} + +func run(ctx context.Context, cfg Config) error { + logger := logging.New() + if cfg.Debug { + logger = logger.With(slog.String("deployment", cfg.DeploymentID)) + } + + logger.Debug("starting unkey-env", + "provider", cfg.Provider, + "endpoint", cfg.Endpoint, + "deployment", cfg.DeploymentID, + ) + + p, err := provider.New(provider.Config{ + Type: cfg.Provider, + Endpoint: cfg.Endpoint, + }) + if err != nil { + return fmt.Errorf("failed to create provider: %w", err) + } + + fetchCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + var encryptedBlob []byte + if cfg.EncryptedBlob != "" { + var decodeErr error + encryptedBlob, decodeErr = base64.StdEncoding.DecodeString(cfg.EncryptedBlob) + if decodeErr != nil { + return fmt.Errorf("failed to decode secrets blob: %w", decodeErr) + } + } + + secrets, err := p.FetchSecrets(fetchCtx, provider.FetchOptions{ + DeploymentID: cfg.DeploymentID, + EnvironmentID: cfg.EnvironmentID, + EncryptedBlob: encryptedBlob, + Token: cfg.Token, + TokenPath: cfg.TokenPath, + }) + if err != nil { + return fmt.Errorf("failed to fetch secrets: %w", err) + } + + logger.Debug("fetched secrets", "count", len(secrets)) + + for _, env := range os.Environ() { + name, _, _ := strings.Cut(env, "=") + if strings.HasPrefix(name, "UNKEY_") && !allowedUnkeyVars[name] { + os.Unsetenv(name) + } + } + + for key, value := range secrets { + if err := os.Setenv(key, value); err != nil { + return fmt.Errorf("failed to set env var %s: %w", key, err) + } + logger.Debug("set environment variable", "key", key) + } + + command := cfg.Args[0] + binary, err := exec.LookPath(command) + if err != nil { + return fmt.Errorf("command not found: %s: %w", command, err) + } + + logger.Debug("executing command", "binary", binary, "args", cfg.Args) + + err = syscall.Exec(binary, cfg.Args, os.Environ()) + if err != nil { + return fmt.Errorf("exec failed: %w", err) + } + + return nil +} diff --git a/go/cmd/version/main.go b/go/cmd/version/main.go index 3215a90bb2..fc4c3f9d08 100644 --- a/go/cmd/version/main.go +++ b/go/cmd/version/main.go @@ -31,12 +31,13 @@ AVAILABLE COMMANDS: } var getCmd = &cli.Command{ - Flags: []cli.Flag{}, - Version: "", - Commands: []*cli.Command{}, - Aliases: []string{}, - Name: "get", - Usage: "Get details about a version", + Flags: []cli.Flag{}, + Version: "", + Commands: []*cli.Command{}, + Aliases: []string{}, + AcceptsArgs: true, + Name: "get", + Usage: "Get details about a version", Description: `Get comprehensive details about a specific version including status, branch, creation time, and associated hostnames. EXAMPLES: @@ -105,8 +106,9 @@ unkey version list --branch main --status active --limit 3 # Combine filters`, // nolint: exhaustruct var rollbackCmd = &cli.Command{ - Name: "rollback", - Usage: "Rollback to a previous version", + AcceptsArgs: true, + Name: "rollback", + Usage: "Rollback to a previous version", Description: `Rollback a hostname to a previous version. This operation will switch traffic from the current version to the specified target version. WARNING: diff --git a/go/gen/proto/krane/v1/deployment.pb.go b/go/gen/proto/krane/v1/deployment.pb.go index 75463ddfe8..965952fc98 100644 --- a/go/gen/proto/krane/v1/deployment.pb.go +++ b/go/gen/proto/krane/v1/deployment.pb.go @@ -81,9 +81,14 @@ type DeploymentRequest struct { Replicas uint32 `protobuf:"varint,4,opt,name=replicas,proto3" json:"replicas,omitempty"` CpuMillicores uint32 `protobuf:"varint,5,opt,name=cpu_millicores,json=cpuMillicores,proto3" json:"cpu_millicores,omitempty"` MemorySizeMib uint64 `protobuf:"varint,6,opt,name=memory_size_mib,json=memorySizeMib,proto3" json:"memory_size_mib,omitempty"` - // Environment variables to inject into the container. - // Keys are variable names, values are the (decrypted) values. - EnvVars map[string]string `protobuf:"bytes,7,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Environment slug (e.g., production, staging). + EnvironmentSlug string `protobuf:"bytes,7,opt,name=environment_slug,json=environmentSlug,proto3" json:"environment_slug,omitempty"` + // Encrypted secrets blob to be decrypted at runtime by unkey-env. + // This is set as UNKEY_SECRETS_BLOB env var in the container. + // unkey-env calls krane's DecryptSecretsBlob RPC to decrypt. + EncryptedSecretsBlob []byte `protobuf:"bytes,8,opt,name=encrypted_secrets_blob,json=encryptedSecretsBlob,proto3" json:"encrypted_secrets_blob,omitempty"` + // Environment ID for secrets decryption (keyring identifier). + EnvironmentId string `protobuf:"bytes,9,opt,name=environment_id,json=environmentId,proto3" json:"environment_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -160,13 +165,27 @@ func (x *DeploymentRequest) GetMemorySizeMib() uint64 { return 0 } -func (x *DeploymentRequest) GetEnvVars() map[string]string { +func (x *DeploymentRequest) GetEnvironmentSlug() string { if x != nil { - return x.EnvVars + return x.EnvironmentSlug + } + return "" +} + +func (x *DeploymentRequest) GetEncryptedSecretsBlob() []byte { + if x != nil { + return x.EncryptedSecretsBlob } return nil } +func (x *DeploymentRequest) GetEnvironmentId() string { + if x != nil { + return x.EnvironmentId + } + return "" +} + type CreateDeploymentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Deployment *DeploymentRequest `protobuf:"bytes,1,opt,name=deployment,proto3" json:"deployment,omitempty"` @@ -591,18 +610,17 @@ var File_krane_v1_deployment_proto protoreflect.FileDescriptor const file_krane_v1_deployment_proto_rawDesc = "" + "\n" + - "\x19krane/v1/deployment.proto\x12\bkrane.v1\"\xd8\x02\n" + + "\x19krane/v1/deployment.proto\x12\bkrane.v1\"\xdf\x02\n" + "\x11DeploymentRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12#\n" + "\rdeployment_id\x18\x02 \x01(\tR\fdeploymentId\x12\x14\n" + "\x05image\x18\x03 \x01(\tR\x05image\x12\x1a\n" + "\breplicas\x18\x04 \x01(\rR\breplicas\x12%\n" + "\x0ecpu_millicores\x18\x05 \x01(\rR\rcpuMillicores\x12&\n" + - "\x0fmemory_size_mib\x18\x06 \x01(\x04R\rmemorySizeMib\x12C\n" + - "\benv_vars\x18\a \x03(\v2(.krane.v1.DeploymentRequest.EnvVarsEntryR\aenvVars\x1a:\n" + - "\fEnvVarsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"V\n" + + "\x0fmemory_size_mib\x18\x06 \x01(\x04R\rmemorySizeMib\x12)\n" + + "\x10environment_slug\x18\a \x01(\tR\x0fenvironmentSlug\x124\n" + + "\x16encrypted_secrets_blob\x18\b \x01(\fR\x14encryptedSecretsBlob\x12%\n" + + "\x0eenvironment_id\x18\t \x01(\tR\renvironmentId\"V\n" + "\x17CreateDeploymentRequest\x12;\n" + "\n" + "deployment\x18\x01 \x01(\v2\x1b.krane.v1.DeploymentRequestR\n" + @@ -652,7 +670,7 @@ func file_krane_v1_deployment_proto_rawDescGZIP() []byte { } var file_krane_v1_deployment_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_krane_v1_deployment_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_krane_v1_deployment_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_krane_v1_deployment_proto_goTypes = []any{ (DeploymentStatus)(0), // 0: krane.v1.DeploymentStatus (*DeploymentRequest)(nil), // 1: krane.v1.DeploymentRequest @@ -665,26 +683,24 @@ var file_krane_v1_deployment_proto_goTypes = []any{ (*GetDeploymentRequest)(nil), // 8: krane.v1.GetDeploymentRequest (*GetDeploymentResponse)(nil), // 9: krane.v1.GetDeploymentResponse (*Instance)(nil), // 10: krane.v1.Instance - nil, // 11: krane.v1.DeploymentRequest.EnvVarsEntry } var file_krane_v1_deployment_proto_depIdxs = []int32{ - 11, // 0: krane.v1.DeploymentRequest.env_vars:type_name -> krane.v1.DeploymentRequest.EnvVarsEntry - 1, // 1: krane.v1.CreateDeploymentRequest.deployment:type_name -> krane.v1.DeploymentRequest - 0, // 2: krane.v1.CreateDeploymentResponse.status:type_name -> krane.v1.DeploymentStatus - 1, // 3: krane.v1.UpdateDeploymentRequest.deployment:type_name -> krane.v1.DeploymentRequest - 10, // 4: krane.v1.GetDeploymentResponse.instances:type_name -> krane.v1.Instance - 0, // 5: krane.v1.Instance.status:type_name -> krane.v1.DeploymentStatus - 2, // 6: krane.v1.DeploymentService.CreateDeployment:input_type -> krane.v1.CreateDeploymentRequest - 8, // 7: krane.v1.DeploymentService.GetDeployment:input_type -> krane.v1.GetDeploymentRequest - 6, // 8: krane.v1.DeploymentService.DeleteDeployment:input_type -> krane.v1.DeleteDeploymentRequest - 3, // 9: krane.v1.DeploymentService.CreateDeployment:output_type -> krane.v1.CreateDeploymentResponse - 9, // 10: krane.v1.DeploymentService.GetDeployment:output_type -> krane.v1.GetDeploymentResponse - 7, // 11: krane.v1.DeploymentService.DeleteDeployment:output_type -> krane.v1.DeleteDeploymentResponse - 9, // [9:12] is the sub-list for method output_type - 6, // [6:9] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 1, // 0: krane.v1.CreateDeploymentRequest.deployment:type_name -> krane.v1.DeploymentRequest + 0, // 1: krane.v1.CreateDeploymentResponse.status:type_name -> krane.v1.DeploymentStatus + 1, // 2: krane.v1.UpdateDeploymentRequest.deployment:type_name -> krane.v1.DeploymentRequest + 10, // 3: krane.v1.GetDeploymentResponse.instances:type_name -> krane.v1.Instance + 0, // 4: krane.v1.Instance.status:type_name -> krane.v1.DeploymentStatus + 2, // 5: krane.v1.DeploymentService.CreateDeployment:input_type -> krane.v1.CreateDeploymentRequest + 8, // 6: krane.v1.DeploymentService.GetDeployment:input_type -> krane.v1.GetDeploymentRequest + 6, // 7: krane.v1.DeploymentService.DeleteDeployment:input_type -> krane.v1.DeleteDeploymentRequest + 3, // 8: krane.v1.DeploymentService.CreateDeployment:output_type -> krane.v1.CreateDeploymentResponse + 9, // 9: krane.v1.DeploymentService.GetDeployment:output_type -> krane.v1.GetDeploymentResponse + 7, // 10: krane.v1.DeploymentService.DeleteDeployment:output_type -> krane.v1.DeleteDeploymentResponse + 8, // [8:11] is the sub-list for method output_type + 5, // [5:8] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_krane_v1_deployment_proto_init() } @@ -698,7 +714,7 @@ func file_krane_v1_deployment_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_krane_v1_deployment_proto_rawDesc), len(file_krane_v1_deployment_proto_rawDesc)), NumEnums: 1, - NumMessages: 11, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/go/gen/proto/krane/v1/kranev1connect/secrets.connect.go b/go/gen/proto/krane/v1/kranev1connect/secrets.connect.go new file mode 100644 index 0000000000..7af90143b6 --- /dev/null +++ b/go/gen/proto/krane/v1/kranev1connect/secrets.connect.go @@ -0,0 +1,115 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: krane/v1/secrets.proto + +package kranev1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // SecretsServiceName is the fully-qualified name of the SecretsService service. + SecretsServiceName = "krane.v1.SecretsService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // SecretsServiceDecryptSecretsBlobProcedure is the fully-qualified name of the SecretsService's + // DecryptSecretsBlob RPC. + SecretsServiceDecryptSecretsBlobProcedure = "/krane.v1.SecretsService/DecryptSecretsBlob" +) + +// SecretsServiceClient is a client for the krane.v1.SecretsService service. +type SecretsServiceClient interface { + // DecryptSecretsBlob decrypts an encrypted secrets blob passed in the pod spec. + // This avoids DB lookups - the encrypted blob travels with the pod. + // Authentication is via K8s service account token or DB-stored token. + DecryptSecretsBlob(context.Context, *connect.Request[v1.DecryptSecretsBlobRequest]) (*connect.Response[v1.DecryptSecretsBlobResponse], error) +} + +// NewSecretsServiceClient constructs a client for the krane.v1.SecretsService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewSecretsServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SecretsServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + secretsServiceMethods := v1.File_krane_v1_secrets_proto.Services().ByName("SecretsService").Methods() + return &secretsServiceClient{ + decryptSecretsBlob: connect.NewClient[v1.DecryptSecretsBlobRequest, v1.DecryptSecretsBlobResponse]( + httpClient, + baseURL+SecretsServiceDecryptSecretsBlobProcedure, + connect.WithSchema(secretsServiceMethods.ByName("DecryptSecretsBlob")), + connect.WithClientOptions(opts...), + ), + } +} + +// secretsServiceClient implements SecretsServiceClient. +type secretsServiceClient struct { + decryptSecretsBlob *connect.Client[v1.DecryptSecretsBlobRequest, v1.DecryptSecretsBlobResponse] +} + +// DecryptSecretsBlob calls krane.v1.SecretsService.DecryptSecretsBlob. +func (c *secretsServiceClient) DecryptSecretsBlob(ctx context.Context, req *connect.Request[v1.DecryptSecretsBlobRequest]) (*connect.Response[v1.DecryptSecretsBlobResponse], error) { + return c.decryptSecretsBlob.CallUnary(ctx, req) +} + +// SecretsServiceHandler is an implementation of the krane.v1.SecretsService service. +type SecretsServiceHandler interface { + // DecryptSecretsBlob decrypts an encrypted secrets blob passed in the pod spec. + // This avoids DB lookups - the encrypted blob travels with the pod. + // Authentication is via K8s service account token or DB-stored token. + DecryptSecretsBlob(context.Context, *connect.Request[v1.DecryptSecretsBlobRequest]) (*connect.Response[v1.DecryptSecretsBlobResponse], error) +} + +// NewSecretsServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewSecretsServiceHandler(svc SecretsServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + secretsServiceMethods := v1.File_krane_v1_secrets_proto.Services().ByName("SecretsService").Methods() + secretsServiceDecryptSecretsBlobHandler := connect.NewUnaryHandler( + SecretsServiceDecryptSecretsBlobProcedure, + svc.DecryptSecretsBlob, + connect.WithSchema(secretsServiceMethods.ByName("DecryptSecretsBlob")), + connect.WithHandlerOptions(opts...), + ) + return "/krane.v1.SecretsService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SecretsServiceDecryptSecretsBlobProcedure: + secretsServiceDecryptSecretsBlobHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedSecretsServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedSecretsServiceHandler struct{} + +func (UnimplementedSecretsServiceHandler) DecryptSecretsBlob(context.Context, *connect.Request[v1.DecryptSecretsBlobRequest]) (*connect.Response[v1.DecryptSecretsBlobResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("krane.v1.SecretsService.DecryptSecretsBlob is not implemented")) +} diff --git a/go/gen/proto/krane/v1/secrets.pb.go b/go/gen/proto/krane/v1/secrets.pb.go new file mode 100644 index 0000000000..3836065534 --- /dev/null +++ b/go/gen/proto/krane/v1/secrets.pb.go @@ -0,0 +1,212 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.8 +// protoc (unknown) +// source: krane/v1/secrets.proto + +package kranev1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type DecryptSecretsBlobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The encrypted secrets blob from the pod spec (UNKEY_SECRETS_BLOB env var). + // This is the SecretsConfig proto, encrypted with the environment's vault keyring. + EncryptedBlob []byte `protobuf:"bytes,1,opt,name=encrypted_blob,json=encryptedBlob,proto3" json:"encrypted_blob,omitempty"` + // Environment ID (keyring) to use for decryption. + EnvironmentId string `protobuf:"bytes,2,opt,name=environment_id,json=environmentId,proto3" json:"environment_id,omitempty"` + // Token for authentication (K8s service account token or DB-stored token). + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + // Deployment ID for token validation. + DeploymentId string `protobuf:"bytes,4,opt,name=deployment_id,json=deploymentId,proto3" json:"deployment_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DecryptSecretsBlobRequest) Reset() { + *x = DecryptSecretsBlobRequest{} + mi := &file_krane_v1_secrets_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DecryptSecretsBlobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DecryptSecretsBlobRequest) ProtoMessage() {} + +func (x *DecryptSecretsBlobRequest) ProtoReflect() protoreflect.Message { + mi := &file_krane_v1_secrets_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DecryptSecretsBlobRequest.ProtoReflect.Descriptor instead. +func (*DecryptSecretsBlobRequest) Descriptor() ([]byte, []int) { + return file_krane_v1_secrets_proto_rawDescGZIP(), []int{0} +} + +func (x *DecryptSecretsBlobRequest) GetEncryptedBlob() []byte { + if x != nil { + return x.EncryptedBlob + } + return nil +} + +func (x *DecryptSecretsBlobRequest) GetEnvironmentId() string { + if x != nil { + return x.EnvironmentId + } + return "" +} + +func (x *DecryptSecretsBlobRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *DecryptSecretsBlobRequest) GetDeploymentId() string { + if x != nil { + return x.DeploymentId + } + return "" +} + +type DecryptSecretsBlobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Decrypted environment variables (key -> plaintext value) + EnvVars map[string]string `protobuf:"bytes,1,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DecryptSecretsBlobResponse) Reset() { + *x = DecryptSecretsBlobResponse{} + mi := &file_krane_v1_secrets_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DecryptSecretsBlobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DecryptSecretsBlobResponse) ProtoMessage() {} + +func (x *DecryptSecretsBlobResponse) ProtoReflect() protoreflect.Message { + mi := &file_krane_v1_secrets_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DecryptSecretsBlobResponse.ProtoReflect.Descriptor instead. +func (*DecryptSecretsBlobResponse) Descriptor() ([]byte, []int) { + return file_krane_v1_secrets_proto_rawDescGZIP(), []int{1} +} + +func (x *DecryptSecretsBlobResponse) GetEnvVars() map[string]string { + if x != nil { + return x.EnvVars + } + return nil +} + +var File_krane_v1_secrets_proto protoreflect.FileDescriptor + +const file_krane_v1_secrets_proto_rawDesc = "" + + "\n" + + "\x16krane/v1/secrets.proto\x12\bkrane.v1\"\xa4\x01\n" + + "\x19DecryptSecretsBlobRequest\x12%\n" + + "\x0eencrypted_blob\x18\x01 \x01(\fR\rencryptedBlob\x12%\n" + + "\x0eenvironment_id\x18\x02 \x01(\tR\renvironmentId\x12\x14\n" + + "\x05token\x18\x03 \x01(\tR\x05token\x12#\n" + + "\rdeployment_id\x18\x04 \x01(\tR\fdeploymentId\"\xa6\x01\n" + + "\x1aDecryptSecretsBlobResponse\x12L\n" + + "\benv_vars\x18\x01 \x03(\v21.krane.v1.DecryptSecretsBlobResponse.EnvVarsEntryR\aenvVars\x1a:\n" + + "\fEnvVarsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x012q\n" + + "\x0eSecretsService\x12_\n" + + "\x12DecryptSecretsBlob\x12#.krane.v1.DecryptSecretsBlobRequest\x1a$.krane.v1.DecryptSecretsBlobResponseB\x95\x01\n" + + "\fcom.krane.v1B\fSecretsProtoP\x01Z6github.com/unkeyed/unkey/go/gen/proto/krane/v1;kranev1\xa2\x02\x03KXX\xaa\x02\bKrane.V1\xca\x02\bKrane\\V1\xe2\x02\x14Krane\\V1\\GPBMetadata\xea\x02\tKrane::V1b\x06proto3" + +var ( + file_krane_v1_secrets_proto_rawDescOnce sync.Once + file_krane_v1_secrets_proto_rawDescData []byte +) + +func file_krane_v1_secrets_proto_rawDescGZIP() []byte { + file_krane_v1_secrets_proto_rawDescOnce.Do(func() { + file_krane_v1_secrets_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_krane_v1_secrets_proto_rawDesc), len(file_krane_v1_secrets_proto_rawDesc))) + }) + return file_krane_v1_secrets_proto_rawDescData +} + +var file_krane_v1_secrets_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_krane_v1_secrets_proto_goTypes = []any{ + (*DecryptSecretsBlobRequest)(nil), // 0: krane.v1.DecryptSecretsBlobRequest + (*DecryptSecretsBlobResponse)(nil), // 1: krane.v1.DecryptSecretsBlobResponse + nil, // 2: krane.v1.DecryptSecretsBlobResponse.EnvVarsEntry +} +var file_krane_v1_secrets_proto_depIdxs = []int32{ + 2, // 0: krane.v1.DecryptSecretsBlobResponse.env_vars:type_name -> krane.v1.DecryptSecretsBlobResponse.EnvVarsEntry + 0, // 1: krane.v1.SecretsService.DecryptSecretsBlob:input_type -> krane.v1.DecryptSecretsBlobRequest + 1, // 2: krane.v1.SecretsService.DecryptSecretsBlob:output_type -> krane.v1.DecryptSecretsBlobResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_krane_v1_secrets_proto_init() } +func file_krane_v1_secrets_proto_init() { + if File_krane_v1_secrets_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_krane_v1_secrets_proto_rawDesc), len(file_krane_v1_secrets_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_krane_v1_secrets_proto_goTypes, + DependencyIndexes: file_krane_v1_secrets_proto_depIdxs, + MessageInfos: file_krane_v1_secrets_proto_msgTypes, + }.Build() + File_krane_v1_secrets_proto = out.File + file_krane_v1_secrets_proto_goTypes = nil + file_krane_v1_secrets_proto_depIdxs = nil +} diff --git a/go/go.mod b/go/go.mod index da64de8938..48af0f9bb3 100644 --- a/go/go.mod +++ b/go/go.mod @@ -10,18 +10,20 @@ require ( connectrpc.com/connect v1.19.0 github.com/AfterShip/clickhouse-sql-parser v0.4.16 github.com/ClickHouse/clickhouse-go/v2 v2.40.1 - github.com/aws/aws-sdk-go-v2 v1.36.6 - github.com/aws/aws-sdk-go-v2/config v1.29.18 - github.com/aws/aws-sdk-go-v2/credentials v1.17.71 + github.com/aws/aws-sdk-go-v2 v1.39.6 + github.com/aws/aws-sdk-go-v2/config v1.31.17 + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 github.com/bytedance/sonic v1.14.2 github.com/depot/depot-go v0.5.1 - github.com/docker/cli v28.4.0+incompatible - github.com/docker/docker v28.4.0+incompatible + github.com/docker/cli v29.0.3+incompatible + github.com/docker/docker v28.5.2+incompatible github.com/docker/go-connections v0.6.0 github.com/getkin/kin-openapi v0.133.0 github.com/go-acme/lego/v4 v4.25.2 github.com/go-sql-driver/mysql v1.9.3 + github.com/google/go-containerregistry v0.20.7 + github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20251124222020-e075f209120b github.com/lmittmann/tint v1.1.2 github.com/maypok86/otter v1.2.4 github.com/moby/buildkit v0.25.0 @@ -51,35 +53,48 @@ require ( go.opentelemetry.io/otel/sdk/log v0.14.0 go.opentelemetry.io/otel/sdk/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/net v0.44.0 - golang.org/x/text v0.29.0 + golang.org/x/net v0.47.0 + golang.org/x/text v0.31.0 google.golang.org/protobuf v1.36.10 - k8s.io/api v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 + k8s.io/api v0.34.2 + k8s.io/apimachinery v0.34.2 + k8s.io/client-go v0.34.2 ) require ( cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.30 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/ClickHouse/ch-go v0.68.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/TwiN/go-color v1.4.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect - github.com/aws/smithy-go v1.23.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -88,6 +103,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/console v1.0.5 // indirect github.com/containerd/containerd/api v1.9.0 // indirect @@ -97,13 +113,15 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.1 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dolthub/maphash v0.1.0 // indirect github.com/ebitengine/purego v0.9.0 // indirect @@ -133,10 +151,12 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect @@ -146,15 +166,15 @@ require ( github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/miekg/dns v1.1.68 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -200,6 +220,7 @@ require ( github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/wI2L/jsondiff v0.7.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect @@ -217,22 +238,21 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect golang.org/x/time v0.13.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/grpc v1.75.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.0.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect @@ -247,3 +267,5 @@ replace github.com/dprotaso/go-yit => github.com/dprotaso/go-yit v0.0.0-20191028 // Sqlc engine errors replace github.com/pingcap/tidb/pkg/parser => github.com/pingcap/tidb/pkg/parser v0.0.0-20250806091815-327a22d5ebf8 + +replace cloud.google.com/go/compute => cloud.google.com/go/compute v1.49.1 diff --git a/go/go.sum b/go/go.sum index 0a390767d5..1c0cf1e850 100644 --- a/go/go.sum +++ b/go/go.sum @@ -4,6 +4,8 @@ buf.build/gen/go/depot/api/protocolbuffers/go v1.36.10-20250915125527-3af9e416de buf.build/gen/go/depot/api/protocolbuffers/go v1.36.10-20250915125527-3af9e416de91.1/go.mod h1:uwFBBwta8N7weZkaVAdyWLFn0kX/vchbQZxVekqiUg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= connectrpc.com/connect v1.19.0 h1:LuqUbq01PqbtL0o7vn0WMRXzR2nNsiINe5zfcJ24pJM= connectrpc.com/connect v1.19.0/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -12,8 +14,36 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AfterShip/clickhouse-sql-parser v0.4.16 h1:gpl+wXclYUKT0p4+gBq22XeRYWwEoZ9f35vogqMvkLQ= github.com/AfterShip/clickhouse-sql-parser v0.4.16/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/ClickHouse/ch-go v0.68.0 h1:zd2VD8l2aVYnXFRyhTyKCrxvhSz1AaY4wBUXu/f0GiU= github.com/ClickHouse/ch-go v0.68.0/go.mod h1:C89Fsm7oyck9hr6rRo5gqqiVtaIY6AjdD0WFMyNRQ5s= github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4= @@ -30,42 +60,48 @@ github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1 github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU= -github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= -github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I= -github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA= -github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 h1:XTZZ0I3SZUHAtBLBU6395ad+VOblE0DwQP6MuaNeics= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37/go.mod h1:Pi6ksbniAWVwu2S8pEzcYPyhUkAcLaufxN7PfAUQjBk= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2 h1:aq2N/9UkbEyljIQ7OFcudEgUsJzO8MYucmfsM/k/dmc= +github.com/aws/aws-sdk-go-v2/service/ecr v1.51.2/go.mod h1:1NVD1KuMjH2GqnPwMotPndQaT/MreKkWpjkF12d6oKU= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2 h1:9fe6w8bydUwNAhFVmjo+SRqAJjbBMOyILL/6hTTVkyA= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.38.2/go.mod h1:x7gU4CAyAz4BsM9hlRkhHiYw2GIr1QCmN45uwQw9l/E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 h1:M5/B8JUaCI8+9QD+u3S/f4YHpvqE9RpSkV3rf0Iks2w= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5/go.mod h1:Bktzci1bwdbpuLiu3AOksiNPMl/LLKmX1TWmqp2xbvs= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 h1:OS2e0SKqsU2LiJPqL8u9x41tKc6MMEHrWjLVLn3oysg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18/go.mod h1:+Yrk+MDGzlNGxCXieljNeWpoZTCQUQVL+Jk9hGGJ8qM= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 h1:RkHXU9jP0DptGy7qKI8CBGsUJruWz0v5IgwBa2DwWcU= github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1/go.mod h1:3xAOf7tdKF+qbb+XpU+EPhNXAdun3Lu1RcDrj8KC24I= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0 h1:GOPttfOAf5qAgx7r6b+zCWZrvCsfKffkL4H6mSYx1kA= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0/go.mod h1:a2HN6+p7k0JLDO8514sMr0l4cnrR52z4sWoZ/Uc82ho= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -88,6 +124,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= @@ -116,8 +154,8 @@ github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsW github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= -github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE= -github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -130,16 +168,20 @@ github.com/depot/depot-go v0.5.1 h1:Kdrsk8q7W2fQvoudWNjxsXG4ZbdlUAa6EV18udDnTFQ= github.com/depot/depot-go v0.5.1/go.mod h1:QQtSqwRn0flx4KxrUVSJGlh0hTFeZ19MLYvOcJbxtP0= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= -github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= -github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= +github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -214,6 +256,11 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -224,11 +271,16 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20251124222020-e075f209120b h1:MaiUT//WzZb/7yWZLyUzidm4ewVnJFkmxNTsKD9N5iM= +github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20251124222020-e075f209120b/go.mod h1:J7Vegj1A02fAWDsezb9jFU3T/9rNOc775xp7pE6LMJ0= +github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f h1:GJRzEBoJv/A/E7JbTekq1Q0jFtAfY7TIxUFAK89Mmic= +github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20250225234217-098045d5e61f/go.mod h1:ZT74/OE6eosKneM9/LQItNxIMBV6CI5S46EXAnvkTBI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= @@ -251,8 +303,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -274,6 +326,8 @@ github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc= github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og= github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -350,7 +404,6 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -396,7 +449,6 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= @@ -414,6 +466,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -450,8 +503,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/unkeyed/unkey/go/deploy/pkg/spiffe v0.0.0-20250929110415-ca2de7336e18 h1:8TyQZ28XT82Rji0UdPoZpwk3p7zsMKpdChCIBxlRMwo= github.com/unkeyed/unkey/go/deploy/pkg/spiffe v0.0.0-20250929110415-ca2de7336e18/go.mod h1:fvqbcz5BljbvrZ1p8LWkNAty8vRsTo1aPLw1sOmLXEI= -github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= -github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= @@ -475,6 +528,7 @@ github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGu github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= @@ -535,31 +589,41 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= -golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -568,28 +632,41 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -620,14 +697,14 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/go/k8s/manifests/krane.yaml b/go/k8s/manifests/krane.yaml index ca5487a9cf..f49d719c25 100644 --- a/go/k8s/manifests/krane.yaml +++ b/go/k8s/manifests/krane.yaml @@ -35,6 +35,17 @@ spec: value: "8080" - name: UNKEY_DEPLOYMENT_EVICTION_TTL value: "10m" + # Vault Configuration + - name: UNKEY_VAULT_MASTER_KEYS + value: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" + - name: UNKEY_VAULT_S3_URL + value: "http://s3:3902" + - name: UNKEY_VAULT_S3_BUCKET + value: "vault" + - name: UNKEY_VAULT_S3_ACCESS_KEY_ID + value: "minio_root_user" + - name: UNKEY_VAULT_S3_ACCESS_KEY_SECRET + value: "minio_root_password" --- apiVersion: v1 diff --git a/go/k8s/manifests/observability.yaml b/go/k8s/manifests/observability.yaml index 014206ceb8..62bc41f080 100644 --- a/go/k8s/manifests/observability.yaml +++ b/go/k8s/manifests/observability.yaml @@ -90,71 +90,11 @@ spec: spec: containers: - name: otel-collector - image: otel/opentelemetry-collector-contrib:0.92.0 + image: grafana/otel-lgtm:0.11.7 ports: + - containerPort: 3000 # Grafana - containerPort: 4317 # OTLP gRPC - containerPort: 4318 # OTLP HTTP - - containerPort: 8889 # Prometheus metrics - command: - - /otelcol-contrib - - --config=/etc/otelcol-contrib/otel-collector.yml - env: - - name: GOMEMLIMIT - value: "160MiB" - volumeMounts: - - name: otel-collector-config - mountPath: /etc/otelcol-contrib/ - volumes: - - name: otel-collector-config - configMap: - name: otel-collector-config - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: otel-collector-config - namespace: unkey -data: - otel-collector.yml: | - receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - prometheus: - config: - scrape_configs: - - job_name: 'otel-collector' - scrape_interval: 10s - static_configs: - - targets: ['0.0.0.0:8889'] - - processors: - batch: - - exporters: - prometheus: - endpoint: "0.0.0.0:8889" - logging: - loglevel: info - - service: - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [logging] - metrics: - receivers: [otlp, prometheus] - processors: [batch] - exporters: [prometheus, logging] - logs: - receivers: [otlp] - processors: [batch] - exporters: [logging] --- apiVersion: v1 @@ -168,6 +108,10 @@ spec: selector: app: otel-collector ports: + - name: grafana + port: 3000 + targetPort: 3000 + protocol: TCP - name: otlp-grpc port: 4317 targetPort: 4317 @@ -176,8 +120,4 @@ spec: port: 4318 targetPort: 4318 protocol: TCP - - name: prometheus - port: 8889 - targetPort: 8889 - protocol: TCP type: NodePort diff --git a/go/k8s/manifests/rbac.yaml b/go/k8s/manifests/rbac.yaml index e8c668feae..0ca648303a 100644 --- a/go/k8s/manifests/rbac.yaml +++ b/go/k8s/manifests/rbac.yaml @@ -10,7 +10,8 @@ metadata: --- # Restricted service account for customer workloads -# This account has NO permissions - customers cannot query the K8s API +# This account has NO K8s API permissions - customers cannot query the K8s API. +# Token is mounted for unkey-env to authenticate with the secrets provider. apiVersion: v1 kind: ServiceAccount metadata: @@ -19,7 +20,6 @@ metadata: labels: app: unkey component: customer -# automountServiceAccountToken is also disabled at pod level for defense in depth --- apiVersion: rbac.authorization.k8s.io/v1 @@ -55,6 +55,11 @@ rules: resources: ["events"] verbs: ["get", "list", "watch"] + # TokenReview for validating service account tokens (secrets injection) + - apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: ["create"] + --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/go/k8s/manifests/secrets-webhook.yaml b/go/k8s/manifests/secrets-webhook.yaml new file mode 100644 index 0000000000..e4f9d4b1d1 --- /dev/null +++ b/go/k8s/manifests/secrets-webhook.yaml @@ -0,0 +1,163 @@ +--- +# ServiceAccount for the secrets-webhook +apiVersion: v1 +kind: ServiceAccount +metadata: + name: secrets-webhook + namespace: unkey +--- +# ClusterRole for the webhook to read pods and registry auth +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: secrets-webhook +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get"] +--- +# ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: secrets-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: secrets-webhook +subjects: + - kind: ServiceAccount + name: secrets-webhook + namespace: unkey +--- +# TLS Secret for webhook is created by Tiltfile (local dev) or cert-manager (production) +# For manual setup: mkcert -cert-file tls.crt -key-file tls.key secrets-webhook.unkey.svc secrets-webhook.unkey.svc.cluster.local +# kubectl create secret tls secrets-webhook-tls -n unkey --cert=tls.crt --key=tls.key +--- +# Deployment for the secrets-webhook +apiVersion: apps/v1 +kind: Deployment +metadata: + name: secrets-webhook + namespace: unkey + labels: + app: secrets-webhook +spec: + replicas: 1 + selector: + matchLabels: + app: secrets-webhook + template: + metadata: + labels: + app: secrets-webhook + spec: + serviceAccountName: secrets-webhook + containers: + - name: webhook + image: unkey-secrets-webhook:latest + imagePullPolicy: IfNotPresent + command: ["/unkey", "run", "secrets-webhook"] + env: + - name: WEBHOOK_PORT + value: "8443" + - name: WEBHOOK_TLS_CERT_FILE + value: "/certs/tls.crt" + - name: WEBHOOK_TLS_KEY_FILE + value: "/certs/tls.key" + - name: UNKEY_ENV_IMAGE + value: "unkey-env:latest" + - name: UNKEY_ENV_IMAGE_PULL_POLICY + value: "Never" # Local dev uses pre-loaded images; use IfNotPresent in prod + - name: KRANE_ENDPOINT + value: "http://krane.unkey.svc.cluster.local:8080" + ports: + - containerPort: 8443 + name: https + volumeMounts: + - name: tls-certs + mountPath: /certs + readOnly: true + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + livenessProbe: + httpGet: + path: /healthz + port: 8443 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: 8443 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: tls-certs + secret: + secretName: secrets-webhook-tls +--- +# Service for the webhook +apiVersion: v1 +kind: Service +metadata: + name: secrets-webhook + namespace: unkey +spec: + selector: + app: secrets-webhook + ports: + - port: 443 + targetPort: 8443 + protocol: TCP +--- +# MutatingWebhookConfiguration +# caBundle must be set to mkcert's root CA: +# cat "$(mkcert -CAROOT)/rootCA.pem" | base64 | tr -d '\n' +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: secrets-webhook +webhooks: + - name: secrets.unkey.com + clientConfig: + service: + name: secrets-webhook + namespace: unkey + path: /mutate + port: 443 + caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZLRENDQTVDZ0F3SUJBZ0lSQU5IdndmanJocmkzc1RSUktKVTVJb013RFFZSktvWklodmNOQVFFTEJRQXcKZ2FzeEhqQWNCZ05WQkFvVEZXMXJZMlZ5ZENCa1pYWmxiRzl3YldWdWRDQkRRVEZBTUQ0R0ExVUVDd3czWm14dgpjbWxoYm1WcGEyVnNRRVpzYjNKcFlXNXpMVTFDVUMwekxteHZZMkZzWkc5dFlXbHVJQ2hHYkc5eWFXRnVJRVZwCmEyVnNLVEZITUVVR0ExVUVBd3crYld0alpYSjBJR1pzYjNKcFlXNWxhV3RsYkVCR2JHOXlhV0Z1Y3kxTlFsQXQKTXk1c2IyTmhiR1J2YldGcGJpQW9SbXh2Y21saGJpQkZhV3RsYkNrd0hoY05NalV3T0RBMU1UVTBPVFV3V2hjTgpNelV3T0RBMU1UVTBPVFV3V2pDQnF6RWVNQndHQTFVRUNoTVZiV3RqWlhKMElHUmxkbVZzYjNCdFpXNTBJRU5CCk1VQXdQZ1lEVlFRTEREZG1iRzl5YVdGdVpXbHJaV3hBUm14dmNtbGhibk10VFVKUUxUTXViRzlqWVd4a2IyMWgKYVc0Z0tFWnNiM0pwWVc0Z1JXbHJaV3dwTVVjd1JRWURWUVFEREQ1dGEyTmxjblFnWm14dmNtbGhibVZwYTJWcwpRRVpzYjNKcFlXNXpMVTFDVUMwekxteHZZMkZzWkc5dFlXbHVJQ2hHYkc5eWFXRnVJRVZwYTJWc0tUQ0NBYUl3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dHUEFEQ0NBWW9DZ2dHQkFLTmwwR2kxUEZNa3d3eHg3b2c4NE8xalhHdUIKVVJ1N3VWR3oyUHZVV2pxbm43SEtwQVovbmRZblB2ZUUrQlo0WEhmeE1TTllIVzZOWUppdVV4YVlpcEhpbVdOOApuTUt6SS9nZjBVVlVNdldTemZQSXFPSTFWYlF6Tko2djFIell4NUhidDRndnRLWEpxc3Jxbi91VzIwN25BZTBlClA1TkE1MUVISjZ4OWNnQUpZUTlSaXd2SXBZaTFZRU1ZTlhCWCtYMnQ1S3NyUHRUbW9FN09KTWQ1SiszZkljVUUKd2tqOHlKZm5Nc01BU2tsTEZuWUpEczhhOXhKUWszc0hhUmJhTVFBaDVzNWZYT0FxandodjhzS3FsZVFBMDkxSQpKVndKRlZjdkIvQXlFQ1JZdzdjVlZmWkUyTUJrdlVsSGxOOXQ5MU1kbnpGdDQvdUZTYmRJZXZiRU5wcng1K2JPCjNxUldIL0F2ZmpFditmM1ozaW4vRlBWMFhwT21TVU1XK1BjWEU2aVFhcVZWT1hOWmR1Z0dUY3pLQTZkK0dWa24KYzJFS2h4NStXalNBMEt5Q2ZFZzNLNkc0MTc3cTY4V1lxOEdYTWo3aVNrOTJVekV6TkRZYm5peklWK0UxRHJMNQo1d0ZmMzFDTURpYU5DR0ljYnR4Q2Q4dkRKWEFFQTV5Q1NVRzdsd0lEQVFBQm8wVXdRekFPQmdOVkhROEJBZjhFCkJBTUNBZ1F3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVS91bkZtbStIcFZjaG8xWC8KUTBOU0VhNWs5M0V3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUppdkZXNjJIRVNjQnBUM0xjM1dMdE9LcXAwYQpaTnBFS3dIWGNwckNYSjJQdHpGWkRSaVpnQ1hHK3N5YTcrWnEvc1VYZVdSS2NZK0xkeUNRL3BUbWUzWDdwa09pCkZycEo0ZlJOeDdTdFYrVTNJUEc3TW9jSkk4NnpnVnZPZ2loU3VjRWVHY20xVHlWRGhTeTJrWldWc3luU0hzeTQKS1RuWTR5VlpVQjVHT0tBay9xM0h6c05jaHpqSS9UUkViRVFXUWh3Y3FuNkdZYjRlU0FudTZTS2dvUHEzTU9WegpzNjRaaFF4ek11QzZnZkhkV3V3bmdhdDlGVEJONWpxa0NsemtvVXREMlhjOVlOQkFZSEw3L2RQSk9kMjRjT2E5CnpONTZ0SWpVN1ZGcHdrU0RCZ25NSUFaR3BxUWVZWTNqaFdPem8wMXBJOHY1OUhFeVpnUUxtZjhvN0hHVStyaE4KU2hLdkJSQzBpS3YvYXVTYUlubUNHWVlwWk90dmtoa0hRc3AwKzVndndXVk5lYW1BSWwweEYyZEFhQkxsN2xueApnbzc5ZWJ1RU5saHgxcnB5cHBGSUEraUc4OG9tL0xrM0tQSENHUmFncVdPS0tIYVgvVXNpaHBTcVl3UUdwQVh6CkkzM2s4VnBZZTRVZDIzTzZadXdQMDlrdFFqMUU4emluMzREcjBRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=" + rules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + scope: Namespaced + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: ["kube-system"] + objectSelector: + matchExpressions: + - key: unkey.com/inject + operator: In + values: ["true"] + failurePolicy: Ignore + reinvocationPolicy: Never + sideEffects: None + admissionReviewVersions: ["v1"] + timeoutSeconds: 10 diff --git a/go/pkg/cli/command.go b/go/pkg/cli/command.go index c9e6a629f5..b299013921 100644 --- a/go/pkg/cli/command.go +++ b/go/pkg/cli/command.go @@ -31,6 +31,7 @@ type Command struct { Flags []Flag // Available flags for this command Action Action // Function to execute when command is run Aliases []string // Alternative names for this command + AcceptsArgs bool // Whether this command accepts positional arguments commandPath string // Full command path for MDX generation (e.g., "run api") // Runtime state (populated during parsing) diff --git a/go/pkg/cli/parser.go b/go/pkg/cli/parser.go index 22ae3920ee..9430050f03 100644 --- a/go/pkg/cli/parser.go +++ b/go/pkg/cli/parser.go @@ -87,14 +87,11 @@ func (c *Command) parse(ctx context.Context, args []string) error { // Store parsed arguments c.args = commandArgs - if len(commandArgs) > 0 { - availableCommands := make([]string, len(c.Commands)) - for j, cmd := range c.Commands { - availableCommands[j] = cmd.Name - } + // Reject positional arguments unless the command explicitly accepts them + if len(commandArgs) > 0 && !c.AcceptsArgs { availableFlags := c.getAvailableFlags() - return fmt.Errorf("unexpected argument: %s\nAvailable flags: %s", - commandArgs[0], availableFlags) + return fmt.Errorf("command %q does not accept positional arguments, got: %s\nAvailable flags: %s", + c.Name, commandArgs[0], availableFlags) } // Validate all required flags are present diff --git a/go/pkg/secrets/provider/krane_vault.go b/go/pkg/secrets/provider/krane_vault.go new file mode 100644 index 0000000000..74a16b0f93 --- /dev/null +++ b/go/pkg/secrets/provider/krane_vault.go @@ -0,0 +1,85 @@ +package provider + +import ( + "context" + "net/http" + "os" + "strings" + "time" + + "connectrpc.com/connect" + kranev1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1" + "github.com/unkeyed/unkey/go/gen/proto/krane/v1/kranev1connect" + "github.com/unkeyed/unkey/go/pkg/assert" +) + +// KraneVaultProvider fetches secrets via Krane's SecretsService. +// Krane handles token validation and calls Vault for decryption. +type KraneVaultProvider struct { + client kranev1connect.SecretsServiceClient + endpoint string +} + +// NewKraneVaultProvider creates a new Krane-Vault secrets provider. +func NewKraneVaultProvider(cfg Config) (*KraneVaultProvider, error) { + if err := assert.NotEmpty(cfg.Endpoint, "krane-vault provider requires endpoint"); err != nil { + return nil, err + } + + client := kranev1connect.NewSecretsServiceClient( + &http.Client{Timeout: 30 * time.Second}, + cfg.Endpoint, + ) + + return &KraneVaultProvider{ + client: client, + endpoint: cfg.Endpoint, + }, nil +} + +// Name returns the provider name. +func (p *KraneVaultProvider) Name() string { + return string(KraneVault) +} + +// FetchSecrets retrieves secrets from Krane (which decrypts via Vault). +// If EncryptedBlob is provided, uses DecryptSecretsBlob RPC (no DB lookup). +// Otherwise falls back to GetDeploymentSecrets (requires DB lookup). +func (p *KraneVaultProvider) FetchSecrets(ctx context.Context, opts FetchOptions) (map[string]string, error) { + token := opts.Token + + if opts.TokenPath != "" { + tokenBytes, err := os.ReadFile(opts.TokenPath) + if err != nil { + return nil, err + } + token = strings.TrimSpace(string(tokenBytes)) + } + + if err := assert.All( + assert.NotEmpty(token, "token is required"), + assert.NotEmpty(opts.DeploymentID, "deployment_id is required"), + ); err != nil { + return nil, err + } + + // Use DecryptSecretsBlob if we have an encrypted blob (preferred - no DB lookup) + if len(opts.EncryptedBlob) > 0 { + if err := assert.NotEmpty(opts.EnvironmentID, "environment_id is required for blob decryption"); err != nil { + return nil, err + } + + resp, err := p.client.DecryptSecretsBlob(ctx, connect.NewRequest(&kranev1.DecryptSecretsBlobRequest{ + EncryptedBlob: opts.EncryptedBlob, + EnvironmentId: opts.EnvironmentID, + Token: token, + DeploymentId: opts.DeploymentID, + })) + if err != nil { + return nil, err + } + return resp.Msg.GetEnvVars(), nil + } + + return make(map[string]string), nil +} diff --git a/go/pkg/secrets/provider/provider.go b/go/pkg/secrets/provider/provider.go new file mode 100644 index 0000000000..886a4e99cd --- /dev/null +++ b/go/pkg/secrets/provider/provider.go @@ -0,0 +1,68 @@ +// Package provider defines the interface for secrets providers. +// This abstraction allows unkey-env to fetch secrets from different backends. +package provider + +import ( + "context" + "fmt" +) + +// Type represents a secrets provider type. +type Type string + +const ( + // KraneVault fetches secrets via Krane's SecretsService which decrypts via Vault. + KraneVault Type = "krane-vault" +) + +// Provider fetches secrets for a deployment. +type Provider interface { + // Name returns the provider name for logging/debugging. + Name() string + + // FetchSecrets retrieves decrypted secrets for a deployment. + // Returns a map of environment variable names to their values. + FetchSecrets(ctx context.Context, opts FetchOptions) (map[string]string, error) +} + +// FetchOptions contains parameters for fetching secrets. +type FetchOptions struct { + // DeploymentID is the deployment to fetch secrets for. + DeploymentID string + + // EnvironmentID is the environment (keyring) for decryption. + EnvironmentID string + + // Token is the authentication token. + Token string + + // TokenPath is an optional path to read the token from (e.g., K8s service account token). + // If set, Token is ignored and the token is read from this file. + TokenPath string + + // EncryptedBlob is the encrypted secrets blob from UNKEY_SECRETS_BLOB env var. + // If set, uses DecryptSecretsBlob RPC instead of GetDeploymentSecrets. + EncryptedBlob []byte +} + +// Config holds configuration for creating a provider. +type Config struct { + // Type: KraneVault + Type Type + + // Endpoint is the provider's API endpoint. + Endpoint string +} + +// ErrProviderNotFound is returned when an unknown provider type is requested. +var ErrProviderNotFound = fmt.Errorf("provider not found") + +// New creates a provider based on the config type. +func New(cfg Config) (Provider, error) { + switch cfg.Type { + case KraneVault: + return NewKraneVaultProvider(cfg) + default: + return nil, fmt.Errorf("%w: %s", ErrProviderNotFound, cfg.Type) + } +} diff --git a/go/proto/krane/v1/deployment.proto b/go/proto/krane/v1/deployment.proto index 96b3fca2ff..ef5c1593d8 100644 --- a/go/proto/krane/v1/deployment.proto +++ b/go/proto/krane/v1/deployment.proto @@ -25,9 +25,16 @@ message DeploymentRequest { uint32 cpu_millicores = 5; uint64 memory_size_mib = 6; - // Environment variables to inject into the container. - // Keys are variable names, values are the (decrypted) values. - map env_vars = 7; + // Environment slug (e.g., production, staging). + string environment_slug = 7; + + // Encrypted secrets blob to be decrypted at runtime by unkey-env. + // This is set as UNKEY_SECRETS_BLOB env var in the container. + // unkey-env calls krane's DecryptSecretsBlob RPC to decrypt. + bytes encrypted_secrets_blob = 8; + + // Environment ID for secrets decryption (keyring identifier). + string environment_id = 9; } message CreateDeploymentRequest { diff --git a/go/proto/krane/v1/secrets.proto b/go/proto/krane/v1/secrets.proto new file mode 100644 index 0000000000..2dd58d6de0 --- /dev/null +++ b/go/proto/krane/v1/secrets.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package krane.v1; + +option go_package = "github.com/unkeyed/unkey/go/gen/proto/krane/v1;kranev1"; + +// SecretsService provides decrypted secrets to running workloads. +// Called by the unkey-env binary injected into customer pods/containers. +service SecretsService { + // DecryptSecretsBlob decrypts an encrypted secrets blob passed in the pod spec. + // This avoids DB lookups - the encrypted blob travels with the pod. + // Authentication is via K8s service account token or DB-stored token. + rpc DecryptSecretsBlob(DecryptSecretsBlobRequest) returns (DecryptSecretsBlobResponse); +} + +message DecryptSecretsBlobRequest { + // The encrypted secrets blob from the pod spec (UNKEY_SECRETS_BLOB env var). + // This is the SecretsConfig proto, encrypted with the environment's vault keyring. + bytes encrypted_blob = 1; + + // Environment ID (keyring) to use for decryption. + string environment_id = 2; + + // Token for authentication (K8s service account token or DB-stored token). + string token = 3; + + // Deployment ID for token validation. + string deployment_id = 4; +} + +message DecryptSecretsBlobResponse { + // Decrypted environment variables (key -> plaintext value) + map env_vars = 1; +}