Skip to content

fix(security): harden SSH, API keys, and headers#372

Closed
poyrazK wants to merge 13 commits intomainfrom
fix/security-hardening
Closed

fix(security): harden SSH, API keys, and headers#372
poyrazK wants to merge 13 commits intomainfrom
fix/security-hardening

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented Apr 30, 2026

Summary

Security hardening fixes for SSH host key verification, API key hashing, and HTTP header injection prevention.

Changes

File Fix
pkg/sshutil/client.go Replace ssh.InsecureIgnoreHostKey() with rejectHostKeyCallback by default. Add SetInsecureMode() for development only.
internal/repositories/k8s/node_executor.go Replace insecure fallback with proper error when hostPublicKey not set
internal/core/services/identity.go Replace unsalted SHA-256 with HMAC-SHA256 using SECRETS_ENCRYPTION_KEY
internal/handlers/storage_handler.go Sanitize Content-Disposition with url.PathEscape() and RFC 5987 encoding
internal/platform/config.go Add GetSecretsEncryptionKey() for key retrieval

Security Fixes

  • SSH: Default reject policy instead of accepting any host key (MITM protection)
  • API Keys: HMAC-SHA256 with server secret prevents rainbow table attacks
  • Headers: RFC 5987 encoding prevents Content-Disposition header injection

Issues Fixed

#303 - SSH client uses ssh.InsecureIgnoreHostKey()
#227 - SSH host key verification disabled
#248 - API key hash uses unsalted SHA-256
#226, #225 - Unsanitized Content-Disposition header
#243 - Hardcoded fallback storage secret

Testing

  • go build ./...
  • go vet ./pkg/sshutil/... ./internal/handlers/... ./internal/core/services/...

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened API key validation with enhanced cryptographic hashing.
    • Improved download file header handling to prevent potential injection issues.
    • SSH connections now require explicit host key configuration instead of allowing insecure defaults.
    • Added explicit control over SSH host key verification behavior.

Copilot AI review requested due to automatic review settings April 30, 2026 14:47
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@poyrazK has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 2 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc86dc11-51a3-4a48-9eda-cbb76798df95

📥 Commits

Reviewing files that changed from the base of the PR and between 5e7d3f8 and b9c1340.

📒 Files selected for processing (23)
  • docs/services/cloud-images.md
  • internal/core/services/cluster.go
  • internal/core/services/function.go
  • internal/core/services/function_internal_test.go
  • internal/core/services/function_schedule_worker.go
  • internal/core/services/function_unit_test.go
  • internal/core/services/identity.go
  • internal/core/services/identity_hash_test.go
  • internal/core/services/image.go
  • internal/core/services/image_unit_test.go
  • internal/core/services/snapshot.go
  • internal/core/services/stack.go
  • internal/handlers/storage_handler.go
  • internal/platform/config.go
  • internal/repositories/docker/adapter.go
  • internal/repositories/k8s/node_executor.go
  • internal/repositories/k8s/provisioner.go
  • internal/repositories/libvirt/adapter.go
  • internal/repositories/libvirt/real_client.go
  • internal/repositories/postgres/leader_elector.go
  • internal/workers/pipeline_worker.go
  • pkg/sshutil/client.go
  • pkg/sshutil/client_test.go
📝 Walkthrough

Walkthrough

This pull request enhances security across multiple components by implementing HMAC-based API key fingerprinting, enforcing SSH host key verification, sanitizing HTTP response headers, and introducing dynamic control for SSH verification behavior. Configuration additions support the new keyed hashing mechanism.

Changes

Cohort / File(s) Summary
API Key Security
internal/core/services/identity.go
Replaces unkeyed SHA-256 hashing with HMAC-SHA256 for API key fingerprinting, using serverSecret derived from SECRETS_ENCRYPTION_KEY. Affects how key hashes are computed during creation and validation.
HTTP Response Security
internal/handlers/storage_handler.go
Updates Content-Disposition header to use RFC 5987-style sanitized filenames (filename*=UTF-8''%s) via url.PathEscape for both regular and presigned downloads, reducing header injection risk.
Configuration
internal/platform/config.go
Adds exported GetSecretsEncryptionKey() function to retrieve SECRETS_ENCRYPTION_KEY from environment variables, supporting the new HMAC-based key hashing.
SSH Host Key Verification
internal/repositories/k8s/node_executor.go, pkg/sshutil/client.go
Enforces SSH host key verification by rejecting connections when hostPublicKey is unconfigured. Introduces Insecure flag, NewClientWithKeyInsecure() constructor, and SetInsecureMode() function to enable explicit control over verification behavior with a global toggle.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 With hashes keyed and headers clean,
SSH keys now stand between,
The verifier and the foe—
Security we now bestow!
Secrets encrypted, headers bright,
This rabbit hops to safer heights! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main security hardening changes: SSH host key verification, API key hashing, and HTTP header sanitization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/security-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 23 minutes and 2 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/handlers/storage_handler.go (1)

