Skip to content

Commit fba0b0f

Browse files
committed
improvement(auth): added ability to inject secrets to kubernetes, server-side ff to disable email registration
1 parent 1b22d2c commit fba0b0f

18 files changed

+992
-49
lines changed

apps/sim/lib/auth/auth.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { env } from '@/lib/core/config/env'
4747
import {
4848
isAuthDisabled,
4949
isBillingEnabled,
50+
isEmailPasswordEnabled,
5051
isEmailVerificationEnabled,
5152
isHosted,
5253
isRegistrationDisabled,
@@ -461,6 +462,12 @@ export const auth = betterAuth({
461462
if (ctx.path.startsWith('/sign-up') && isRegistrationDisabled)
462463
throw new Error('Registration is disabled, please contact your admin.')
463464

465+
if (!isEmailPasswordEnabled) {
466+
const emailPasswordPaths = ['/sign-in/email', '/sign-up/email', '/email-otp']
467+
if (emailPasswordPaths.some((path) => ctx.path.startsWith(path)))
468+
throw new Error('Email/password authentication is disabled. Please use SSO to sign in.')
469+
}
470+
464471
if (
465472
(ctx.path.startsWith('/sign-in') || ctx.path.startsWith('/sign-up')) &&
466473
(env.ALLOWED_LOGIN_EMAILS || env.ALLOWED_LOGIN_DOMAINS)

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const env = createEnv({
2020
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
2121
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
2222
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration
23+
EMAIL_PASSWORD_SIGNUP_ENABLED: z.boolean().optional().default(true), // Enable email/password authentication (server-side enforcement)
2324
DISABLE_AUTH: z.boolean().optional(), // Bypass authentication entirely (self-hosted only, creates anonymous session)
2425
ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login
2526
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ if (isTruthy(env.DISABLE_AUTH)) {
6565
*/
6666
export const isRegistrationDisabled = isTruthy(env.DISABLE_REGISTRATION)
6767

68+
/**
69+
* Is email/password authentication enabled (defaults to true)
70+
*/
71+
export const isEmailPasswordEnabled = env.EMAIL_PASSWORD_SIGNUP_ENABLED !== false
72+
6873
/**
6974
* Is Trigger.dev enabled for async job processing
7075
*/

apps/sim/lib/logs/execution/logging-session.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,12 @@ export class LoggingSession {
289289

290290
this.completed = true
291291

292-
// Track workflow execution outcome
292+
// Track workflow execution outcome and create trace spans
293293
if (traceSpans && traceSpans.length > 0) {
294294
try {
295-
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
295+
const { trackPlatformEvent, createOTelSpansForWorkflowExecution } = await import(
296+
'@/lib/core/telemetry'
297+
)
296298

297299
// Determine status from trace spans
298300
const hasErrors = traceSpans.some((span: any) => {
@@ -315,6 +317,20 @@ export class LoggingSession {
315317
'execution.has_errors': hasErrors,
316318
'execution.total_cost': costSummary.totalCost || 0,
317319
})
320+
321+
// Create OpenTelemetry trace spans for the workflow execution
322+
const startTime = new Date(new Date(endTime).getTime() - duration).toISOString()
323+
createOTelSpansForWorkflowExecution({
324+
workflowId: this.workflowId,
325+
workflowName: this.workflowState?.metadata?.name,
326+
executionId: this.executionId,
327+
traceSpans,
328+
trigger: this.triggerType,
329+
startTime,
330+
endTime,
331+
totalDurationMs: duration,
332+
status: hasErrors ? 'error' : 'success',
333+
})
318334
} catch (_e) {
319335
// Silently fail
320336
}
@@ -404,9 +420,11 @@ export class LoggingSession {
404420

405421
this.completed = true
406422

407-
// Track workflow execution error outcome
423+
// Track workflow execution error outcome and create trace spans
408424
try {
409-
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
425+
const { trackPlatformEvent, createOTelSpansForWorkflowExecution } = await import(
426+
'@/lib/core/telemetry'
427+
)
410428
trackPlatformEvent('platform.workflow.executed', {
411429
'workflow.id': this.workflowId,
412430
'execution.duration_ms': Math.max(1, durationMs),
@@ -416,6 +434,20 @@ export class LoggingSession {
416434
'execution.has_errors': true,
417435
'execution.error_message': message,
418436
})
437+
438+
// Create OpenTelemetry trace spans for the workflow execution
439+
createOTelSpansForWorkflowExecution({
440+
workflowId: this.workflowId,
441+
workflowName: this.workflowState?.metadata?.name,
442+
executionId: this.executionId,
443+
traceSpans: spans,
444+
trigger: this.triggerType,
445+
startTime: startTime.toISOString(),
446+
endTime: endTime.toISOString(),
447+
totalDurationMs: Math.max(1, durationMs),
448+
status: 'error',
449+
error: message,
450+
})
419451
} catch (_e) {
420452
// Silently fail
421453
}
@@ -477,7 +509,9 @@ export class LoggingSession {
477509
this.completed = true
478510

479511
try {
480-
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
512+
const { trackPlatformEvent, createOTelSpansForWorkflowExecution } = await import(
513+
'@/lib/core/telemetry'
514+
)
481515
trackPlatformEvent('platform.workflow.executed', {
482516
'workflow.id': this.workflowId,
483517
'execution.duration_ms': Math.max(1, durationMs),
@@ -486,6 +520,22 @@ export class LoggingSession {
486520
'execution.blocks_executed': traceSpans?.length || 0,
487521
'execution.has_errors': false,
488522
})
523+
524+
// Create OpenTelemetry trace spans for the workflow execution
525+
if (traceSpans && traceSpans.length > 0) {
526+
const startTime = new Date(endTime.getTime() - Math.max(1, durationMs))
527+
createOTelSpansForWorkflowExecution({
528+
workflowId: this.workflowId,
529+
workflowName: this.workflowState?.metadata?.name,
530+
executionId: this.executionId,
531+
traceSpans,
532+
trigger: this.triggerType,
533+
startTime: startTime.toISOString(),
534+
endTime: endTime.toISOString(),
535+
totalDurationMs: Math.max(1, durationMs),
536+
status: 'success', // Cancelled executions are not errors
537+
})
538+
}
489539
} catch (_e) {
490540
// Silently fail
491541
}
@@ -540,7 +590,9 @@ export class LoggingSession {
540590
})
541591

542592
try {
543-
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
593+
const { trackPlatformEvent, createOTelSpansForWorkflowExecution } = await import(
594+
'@/lib/core/telemetry'
595+
)
544596
trackPlatformEvent('platform.workflow.executed', {
545597
'workflow.id': this.workflowId,
546598
'execution.duration_ms': Math.max(1, durationMs),
@@ -550,6 +602,22 @@ export class LoggingSession {
550602
'execution.has_errors': false,
551603
'execution.total_cost': costSummary.totalCost || 0,
552604
})
605+
606+
// Create OpenTelemetry trace spans for the workflow execution
607+
if (traceSpans && traceSpans.length > 0) {
608+
const startTime = new Date(endTime.getTime() - Math.max(1, durationMs))
609+
createOTelSpansForWorkflowExecution({
610+
workflowId: this.workflowId,
611+
workflowName: this.workflowState?.metadata?.name,
612+
executionId: this.executionId,
613+
traceSpans,
614+
trigger: this.triggerType,
615+
startTime: startTime.toISOString(),
616+
endTime: endTime.toISOString(),
617+
totalDurationMs: Math.max(1, durationMs),
618+
status: 'success', // Paused executions are not errors
619+
})
620+
}
553621
} catch (_e) {}
554622

555623
if (this.requestId) {

helm/sim/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ The chart includes several pre-configured values files for different scenarios:
3939
| `values-azure.yaml` | Azure AKS optimized | Azure Kubernetes Service |
4040
| `values-aws.yaml` | AWS EKS optimized | Amazon Elastic Kubernetes Service |
4141
| `values-gcp.yaml` | GCP GKE optimized | Google Kubernetes Engine |
42+
| `values-external-secrets.yaml` | External Secrets Operator integration | Using Azure Key Vault, AWS Secrets Manager, Vault |
43+
| `values-existing-secret.yaml` | Pre-existing Kubernetes secrets | GitOps, Sealed Secrets, manual secret management |
4244

4345
### Development Environment
4446

@@ -623,6 +625,111 @@ To uninstall/delete the release:
623625
helm uninstall sim
624626
```
625627

628+
## External Secret Management
629+
630+
The chart supports integration with external secret management systems for production-grade secret handling. This enables you to store secrets in secure vaults and have them automatically synced to Kubernetes.
631+
632+
### Option 1: External Secrets Operator (Recommended)
633+
634+
[External Secrets Operator](https://external-secrets.io/) is the industry-standard solution for syncing secrets from external stores like Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, and GCP Secret Manager.
635+
636+
**Prerequisites:**
637+
```bash
638+
# Install External Secrets Operator
639+
helm repo add external-secrets https://charts.external-secrets.io
640+
helm install external-secrets external-secrets/external-secrets \
641+
-n external-secrets --create-namespace
642+
```
643+
644+
**Configuration:**
645+
```yaml
646+
externalSecrets:
647+
enabled: true
648+
refreshInterval: "1h"
649+
secretStoreRef:
650+
name: "my-secret-store"
651+
kind: "ClusterSecretStore"
652+
remoteRefs:
653+
app:
654+
BETTER_AUTH_SECRET: "sim/app/better-auth-secret"
655+
ENCRYPTION_KEY: "sim/app/encryption-key"
656+
INTERNAL_API_SECRET: "sim/app/internal-api-secret"
657+
postgresql:
658+
password: "sim/postgresql/password"
659+
```
660+
661+
See `examples/values-external-secrets.yaml` for complete examples including SecretStore configurations for Azure, AWS, GCP, and Vault.
662+
663+
### Option 2: Pre-Existing Kubernetes Secrets
664+
665+
Reference secrets you've created manually, via GitOps (Sealed Secrets, SOPS), or through other automation.
666+
667+
**Configuration:**
668+
```yaml
669+
app:
670+
secrets:
671+
existingSecret:
672+
enabled: true
673+
name: "my-app-secrets"
674+
675+
postgresql:
676+
auth:
677+
existingSecret:
678+
enabled: true
679+
name: "my-postgresql-secret"
680+
passwordKey: "POSTGRES_PASSWORD"
681+
682+
externalDatabase:
683+
existingSecret:
684+
enabled: true
685+
name: "my-external-db-secret"
686+
passwordKey: "password"
687+
```
688+
689+
**Create secrets manually:**
690+
```bash
691+
# Generate secure values
692+
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
693+
ENCRYPTION_KEY=$(openssl rand -hex 32)
694+
INTERNAL_API_SECRET=$(openssl rand -hex 32)
695+
POSTGRES_PASSWORD=$(openssl rand -base64 16 | tr -d '/+=')
696+
697+
# Create app secrets
698+
kubectl create secret generic my-app-secrets \
699+
--namespace sim \
700+
--from-literal=BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
701+
--from-literal=ENCRYPTION_KEY="$ENCRYPTION_KEY" \
702+
--from-literal=INTERNAL_API_SECRET="$INTERNAL_API_SECRET"
703+
704+
# Create PostgreSQL secret
705+
kubectl create secret generic my-postgresql-secret \
706+
--namespace sim \
707+
--from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
708+
```
709+
710+
See `examples/values-existing-secret.yaml` for more details.
711+
712+
### External Secrets Parameters
713+
714+
| Parameter | Description | Default |
715+
|-----------|-------------|---------|
716+
| `app.secrets.existingSecret.enabled` | Use existing secret for app credentials | `false` |
717+
| `app.secrets.existingSecret.name` | Name of existing secret | `""` |
718+
| `app.secrets.existingSecret.keys` | Key name mappings | See values.yaml |
719+
| `postgresql.auth.existingSecret.enabled` | Use existing secret for PostgreSQL | `false` |
720+
| `postgresql.auth.existingSecret.name` | Name of existing secret | `""` |
721+
| `postgresql.auth.existingSecret.passwordKey` | Key containing password | `"POSTGRES_PASSWORD"` |
722+
| `externalDatabase.existingSecret.enabled` | Use existing secret for external DB | `false` |
723+
| `externalDatabase.existingSecret.name` | Name of existing secret | `""` |
724+
| `externalDatabase.existingSecret.passwordKey` | Key containing password | `"EXTERNAL_DB_PASSWORD"` |
725+
| `externalSecrets.enabled` | Enable External Secrets Operator integration | `false` |
726+
| `externalSecrets.refreshInterval` | How often to sync secrets | `"1h"` |
727+
| `externalSecrets.secretStoreRef.name` | Name of SecretStore/ClusterSecretStore | `""` |
728+
| `externalSecrets.secretStoreRef.kind` | Kind of store | `"ClusterSecretStore"` |
729+
| `externalSecrets.remoteRefs.app.*` | Remote paths for app secrets | See values.yaml |
730+
| `externalSecrets.remoteRefs.postgresql.password` | Remote path for PostgreSQL password | `""` |
731+
| `externalSecrets.remoteRefs.externalDatabase.password` | Remote path for external DB password | `""` |
732+
626733
## Security Considerations
627734

628735
### Production Secrets

0 commit comments

Comments
 (0)