Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
50e0cbf
feat: add VerticalCollapsibleSteps component for guided user onboarding
JustNZ Aug 5, 2025
919e259
feat: Implement project encryption management
JustNZ Aug 18, 2025
0c21ca5
refactor: Remove encryption settings from flow modals and related fun…
JustNZ Aug 18, 2025
2915383
chore(deps): bump eslint-config-next in /services/frontend
dependabot[bot] Aug 19, 2025
98c9263
docs: Update README and docker-compose for encryption configuration d…
JustNZ Aug 19, 2025
8414bee
chore(deps): bump @radix-ui/react-popover in /services/frontend
dependabot[bot] Aug 19, 2025
559766e
Merge pull request #267 from v1Flows/feature/new_encryption
JustNZ Aug 19, 2025
2970b1a
Merge pull request #268 from v1Flows/dependabot/npm_and_yarn/services…
JustNZ Aug 19, 2025
08d3712
Merge pull request #266 from v1Flows/dependabot/npm_and_yarn/services…
JustNZ Aug 19, 2025
55443f8
chore(deps): bump eslint-plugin-unused-imports in /services/frontend
dependabot[bot] Aug 19, 2025
8dc1dbc
chore(deps): bump the backend group in /services/backend with 2 updates
dependabot[bot] Aug 19, 2025
300f7e6
chore(deps): bump globals from 15.15.0 to 16.3.0 in /services/frontend
dependabot[bot] Aug 19, 2025
bfd260c
chore(deps): bump @react-aria/utils in /services/frontend
dependabot[bot] Aug 19, 2025
b25f123
feat: integrate SWR for data fetching and cache management across com…
JustNZ Aug 7, 2025
45948b9
Merge pull request #271 from v1Flows/chore/new-welcome
JustNZ Aug 19, 2025
6f8c144
Refactor execution and flow components to utilize SWR for data fetchi…
JustNZ Aug 19, 2025
4171bcd
Merge pull request #269 from v1Flows/dependabot/npm_and_yarn/services…
JustNZ Aug 19, 2025
6a70da6
Merge pull request #270 from v1Flows/dependabot/go_modules/services/b…
JustNZ Aug 19, 2025
31364a8
Merge pull request #272 from v1Flows/dependabot/npm_and_yarn/services…
JustNZ Aug 19, 2025
07ce3a8
Merge pull request #273 from v1Flows/dependabot/npm_and_yarn/services…
JustNZ Aug 19, 2025
cb0995f
refactor: replace useRouter with useRefreshCache for improved cache m…
JustNZ Aug 19, 2025
a18ee3c
Merge branch 'develop' into feature/swr
JustNZ Aug 19, 2025
dcd02a4
refactor: remove unused Reloader component from AdminRunnersHeading a…
JustNZ Aug 19, 2025
2b476fd
Merge pull request #275 from v1Flows/feature/swr
JustNZ Aug 19, 2025
4b8f673
feat: add Flow Failure Pipelines component with drag-and-drop functio…
JustNZ Aug 19, 2025
f515945
refactor: streamline FlowHeading and FlowSettings components for impr…
JustNZ Aug 19, 2025
12cd2fa
feat: Update FlowHeading component to remove secondary color from button
JustNZ Aug 21, 2025
78eb4cc
Merge pull request #276 from v1Flows/chore/ui-redesign
JustNZ Aug 21, 2025
60fc9ba
refactor: remove PostCSS configuration and update Tailwind setup
JustNZ Aug 21, 2025
997618f
fix: downgrade @internationalized/date to version 3.7.0 for compatibi…
JustNZ Aug 21, 2025
0bffd0d
Merge pull request #277 from v1Flows/feature/tailwind-v4
JustNZ Aug 21, 2025
da0d5d2
chore(deps): bump recharts from 2.15.1 to 3.1.2 in /services/frontend
dependabot[bot] Aug 21, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ services/backend/*.exe~
services/backend/*.dll
services/backend/*.so
services/backend/*.dylib
services/backend/*.sql

# Test binary, built with `go test -c`
services/backend/*.test
Expand Down
110 changes: 110 additions & 0 deletions ENCRYPTION_SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Secure Project-Based Encryption Setup

## Overview

The enhanced encryption system now uses **key derivation** instead of storing encryption keys directly in the database. This significantly improves security by ensuring that even if someone gains access to your database, they cannot decrypt the data without the master secret.

## How It Works

1. **Master Secret**: A single secret stored outside the database (environment variable, config file, external key management system)
2. **Project Salts**: Random salts generated per project and stored in the database
3. **Key Derivation**: Encryption keys are derived using PBKDF2(master_secret + project_salt)

Even if an attacker gains access to your database, they only see:
- Encrypted data
- Random salts (which are useless without the master secret)

## Configuration

### Option 1: Environment Variable (Recommended for Production)
```bash
# Set the master secret as an environment variable
export EXFLOW_ENCRYPTION_MASTER_SECRET="your-very-long-and-secure-master-secret-here"
```

### Option 2: Configuration File
```yaml
# In your backend config.yaml
encryption:
master_secret: "your-very-long-and-secure-master-secret-here"
# Fallback key for legacy data (optional)
key: "legacy-key-for-backward-compatibility"
```

## Master Secret Requirements

- **Length**: Minimum 32 characters, recommended 64+ characters
- **Randomness**: Use a cryptographically secure random generator
- **Characters**: Include letters, numbers, and symbols
- **Uniqueness**: Must be unique per exFlow installation

### Generate a Secure Master Secret

```bash
# Option 1: Using OpenSSL
openssl rand -base64 64

# Option 2: Using Python
python3 -c "import secrets; print(secrets.token_urlsafe(64))"

# Option 3: Using Go
go run -c "package main; import (\"crypto/rand\", \"encoding/base64\", \"fmt\"); func main() { b := make([]byte, 64); rand.Read(b); fmt.Println(base64.URLEncoding.EncodeToString(b)) }"
```

## Security Benefits

1. **Database Compromise Protection**: Even with full database access, encrypted data remains secure
2. **Per-Project Isolation**: Each project uses a unique derived key
3. **Key Rotation**: Changing the master secret or project salt rotates all encryption
4. **Audit Trail**: Key derivation can be logged and monitored
5. **Compliance**: Meets most regulatory requirements for encryption key management

## Migration from Legacy System

The system maintains backward compatibility:

1. **New Projects**: Automatically use the secure key derivation system
2. **Existing Projects**: Continue working with existing keys until migrated
3. **Gradual Migration**: Projects can be migrated one by one using the key rotation feature

## Best Practices

### Storage
- **Never** store the master secret in the database
- Use environment variables or external key management systems
- Rotate the master secret periodically (quarterly/annually)
- Keep secure backups of the master secret

### Access Control
- Limit access to the master secret to essential personnel only
- Use separate master secrets for different environments (dev/staging/prod)
- Log all access to encryption keys

### Monitoring
- Monitor for unusual encryption/decryption patterns
- Alert on encryption failures
- Regular security audits of key management processes

## Troubleshooting

### "Master secret not configured" Error
1. Ensure the master secret is set in your configuration
2. Restart the backend service after setting the secret
3. Check environment variable spelling

### "Failed to decrypt" Errors
1. Verify the master secret hasn't changed
2. Check if the project salt was corrupted
3. Consider falling back to legacy key mode temporarily

### Performance Considerations
- Key derivation adds ~1-2ms per operation
- Consider caching derived keys in memory for high-throughput scenarios
- Monitor CPU usage during bulk encryption operations

## Example Implementation

```go
// Environment variable
masterSecret := os.Getenv("EXFLOW_ENCRYPTION_MASTER_SECRET")
```
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ To get started with the exFlow project, follow these steps:
password: postgres

encryption:
enabled: true
# maximum 32 characters
key: null
# Minimum 32 characters, recommended 64+ characters
master_secret: "your-very-long-and-secure-master-secret-here"
# Fallback key for legacy data (optional)
key: "legacy-key-for-backward-compatibility"

jwt:
secret: null
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
# BACKEND_DATABASE_PASSWORD: postgres
# BACKEND_ENCRYPTION_ENABLED: "true"
# BACKEND_ENCRYPTION_KEY: "change-me"
# BACKEND_ENCRYPTION_MASTER_SECRET: "change-me"
# BACKEND_JWT_SECRET: "change-me"
entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./exflow-backend --config /etc/exflow/backend_config.yaml & exec node /app/server.js"]

Expand Down
7 changes: 4 additions & 3 deletions services/backend/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ database:
password: postgres

encryption:
enabled: true
# maximum 32 characters
key: null
# Minimum 32 characters, recommended 64+ characters
master_secret: "your-very-long-and-secure-master-secret-here"
# Fallback key for legacy data (optional)
key: "legacy-key-for-backward-compatibility"

jwt:
secret: null
Expand Down
10 changes: 7 additions & 3 deletions services/backend/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ type JWTConf struct {
}

type EncryptionConf struct {
Enabled bool `mapstructure:"enabled" validate:"required"`
Key string `mapstructure:"key"`
Key string `mapstructure:"key"`
MasterSecret string `mapstructure:"master_secret" validate:"required"`
}

type RunnerConf struct {
Expand Down Expand Up @@ -84,8 +84,8 @@ func (cm *ConfigurationManager) LoadConfig(configFile string) error {
"database.name": "BACKEND_DATABASE_NAME",
"database.user": "BACKEND_DATABASE_USER",
"database.password": "BACKEND_DATABASE_PASSWORD",
"encryption.enabled": "BACKEND_ENCRYPTION_ENABLED",
"encryption.key": "BACKEND_ENCRYPTION_KEY",
"encryption.master_secret": "BACKEND_ENCRYPTION_MASTER_SECRET",
"jwt.secret": "BACKEND_JWT_SECRET",
"runner.shared_runner_secret": "BACKEND_RUNNER_SHARED_RUNNER_SECRET",
}
Expand Down Expand Up @@ -118,6 +118,10 @@ func (cm *ConfigurationManager) LoadConfig(configFile string) error {
// Assign to package-level variable for global access
Config = &config

if config.Encryption.MasterSecret == "" {
log.Fatal("Master secret is required for encryption")
}

log.WithFields(log.Fields{
"file": configFile,
"content": cm.viper.AllSettings(),
Expand Down
60 changes: 60 additions & 0 deletions services/backend/database/migrations/10_flows_type_col.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package migrations

import (
"context"
"fmt"

log "github.com/sirupsen/logrus"
"github.com/uptrace/bun"
)

func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return addTypeToFlows(ctx, db)
}, func(ctx context.Context, db *bun.DB) error {
return removeTypeFromFlows(ctx, db)
})
}

func addTypeToFlows(ctx context.Context, db *bun.DB) error {
// add type column
exists, err := columnExists(ctx, db, "flows", "type")
if err != nil {
return fmt.Errorf("failed to check if type column exists: %v", err)
}
if !exists {
_, err := db.NewAddColumn().
Table("flows").
ColumnExpr("type TEXT DEFAULT 'default'").
Exec(ctx)

if err != nil {
return fmt.Errorf("failed to add type column to flows table: %v", err)
}
} else {
log.Debug("type column already exists in flows table")
}

return nil
}

func removeTypeFromFlows(ctx context.Context, db *bun.DB) error {
exists, err := columnExists(ctx, db, "flows", "type")
if err != nil {
return fmt.Errorf("failed to check if type column exists: %v", err)
}
if exists {
_, err := db.NewDropColumn().
Table("flows").
Column("type").
Exec(ctx)

if err != nil {
return fmt.Errorf("failed to remove type column from flows table: %v", err)
}
} else {
log.Debug("type column already removed from flows table")
}

return nil
}
67 changes: 67 additions & 0 deletions services/backend/database/migrations/7_project_encryption_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package migrations

import (
"context"
"crypto/rand"
"encoding/hex"

"github.com/uptrace/bun"
)

func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
// Add encryption_key column to projects table
_, err := db.ExecContext(ctx, "ALTER TABLE projects ADD COLUMN IF NOT EXISTS encryption_key TEXT DEFAULT ''")
if err != nil {
return err
}

// Add encryption_enabled column to projects table
_, err = db.ExecContext(ctx, "ALTER TABLE projects ADD COLUMN IF NOT EXISTS encryption_enabled BOOLEAN DEFAULT true")
if err != nil {
return err
}

// Generate encryption salts for existing projects that don't have them
rows, err := db.QueryContext(ctx, "SELECT id FROM projects WHERE encryption_key = '' OR encryption_key IS NULL")
if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var projectID string
if err := rows.Scan(&projectID); err != nil {
continue // Skip problematic rows, don't fail the entire migration
}

// Generate a new 32-byte salt (not a key!)
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
continue // Skip if salt generation fails
}
hexSalt := hex.EncodeToString(salt)

// Update the project with the new encryption salt
_, err = db.ExecContext(ctx, "UPDATE projects SET encryption_key = $1, encryption_enabled = true WHERE id = $2", hexSalt, projectID)
if err != nil {
continue // Skip if update fails
}
}

return nil
}, func(ctx context.Context, db *bun.DB) error {
// Drop the added columns
_, err := db.ExecContext(ctx, "ALTER TABLE projects DROP COLUMN IF EXISTS encryption_key")
if err != nil {
return err
}

_, err = db.ExecContext(ctx, "ALTER TABLE projects DROP COLUMN IF EXISTS encryption_enabled")
if err != nil {
return err
}

return nil
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package migrations

import (
"context"
"fmt"

log "github.com/sirupsen/logrus"
"github.com/uptrace/bun"
)

func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return addNewEncryptionMigratedToSettings(ctx, db)
}, func(ctx context.Context, db *bun.DB) error {
return removeNewEncryptionMigratedFromSettings(ctx, db)
})
}

func addNewEncryptionMigratedToSettings(ctx context.Context, db *bun.DB) error {
// add new_encryption_migrated column
exists, err := columnExists(ctx, db, "settings", "new_encryption_migrated")
if err != nil {
return fmt.Errorf("failed to check if new_encryption_migrated column exists: %v", err)
}
if !exists {
_, err := db.NewAddColumn().
Table("settings").
ColumnExpr("new_encryption_migrated BOOLEAN DEFAULT FALSE").
Exec(ctx)

if err != nil {
return fmt.Errorf("failed to add new_encryption_migrated column to settings table: %v", err)
}
} else {
log.Debug("new_encryption_migrated column already exists in settings table")
}

return nil
}

func removeNewEncryptionMigratedFromSettings(ctx context.Context, db *bun.DB) error {
exists, err := columnExists(ctx, db, "settings", "new_encryption_migrated")
if err != nil {
return fmt.Errorf("failed to check if new_encryption_migrated column exists: %v", err)
}
if exists {
_, err := db.NewDropColumn().
Table("settings").
Column("new_encryption_migrated").
Exec(ctx)

if err != nil {
return fmt.Errorf("failed to remove new_encryption_migrated column from settings table: %v", err)
}
} else {
log.Debug("new_encryption_migrated column already removed from settings table")
}

return nil
}
Loading
Loading