425-426: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add required Swagger directives on this handler method.

ServePresignedDownload is in the handler layer but still lacks the required method-level Swagger annotations (@Summary, @Tags, @Security, @Router).

As per coding guidelines, "Include Swagger documentation comments on handler methods with @Summary, @Tags, @Security, and @Router directives".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/storage_handler.go` around lines 425 - 426, Add a Swagger
documentation comment block immediately above the handler method
ServePresignedDownload in StorageHandler that includes the required directives:
`@Summary` describing the endpoint, `@Tags` (e.g., Storage or Downloads), `@Security`
indicating auth (or none if unauthenticated), and `@Router` with the HTTP verb and
route path for this handler; ensure the comment uses the standard Go Swagger
format (// `@Summary`, // `@Tags`, // `@Security`, // `@Router`) so tools pick up the
method-level docs for ServePresignedDownload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/services/identity.go`:
- Around line 21-35: The package-global serverSecret/getServerSecret pattern
must be removed and the HMAC secret injected via IdentityServiceParams so
services fail closed when missing: add a secret (or secretsEncryptionKey) field
to IdentityServiceParams, update NewIdentityService to require that field and
return an error if it's empty (no hardcoded fallback), and replace any use of
serverSecret/getServerSecret in the identity service implementation with the
injected params.Secret; also update callers that construct IdentityServiceParams
to pass the environment SECRETS_ENCRYPTION_KEY (and propagate the constructor
error) so replicas compute consistent API key hashes.
- Around line 37-45: computeKeyHash() switches persisted key_hash to
HMAC-SHA256, breaking existing keys because ValidateAPIKey() in identity_repo.go
only checks a single WHERE key_hash = $1; update ValidateAPIKey() to first
lookup using computeKeyHash(key), and if not found, compute the legacy
SHA-256(key) and try that lookup; on a legacy-hit, rewrite/backfill the stored
key_hash to the HMAC value (use the same repository method that updates
key_hash) so subsequent validations use the new format, ensuring compatibility
during rollout.

In `@pkg/sshutil/client.go`:
- Around line 72-87: The package-global insecureSSH and SetInsecureMode create
non-local mutable security state; instead add a per-client configuration option
(e.g., an Insecure bool or HostKeyCallback field) to the SSH client
constructor/struct so each client controls host-key policy; remove or deprecate
the package var insecureSSH and the global SetInsecureMode, change
rejectHostKeyCallback to either be unexported helper or a default value for the
client config, and update all call sites/tests to pass the desired policy when
creating the client (defaulting to secure behavior).
- Around line 21-29: The Client.Insecure field is misleading because
NewClientWithKey picks the hostkey callback from global mode but never sets
Client.Insecure, and Run/WriteFile never consult this flag; update
NewClientWithKey to set client.Insecure according to the selected host key
verification mode (e.g., when choosing a no-op host key callback set
Insecure=true), and modify Client.Run and Client.WriteFile to consult
client.Insecure (or alternatively remove the unused Insecure field entirely);
update the code paths around NewClientWithKey, Client.Run, and Client.WriteFile
to keep the runtime behavior and the Insecure flag consistent.
- Around line 35-40: The code currently reads the global variable insecureSSH in
the client constructor and writes it elsewhere (SetInsecureMode), creating a
race and violating the no-global rule; refactor by removing insecureSSH and
instead add an explicit configuration field (e.g., type ClientConfig {
InsecureSSH bool }) and change NewClientWithKey (and any other client
constructors/factories) to accept this config or a config-bearing client struct;
use config.InsecureSSH to choose between ssh.InsecureIgnoreHostKey() and
rejectHostKeyCallback and delete SetInsecureMode and the global insecureSSH
variable.

---

