-
Notifications
You must be signed in to change notification settings - Fork 2
fix(security): harden SSH, API keys, and headers #372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ddbbad
4aca5aa
5449696
4b0b89a
7b33864
3e87afa
cacc522
9f9d1b6
a857b3f
8ed8d61
0c4d51d
4683645
b9c1340
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package services | |
|
|
||
| import ( | ||
| "context" | ||
| "crypto/hmac" | ||
| "crypto/rand" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
|
|
@@ -17,6 +18,35 @@ import ( | |
| "github.com/poyrazk/thecloud/internal/platform" | ||
| ) | ||
|
|
||
| // serverSecret is used as HMAC key to prevent rainbow table attacks on API key hashes. | ||
| // This is derived from SECRETS_ENCRYPTION_KEY env var if set, otherwise uses a static value. | ||
| // In production, set SECRETS_ENCRYPTION_KEY for proper security. | ||
| var serverSecret = getServerSecret() | ||
|
|
||
| func getServerSecret() string { | ||
| // Use the secrets encryption key if available, otherwise fall back to a warning string | ||
| // that will be rejected in production | ||
| secret := platform.GetSecretsEncryptionKey() | ||
| if secret != "" { | ||
| return secret | ||
| } | ||
| // Fallback for development - in production this should not be used | ||
| // Log warning to help diagnose configuration issues | ||
| slog.Default().Warn("SECRETS_ENCRYPTION_KEY not set, using development secret for API key hashing - configure for production") | ||
| return "thecloud-development-secret-do-not-use-in-production" | ||
| } | ||
|
|
||
| // computeKeyHash creates a HMAC-SHA256 hash of the API key using the server secret. | ||
| // This prevents rainbow table attacks while maintaining a stable key fingerprint. | ||
| // API keys are machine-generated 32-char hex strings (~128 bits of entropy), | ||
| // but using HMAC adds an additional layer of protection. | ||
| func computeKeyHash(key string) string { | ||
| //nolint:codeql // HMAC-SHA256 is used for key fingerprinting, not password hashing. | ||
| h := hmac.New(sha256.New, []byte(serverSecret)) | ||
| h.Write([]byte(key)) | ||
| return hex.EncodeToString(h.Sum(nil)) | ||
|
Comment on lines
+39
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep a compatibility path for existing API keys before switching the hash format. This changes the persisted Possible rollout shape+func computeLegacyKeyHash(key string) string {
+ sum := sha256.Sum256([]byte(key))
+ return hex.EncodeToString(sum[:])
+}// In ValidateAPIKey():
// 1. Try HMAC hash
// 2. If not found, try legacy SHA-256 hash
// 3. After a legacy hit, rewrite/backfill key_hash to the HMAC value🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // IdentityServiceParams defines the dependencies for IdentityService. | ||
| type IdentityServiceParams struct { | ||
| Repo ports.IdentityRepository | ||
|
|
@@ -222,13 +252,3 @@ func (s *IdentityService) RotateKey(ctx context.Context, userID uuid.UUID, id uu | |
|
|
||
| return newKey, nil | ||
| } | ||
|
|
||
| func computeKeyHash(key string) string { | ||
| //nolint:codeql // SHA-256 is used as a stable key fingerprint, not password hashing. | ||
| // API keys are machine-generated 32-char hex strings (~128 bits of entropy). | ||
| // Using a memory-hard function (argon2/bcrypt) would slow validation | ||
| // unnecessarily and does not improve security for high-entropy keys. | ||
| h := sha256.New() | ||
| h.Write([]byte(key)) | ||
| return hex.EncodeToString(h.Sum(nil)) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package services | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestComputeKeyHash(t *testing.T) { | ||
| // Test that the hash function produces consistent output | ||
| key := "test-api-key-123" | ||
| hash1 := computeKeyHash(key) | ||
| hash2 := computeKeyHash(key) | ||
|
|
||
| // Should be deterministic | ||
| require.NotEmpty(t, hash1) | ||
| assert.Equal(t, hash1, hash2) | ||
|
|
||
| // Different keys should produce different hashes | ||
| differentHash := computeKeyHash("different-key") | ||
| assert.NotEqual(t, hash1, differentHash) | ||
| } | ||
|
|
||
| func TestGetServerSecret(t *testing.T) { | ||
| // Save original value | ||
| origVal := os.Getenv("SECRETS_ENCRYPTION_KEY") | ||
| defer func() { | ||
| if origVal != "" { | ||
| os.Setenv("SECRETS_ENCRYPTION_KEY", origVal) | ||
| } else { | ||
| os.Unsetenv("SECRETS_ENCRYPTION_KEY") | ||
| } | ||
| }() | ||
|
|
||
| t.Run("WithEnvVar", func(t *testing.T) { | ||
| os.Setenv("SECRETS_ENCRYPTION_KEY", "test-secret-key") | ||
| // Re-initialize serverSecret | ||
| serverSecret = getServerSecret() | ||
| assert.Equal(t, "test-secret-key", serverSecret) | ||
| }) | ||
|
|
||
| t.Run("WithoutEnvVar", func(t *testing.T) { | ||
| os.Unsetenv("SECRETS_ENCRYPTION_KEY") | ||
| // Re-initialize serverSecret - will use fallback | ||
| serverSecret = getServerSecret() | ||
| assert.Equal(t, "thecloud-development-secret-do-not-use-in-production", serverSecret) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fail closed on a missing HMAC secret instead of caching a hardcoded global fallback.
If
SECRETS_ENCRYPTION_KEYis unset, every process silently falls back to the same built-in value, and a single misconfigured replica will compute different API key hashes than the rest. Please inject this secret throughIdentityServiceParamsand makeNewIdentityServicereturn an error when it is missing, rather than storing it in a package-global.As per coding guidelines,
**/*.go: Do not use global variables (e.g.,var DB *sql.DB) and Use constructor injection for dependencies instead of global initialization.🤖 Prompt for AI Agents