Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
405ea48
feat: add environment variables db schema and queries
Flo4604 Dec 2, 2025
5b0249e
fix db query
Flo4604 Dec 2, 2025
60f228c
feat: add SecretsConfig proto for encrypted env vars
Flo4604 Dec 2, 2025
ac29218
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 3, 2025
d89e495
feat: dashboard UI for environment variables management
Flo4604 Dec 2, 2025
117e0d4
fix comment and rename file
Flo4604 Dec 2, 2025
8cd90b5
fix file export name
Flo4604 Dec 2, 2025
34ddff9
Remove unnecessary comments from add-env-vars
Flo4604 Dec 2, 2025
4f6534d
add toasts for environment variable operations
Flo4604 Dec 2, 2025
64d9bd6
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 2, 2025
d00a162
fix: add try/catch error handling to env var mutations
Flo4604 Dec 3, 2025
d4c4f50
unfmt file
Flo4604 Dec 3, 2025
8d1c219
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 3, 2025
9adddc0
feat: decrypt env vars in CTRL workflow before passing to Krane
Flo4604 Dec 2, 2025
b071965
feat: inject env vars into pod spec via Krane
Flo4604 Dec 2, 2025
8669905
feat: add customer-workload service account for pod isolation
Flo4604 Dec 2, 2025
e240895
remove gw from k8s manifest, add agent fix ctrl vault for certs
Flo4604 Dec 3, 2025
9fb5cce
seperate master keys too
Flo4604 Dec 3, 2025
dc10509
add inital webhook stuff
Flo4604 Dec 3, 2025
c91d58d
add generated stuff
Flo4604 Dec 3, 2025
40b1986
adjust comments
Flo4604 Dec 3, 2025
ef8e946
use otel lgtm stack in k8s too
Flo4604 Dec 4, 2025
45c13a4
fix some rabbit comments
Flo4604 Dec 4, 2025
d9dea3e
fix some rabbit comments
Flo4604 Dec 4, 2025
92dc1aa
get rid of some unncessary comments
Flo4604 Dec 4, 2025
0374ba7
actually add unkey env cmd gitignores...
Flo4604 Dec 4, 2025
639a942
Merge branch 'main' into feat/better-k8s-injection
Flo4604 Dec 9, 2025
a0248c6
fix golint issues (#4477)
Flo4604 Dec 9, 2025
66ce471
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 9, 2025
fd26690
fix fmt
Flo4604 Dec 9, 2025
9d89744
Merge branch 'main' into feat/better-k8s-injection
Flo4604 Dec 9, 2025
f29396f
linter be happy
Flo4604 Dec 9, 2025
9131760
Merge branch 'main' into feat/better-k8s-injection
chronark Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -154,30 +154,33 @@ 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(
() =>
entries.filter((e) => {
const errors = getErrors(e);
return e.key && e.value && !errors.key && !errors.value;
}),
[entries, getExistingEnvVar],
[entries, getErrors],
);

const handleSave = async () => {
Expand Down
13 changes: 5 additions & 8 deletions apps/dashboard/gen/proto/ctrl/v1/secrets_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<SecretsConfig> =
/*@__PURE__*/
export const SecretsConfigSchema: GenMessage<SecretsConfig> = /*@__PURE__*/
messageDesc(file_ctrl_v1_secrets, 0);

25 changes: 20 additions & 5 deletions apps/dashboard/gen/proto/krane/v1/deployment_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string> 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;
};

/**
Expand Down
96 changes: 96 additions & 0 deletions apps/dashboard/gen/proto/krane/v1/secrets_pb.ts
Original file line number Diff line number Diff line change
@@ -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<DecryptSecretsBlobRequest> = /*@__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<string, string> 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<DecryptSecretsBlobResponse> = /*@__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);

Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const decryptEnvVar = t.procedure
id: true,
value: true,
type: true,
environmentId: true,
},
});

Expand All @@ -53,7 +54,7 @@ export const decryptEnvVar = t.procedure
}

const { plaintext } = await vault.decrypt({
keyring: ctx.workspace.id,
keyring: envVar.environmentId,
encrypted: envVar.value,
});

Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/lib/trpc/routers/deploy/env-vars/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const updateEnvVar = t.procedure
id: true,
type: true,
key: true,
environmentId: true,
},
});

Expand All @@ -59,7 +60,7 @@ export const updateEnvVar = t.procedure
}

const { encrypted } = await vault.encrypt({
keyring: ctx.workspace.id,
keyring: envVar.environmentId,
data: input.value,
});

Expand Down
1 change: 1 addition & 0 deletions deployment/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion go/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
unkey
/unkey
/unkey-env
# Added by goreleaser init:
dist/
10 changes: 10 additions & 0 deletions go/Dockerfile.unkey-env
Original file line number Diff line number Diff line change
@@ -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"]
59 changes: 58 additions & 1 deletion go/Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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')
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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...")
Expand Down Expand Up @@ -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')

Expand Down
Loading