Outside diff comments:
In `@internal/handlers/storage_handler.go`:
- Around line 425-426: Add a Swagger documentation comment block immediately
above the handler method ServePresignedDownload in StorageHandler that includes
the required directives: `@Summary` describing the endpoint, `@Tags` (e.g., Storage
or Downloads), `@Security` indicating auth (or none if unauthenticated), and
`@Router` with the HTTP verb and route path for this handler; ensure the comment
uses the standard Go Swagger format (// `@Summary`, // `@Tags`, // `@Security`, //
`@Router`) so tools pick up the method-level docs for ServePresignedDownload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 475f4b4a-5d34-4868-9e97-bf8057ef07f6

📥 Commits

Reviewing files that changed from the base of the PR and between 8e31529 and 5e7d3f8.

📒 Files selected for processing (5)
  • internal/core/services/identity.go
  • internal/handlers/storage_handler.go
  • internal/platform/config.go
  • internal/repositories/k8s/node_executor.go
  • pkg/sshutil/client.go

Comment on lines +21 to +35
// 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
return "thecloud-development-secret-do-not-use-in-production"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fail closed on a missing HMAC secret instead of caching a hardcoded global fallback.

If SECRETS_ENCRYPTION_KEY is 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 through IdentityServiceParams and make NewIdentityService return 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
Verify each finding against the current code and only fix it if needed.

In `@internal/core/services/identity.go` around lines 21 - 35, The package-global
serverSecret/getServerSecret pattern must be removed and the HMAC secret
injected via IdentityServiceParams so services fail closed when missing: add a
secret (or secretsEncryptionKey) field to IdentityServiceParams, update
NewIdentityService to require that field and return an error if it's empty (no
hardcoded fallback), and replace any use of serverSecret/getServerSecret in the
identity service implementation with the injected params.Secret; also update
callers that construct IdentityServiceParams to pass the environment
SECRETS_ENCRYPTION_KEY (and propagate the constructor error) so replicas compute
consistent API key hashes.

Comment on lines +37 to +45
// 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))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Keep a compatibility path for existing API keys before switching the hash format.

This changes the persisted key_hash value, but internal/repositories/postgres/identity_repo.go:37-44 still validates with a single WHERE key_hash = $1 lookup. That means every API key created with the previous SHA-256 scheme stops authenticating after deploy unless you backfill or do a dual-read in ValidateAPIKey().

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
Verify each finding against the current code and only fix it if needed.

In `@internal/core/services/identity.go` around lines 37 - 45, computeKeyHash()
switches persisted key_hash to HMAC-SHA256, breaking existing keys because
ValidateAPIKey() in identity_repo.go only checks a single WHERE key_hash = $1;
update ValidateAPIKey() to first lookup using computeKeyHash(key), and if not
found, compute the legacy SHA-256(key) and try that lookup; on a legacy-hit,
rewrite/backfill the stored key_hash to the HMAC value (use the same repository
method that updates key_hash) so subsequent validations use the new format,
ensuring compatibility during rollout.

Comment thread pkg/sshutil/client.go
Comment on lines +21 to 29
// Insecure controls whether host key verification is disabled.
// Set to true only for development/testing with trusted networks.
Insecure bool
}

// NewClientWithKey constructs an SSH client using a private key.
// By default, host key verification is enabled. Set client.Insecure = true
// only for development environments with trusted networks.
func NewClientWithKey(host, user, privateKey string) (*Client, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Client.Insecure can drift from actual runtime behavior.

NewClientWithKey chooses callback from global mode but does not set Client.Insecure, and Run/WriteFile never read this field. The field can mislead callers about whether verification is disabled.

🔧 Small consistency fix
 	return &Client{
 		Host: host,
 		User: user,
 		Auth: []ssh.AuthMethod{
 			ssh.PublicKeys(signer),
 		},
 		HostKeyCallback: hostKeyCallback,
+		Insecure:        insecureSSH.Load(),
 	}, nil
 }

