-
Notifications
You must be signed in to change notification settings - Fork 28
Add optional Redfish session token caching to reduce BMC audit log spam #1146
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
base: main
Are you sure you want to change the base?
Changes from all commits
256e365
4b834e0
c7d1d66
257e401
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 |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package bmc | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "errors" | ||
| "fmt" | ||
| "maps" | ||
| "net/http" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/stmcginnis/gofish" | ||
| "github.com/stmcginnis/gofish/schemas" | ||
| ) | ||
|
|
||
| // SessionCacheKey identifies a cached Redfish session by endpoint and username. | ||
| type SessionCacheKey struct { | ||
| Endpoint string | ||
| Username string | ||
| } | ||
|
|
||
| type sessionCacheEntry struct { | ||
| mu sync.Mutex | ||
| session *gofish.Session | ||
| expiresAt time.Time | ||
| insecureTLS bool | ||
| } | ||
|
|
||
| // SessionCache holds live Redfish session tokens keyed by endpoint+username. | ||
| type SessionCache struct { | ||
| mu sync.Mutex | ||
| entries map[SessionCacheKey]*sessionCacheEntry | ||
| ttl time.Duration | ||
| } | ||
|
|
||
| // NewSessionCache returns a SessionCache with the given idle TTL. | ||
| // Returns an error if ttl is not positive. | ||
| func NewSessionCache(ttl time.Duration) (*SessionCache, error) { | ||
| if ttl <= 0 { | ||
| return nil, fmt.Errorf("bmc: session cache TTL must be positive, got %v", ttl) | ||
| } | ||
| return &SessionCache{ | ||
| entries: make(map[SessionCacheKey]*sessionCacheEntry), | ||
| ttl: ttl, | ||
| }, nil | ||
| } | ||
|
|
||
| // GetOrCreate returns a valid Redfish session for the given options, reusing a | ||
| // cached token if one exists and has not expired. | ||
| func (c *SessionCache) GetOrCreate(ctx context.Context, opts Options) (*gofish.Session, error) { | ||
| key := SessionCacheKey{Endpoint: opts.Endpoint, Username: opts.Username} | ||
|
|
||
| c.mu.Lock() | ||
| entry, ok := c.entries[key] | ||
| if !ok { | ||
| entry = &sessionCacheEntry{} | ||
| c.entries[key] = entry | ||
| } | ||
| c.mu.Unlock() | ||
|
|
||
| entry.mu.Lock() | ||
| defer entry.mu.Unlock() | ||
|
|
||
| if entry.session != nil && time.Now().Before(entry.expiresAt) { | ||
| return entry.session, nil | ||
| } | ||
|
|
||
| session, bmcTTL, err := c.createSession(ctx, opts) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| ttl := c.ttl | ||
| if bmcTTL > 0 && bmcTTL < ttl { | ||
| ttl = bmcTTL | ||
| } | ||
| entry.session = session | ||
| entry.expiresAt = time.Now().Add(ttl) | ||
| entry.insecureTLS = opts.InsecureTLS | ||
| return session, nil | ||
| } | ||
|
|
||
| // Invalidate evicts the cached session for the given key so the next call to | ||
| // GetOrCreate creates a fresh one. | ||
| func (c *SessionCache) Invalidate(key SessionCacheKey) { | ||
| if c == nil { | ||
| return | ||
| } | ||
| c.mu.Lock() | ||
| entry, ok := c.entries[key] | ||
| c.mu.Unlock() | ||
| if !ok { | ||
| return | ||
| } | ||
| entry.mu.Lock() | ||
| entry.session = nil | ||
| entry.expiresAt = time.Time{} | ||
| entry.mu.Unlock() | ||
| } | ||
|
|
||
| // Close deletes all live server-side Redfish sessions and clears the cache. | ||
| // Should be called from the manager shutdown hook. | ||
| func (c *SessionCache) Close() { | ||
| if c == nil { | ||
| return | ||
| } | ||
| c.mu.Lock() | ||
| entries := make(map[SessionCacheKey]*sessionCacheEntry, len(c.entries)) | ||
| maps.Copy(entries, c.entries) | ||
| c.entries = make(map[SessionCacheKey]*sessionCacheEntry) | ||
| c.mu.Unlock() | ||
|
|
||
| for key, entry := range entries { | ||
| entry.mu.Lock() | ||
| sess := entry.session | ||
| insecureTLS := entry.insecureTLS | ||
| entry.session = nil | ||
| entry.mu.Unlock() | ||
|
|
||
| if sess == nil || sess.ID == "" { | ||
| continue | ||
| } | ||
| //nolint:gosec | ||
| httpClient := &http.Client{ | ||
| Timeout: 10 * time.Second, | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureTLS}, | ||
| }, | ||
| } | ||
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodDelete, key.Endpoint+sess.ID, nil) | ||
| if err != nil { | ||
| cancel() | ||
| continue | ||
| } | ||
| req.Header.Set("X-Auth-Token", sess.Token) | ||
|
Comment on lines
+134
to
+139
Contributor
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. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- bmc/session_cache.go ---'
cat -n bmc/session_cache.go | sed -n '1,155p'
printf '%s\n' '--- Options and session-cache construction ---'
rg -n -C 8 'type Options struct|SessionCache|bmcAuthMode|protocolScheme|ProtocolScheme|Endpoint' bmc pkg/bmcutils cmd/main.go --glob '*.go'
printf '%s\n' '--- protocol validation ---'
rg -n -C 10 'protocol|skip-cert-validation|http|https' cmd/main.go pkg/bmcutils bmc --glob '*.go'Repository: ironcore-dev/metal-operator Length of output: 50384 Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information Exploitability: Moderate Reject non-HTTPS endpoints before sending session tokens.
🤖 Prompt for AI Agents |
||
| resp, err := httpClient.Do(req) | ||
| if err == nil { | ||
| _ = resp.Body.Close() | ||
| } | ||
| cancel() | ||
| } | ||
| } | ||
|
|
||
| // createSession establishes a new Redfish session, queries the BMC's advertised | ||
| // SessionTimeout to cap the cache TTL, then drops the transient client without | ||
| // calling Logout (which would immediately delete the session we just created). | ||
| func (c *SessionCache) createSession(ctx context.Context, opts Options) (*gofish.Session, time.Duration, error) { | ||
| client, err := gofish.ConnectContext(ctx, gofish.ClientConfig{ | ||
| Endpoint: opts.Endpoint, | ||
| Username: opts.Username, | ||
| Password: opts.Password, | ||
| Insecure: opts.InsecureTLS, | ||
| }) | ||
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
| session, err := client.GetSession() | ||
| if err != nil { | ||
| client.Logout() | ||
| return nil, 0, err | ||
| } | ||
|
|
||
| var bmcTTL time.Duration | ||
| if ss, err := client.Service.SessionService(); err == nil && ss.SessionTimeout > 0 { | ||
| bmcTTL = time.Duration(ss.SessionTimeout) * time.Second | ||
| } | ||
|
|
||
| client.HTTPClient.CloseIdleConnections() | ||
| return session, bmcTTL, nil | ||
| } | ||
|
|
||
| // IsSessionExpiredError reports whether err indicates the BMC rejected the | ||
| // cached session token. Matches HTTP 401 (Unauthorized) and 403 (Forbidden), | ||
| // which are the most common responses for an expired or revoked token. | ||
| func IsSessionExpiredError(err error) bool { | ||
| if err == nil { | ||
| return false | ||
| } | ||
| var redfishErr *schemas.Error | ||
| if !errors.As(err, &redfishErr) { | ||
| return false | ||
| } | ||
| return redfishErr.HTTPReturnedStatusCode == http.StatusUnauthorized || | ||
| redfishErr.HTTPReturnedStatusCode == http.StatusForbidden | ||
| } | ||
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 32263
SSRF (CWE-918): Server-Side Request Forgery (SSRF)
Reachability: External · Exploitability: Difficult
Disable redirects for shutdown DELETE requests.
If the BMC returns a redirect, prevent the client from sending the DELETE to another host. Set
CheckRedirectto returnhttp.ErrUseLastResponseand add a cross-host redirect test.🧰 Tools
🪛 ast-grep (0.45.2)
[warning] 129-129: MinVersion
is missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.Context: tls.Config{InsecureSkipVerify: insecureTLS}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures
(missing-ssl-minversion-go)
🤖 Prompt for AI Agents
Source: MCP tools
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 11909
🌐 Web query:
Go net/http Client.Do redirect custom headers cross-host 307 308 Authorization Cookie behavior source💡 Result:
In Go's net/http package, the http.Client behavior regarding redirect headers is designed to prevent leaking sensitive information to untrusted domains [1][2]. When following a redirect, the client forwards most headers set on the initial request, with specific exceptions for security [1][3]. Sensitive Header Stripping The client automatically strips specific "sensitive" headers—including Authorization, WWW-Authenticate, Cookie, Cookie2, Proxy-Authorization, and Proxy-Authenticate—when a redirect target is not a subdomain match or an exact match of the initial domain [1][4][5]. For example: - A redirect from foo.com to sub.foo.com will forward these headers [1]. - A redirect from foo.com to bar.com will result in these headers being omitted from the redirected request [1][5]. Cookie Behavior - If a non-nil cookie Jar is used, the client manages cookies automatically [1][2]. During a redirect, if the cookie Jar mutates, the client omits any mutated cookies from the forwarded Cookie header, relying on the Jar to insert the updated values when the redirected request is made [1][2]. - If no Jar is configured, the initial cookies are forwarded as-is, subject to the domain-match security check mentioned above [1][2]. Redirect Methods (307/308) - 307 (Temporary Redirect) and 308 (Permanent Redirect) preserve the original HTTP method and body [2][6]. - These redirects are followed only if the Request.GetBody function is defined, which allows the client to re-send the request body [2][4]. - Note that while 307/308 preserve the method and body, the sensitive headers remain subject to the aforementioned cross-host stripping rules [1][7]. As of current Go versions, while the client protects standard sensitive headers, it does not automatically strip custom authentication headers (e.g., X-Api-Key) or the POST body itself across origins, which has been the subject of ongoing security discussions [7]. Source: The primary implementation logic for header handling is located in the net/http/client.go source file within the standard library, specifically within the logic that determines header copying and security checks during redirection [1][4].
Citations:
🌐 Web query:
site:go.googlesource.com/go go1.26.3 src/net/http/client.go shouldRemoveHeaderOnRedirect💡 Result:
In the Go standard library, specifically within src/net/http/client.go, there is no function named shouldRemoveHeaderOnRedirect. Instead, the logic for handling sensitive headers during HTTP redirects is managed by determining whether to strip them, controlled by a boolean variable stripSensitiveHeaders and a helper function named shouldCopyHeaderOnRedirect [1][2]. When the http.Client follows a redirect, it checks if the redirect target's host is different from the initial request's host [1][2]. If they differ, the client determines whether it is safe to carry over sensitive headers (such as Authorization, WWW-Authenticate, Proxy-Authorization, and Cookie) by calling shouldCopyHeaderOnRedirect [1][2]. This function validates the destination host against the initial host to ensure that sensitive headers are only sent to the same domain or a valid subdomain, preventing the accidental leakage of credentials to third-party domains [1][2]. If shouldCopyHeaderOnRedirect returns false, the client sets stripSensitiveHeaders to true, which subsequently causes these sensitive headers to be removed before the request is resent [1][2]. This mechanism is a security measure designed to protect sensitive user information during cross-domain redirects [1][2]. The implementation has been refined in recent versions to handle complex edge cases, such as preventing incorrect subdomain matching for IPv6 addresses [3][4].
Citations:
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 7692
Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor
Reachability: External · Exploitability: Difficult
Disable redirects before sending
X-Auth-Token. Go 1.26.3 forwards custom headers during cross-host 307 and 308 redirects. A malicious BMC can therefore expose the cached session token. Add a cross-host redirect test for this path.🧰 Tools
🪛 ast-grep (0.45.2)
[warning] 129-129: MinVersion
is missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.Context: tls.Config{InsecureSkipVerify: insecureTLS}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures
(missing-ssl-minversion-go)
🤖 Prompt for AI Agents
Source: MCP tools