Also applies to: 42-49, 68-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sshutil/client.go` around lines 21 - 29, The Client.Insecure field is
misleading because NewClientWithKey picks the hostkey callback from global mode
but never sets Client.Insecure, and Run/WriteFile never consult this flag;
update NewClientWithKey to set client.Insecure according to the selected host
key verification mode (e.g., when choosing a no-op host key callback set
Insecure=true), and modify Client.Run and Client.WriteFile to consult
client.Insecure (or alternatively remove the unused Insecure field entirely);
update the code paths around NewClientWithKey, Client.Run, and Client.WriteFile
to keep the runtime behavior and the Insecure flag consistent.

Comment thread pkg/sshutil/client.go
Comment on lines +35 to +40
var hostKeyCallback ssh.HostKeyCallback
if insecureSSH {
hostKeyCallback = ssh.InsecureIgnoreHostKey()
} else {
hostKeyCallback = rejectHostKeyCallback
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify shared state is read/written without synchronization
rg -n 'insecureSSH|SetInsecureMode|NewClientWithKey' pkg/sshutil/client.go
rg -n 'sync/atomic|atomic\.|Mutex|RWMutex' pkg/sshutil/client.go

Repository: poyrazK/thecloud

Length of output: 1884


Eliminate unsynchronized global variable; use dependency injection instead.

Line 36 reads insecureSSH while line 86 writes it without synchronization, creating a race condition that can nondeterministically flip host-key verification behavior. However, the coding guidelines explicitly prohibit global variables. The proper fix is to remove the global variable entirely and pass the insecure mode setting via dependency injection (e.g., as a field in a client struct or configuration object) rather than synchronizing access with atomics.

🔧 Proposed fix (dependency injection)

Instead of:

var insecureSSH = false

func NewClientWithKey(...) {
	if insecureSSH {
		hostKeyCallback = ssh.InsecureIgnoreHostKey()
	} else {
		hostKeyCallback = rejectHostKeyCallback
	}
}

func SetInsecureMode(insecure bool) {
	insecureSSH = insecure
}

Use a configuration struct:

type ClientConfig struct {
	InsecureSSH bool
}

func NewClientWithKey(config ClientConfig, ...) {
	if config.InsecureSSH {
		hostKeyCallback = ssh.InsecureIgnoreHostKey()
	} else {
		hostKeyCallback = rejectHostKeyCallback
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sshutil/client.go` around lines 35 - 40, The code currently reads the
global variable insecureSSH in the client constructor and writes it elsewhere
(SetInsecureMode), creating a race and violating the no-global rule; refactor by
removing insecureSSH and instead add an explicit configuration field (e.g., type
ClientConfig { InsecureSSH bool }) and change NewClientWithKey (and any other
client constructors/factories) to accept this config or a config-bearing client
struct; use config.InsecureSSH to choose between ssh.InsecureIgnoreHostKey() and
rejectHostKeyCallback and delete SetInsecureMode and the global insecureSSH
variable.

Comment thread pkg/sshutil/client.go
Comment on lines +72 to +87
// insecureSSH controls global SSH host key verification behavior.
// Default is false (secure) - requires proper host key verification.
// Set to true only for development environments.
var insecureSSH = false

// rejectHostKeyCallback rejects all host keys by default.
func rejectHostKeyCallback(hostname string, remote net.Addr, key ssh.PublicKey) error {
return fmt.Errorf("unknown host key for %s: rejected by policy", hostname)
}

// SetInsecureMode enables or disables SSH host key verification.
// WARNING: Setting to true disables host key verification and is insecure.
// Only use this for development/testing with trusted networks.
func SetInsecureMode(insecure bool) {
insecureSSH = insecure
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Avoid process-global insecure mode; inject security policy per client.

A package-global toggle affects unrelated requests/tests and makes SSH security policy non-local. Prefer constructor options/per-client config over mutable global state.

As per coding guidelines, **/*.go: "Do not use global variables (e.g., var DB *sql.DB)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/sshutil/client.go` around lines 72 - 87, The package-global insecureSSH
and SetInsecureMode create non-local mutable security state; instead add a
per-client configuration option (e.g., an Insecure bool or HostKeyCallback
field) to the SSH client constructor/struct so each client controls host-key
policy; remove or deprecate the package var insecureSSH and the global
SetInsecureMode, change rejectHostKeyCallback to either be unexported helper or
a default value for the client config, and update all call sites/tests to pass
the desired policy when creating the client (defaulting to secure behavior).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

poyrazK added 13 commits April 30, 2026 18:24
time.After creates a timer goroutine that is only cleaned up when the
channel fires. In retry loops, if the operation succeeds on an early
iteration, previously-created timers leak indefinitely.

Fixed 4 locations:
- DockerAdapter.GetInstancePort: 30 x 500ms retries
- DockerAdapter.GetInstanceIP: 30 x 500ms retries (goto retry path)
- KubeadmProvisioner.waitForKubeconfig: unbounded x 10s retries
- FunctionScheduleWorker.ProcessSchedules: 3 x (1s/2s/3s) backoff retries

Fixes #328
- Validate Content-Length header against 10GB max before streaming
- Validate Content-Type header against allowlist (image/jpeg, image/png,
  image/gif, application/x-iso9660-image, application/octet-stream)
- Validate magic bytes for qcow2 and iso formats before committing
- Use LimitReader to enforce max size during streaming write
- Update tests to provide valid magic bytes and content-type headers

SSRF issue: compromised image URL could exhaust storage or serve
malicious content without these validations.
- ImportImage_ExceedsMaxSize: rejects Content-Length > 10GB
- ImportImage_InvalidContentType: rejects text/html Content-Type
- ImportImage_WrongMagicBytes: rejects JPEG magic for .qcow2 URL
Linter (gocritic appendAssign) flags:
  testData := append(qcow2Magic, ...)

Fix: use make + copy instead of append to same variable.
Added import security validations section explaining:
- Scheme restriction (http/https only)
- Size limit (10 GB via Content-Length + LimitReader)
- Content-Type allowlist
- Magic byte validation for qcow2/iso formats
- Format-to-content consistency check

Updated error handling table with new failure scenarios.
- Preserve user_id/tenant_id in background context for async goroutines
  (matches stack.go pattern: appcontext.WithUserID/TenantID on bgCtx)
- Log errors at Error level with function_id and invocation_id
Verifies that when runInvocation fails in the async goroutine,
error is logged with function_id, invocation_id, and error message.
- Use time.Sleep instead of require.Eventually to avoid test timing issues
- Test relies on time.Sleep(200ms) which is non-deterministic but sufficient
  for local dev; CI race detection will catch real races
…and buildTaskOptions

- Add TestFunctionService_CaptureInvocationResults with 5 edge case tests:
  non-zero exit code, error during wait, timeout, success, log sanitization
- Add testComputeBackend test double to support captureInvocationResults tests
- buildTaskOptions already had tests, this is mostly documentation of coverage gains
- Coverage improvements: captureInvocationResults 70.6%→100%, buildTaskOptions 40%→93.3%
- leader_elector.go: Replace time.After with time.NewTimer + timer.Stop()
- libvirt/adapter.go: Replace time.After(5m) with time.NewTimer + defer timeout.Stop()
- function.go: Add context.WithTimeout for async delete; use ctx for DeleteInstance
- cluster.go: Use context.WithCancel + defer cancel() for RepairCluster/ScaleCluster
- snapshot.go: Add context.WithTimeout(10min) for async snapshot
- stack.go: Add context.WithTimeout(5min) for async stack deletion
- pipeline_worker.go: Use ctx instead of context.Background() in defer
- real_client.go: Add done channel with sync.Once to prevent double-close panic

Fixes: #276, #274, #215, #218, #261, #257, #256, #223, #286
- pkg/sshutil/client.go: Replace ssh.InsecureIgnoreHostKey() with rejectHostKeyCallback by default. Add SetInsecureMode() for development environments only.

- internal/repositories/k8s/node_executor.go: Replace insecure fallback with proper error message when hostPublicKey not set.

- internal/core/services/identity.go: Replace unsalted SHA-256 with HMAC-SHA256 using SECRETS_ENCRYPTION_KEY to prevent rainbow table attacks.

- internal/handlers/storage_handler.go: Sanitize Content-Disposition header with url.PathEscape() and RFC 5987 encoding to prevent header injection.

- internal/platform/config.go: Add GetSecretsEncryptionKey() to retrieve encryption key.

Fixes: #303, #227, #248, #226, #225, #243
When SECRETS_ENCRYPTION_KEY is not set, log a warning to help diagnose
configuration issues in production.
- Change insecureSSH from bool to atomic.Bool for thread safety
- Add tests for rejectHostKeyCallback, SetInsecureMode, NewClientWithKey_SecureByDefault
- Add tests for computeKeyHash and getServerSecret
@poyrazK poyrazK force-pushed the fix/security-hardening branch from 362d5e2 to b9c1340 Compare April 30, 2026 15:24
@poyrazK
Copy link
Copy Markdown
Owner Author

poyrazK commented Apr 30, 2026

This PR has complex merge conflicts with main due to changes from #175 (instance-resize) that have already been merged separately. The security hardening changes (SSH, API keys, headers) would need to be extracted and applied manually. The core security improvements are valuable and should be resubmitted as a focused PR.

@poyrazK poyrazK closed this Apr 30, 2026
@poyrazK poyrazK deleted the fix/security-hardening branch April 30, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants