-
Notifications
You must be signed in to change notification settings - Fork 137
feat: dynamic vault OIDC callback port and in-process token capture #910
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
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 |
|---|---|---|
| @@ -1,17 +1,27 @@ | ||
| package utils | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "strings" | ||
| "time" | ||
|
|
||
| ocmutils "github.com/openshift/ocm-container/pkg/utils" | ||
|
|
||
| log "github.com/sirupsen/logrus" | ||
| "github.com/spf13/viper" | ||
| ) | ||
|
|
||
| const ( | ||
| vaultCallbackPortFile = "/tmp/vault_callback_port" | ||
| defaultVaultOIDCPort = "8250" | ||
| vaultLoginTimeout = 5 * time.Minute | ||
| ) | ||
|
|
||
| const ( | ||
| VaultAddrKey string = "vault_address" | ||
| ) | ||
|
|
@@ -41,17 +51,39 @@ func GetVaultRef(vaultPathKey string) (VaultRef, error) { | |
| return VaultRef{Addr: vaultAddr, Path: vaultPath}, nil | ||
| } | ||
|
|
||
| // readFileFunc is the function used to read files. Replaced in tests | ||
| // to avoid filesystem access. | ||
| var readFileFunc = os.ReadFile | ||
|
|
||
| // readCallbackPort reads the dynamically-assigned host port from the | ||
| // portmap file written by ocm-container's ports feature. | ||
| func readCallbackPort() string { | ||
| data, err := readFileFunc(vaultCallbackPortFile) | ||
| if err != nil { | ||
| log.Debugf("could not read vault callback port file: %v", err) | ||
| return "" | ||
| } | ||
| port := strings.TrimSpace(string(data)) | ||
| if port == "" { | ||
| log.Debugf("vault callback port file is empty") | ||
| return "" | ||
| } | ||
| log.Debugf("read vault callback port: %s", port) | ||
| return port | ||
| } | ||
|
|
||
| // setupVaultToken ensures a valid Vault token exists by checking the current | ||
| // token and requesting a new one via OIDC if needed. In container environments, | ||
| // it configures authentication to work without browser auto-launch. | ||
| // it uses the dynamically-assigned callback port from ocm-container's ports | ||
| // feature and falls back to in-process token capture if the token file is | ||
| // not writable. | ||
| func setupVaultToken(vaultAddr string) error { | ||
| err := os.Setenv("VAULT_ADDR", vaultAddr) | ||
| if err != nil { | ||
| return fmt.Errorf("error setting environment variable: %v", err) | ||
| } | ||
|
|
||
| versionCheckCmd := exec.Command("vault", "version") | ||
|
|
||
| versionCheckCmd.Stdout = os.Stderr | ||
| versionCheckCmd.Stderr = os.Stderr | ||
|
|
||
|
|
@@ -62,51 +94,139 @@ func setupVaultToken(vaultAddr string) error { | |
| tokenCheckCmd := exec.Command("vault", "token", "lookup") | ||
| tokenCheckCmd.Stdout = nil | ||
| tokenCheckCmd.Stderr = nil | ||
| // get new token since old token has expired | ||
| if err = tokenCheckCmd.Run(); err != nil { | ||
| log.Infoln("Vault token no longer valid, requesting new token") | ||
|
|
||
| // Check if we're in a container environment (OCM_CONTAINER env var is set) | ||
| // If so, skip automatic browser launch and print the URL for manual authentication | ||
| loginArgs := []string{"login", "-method=oidc", "-no-print"} | ||
| if ocmutils.IsRunningInOcmContainer() { | ||
| log.Infoln("\nNOTE: Running in container mode - OIDC authentication requires port forwarding.") | ||
| log.Infoln("Ensure port 8250 is exposed in your ocm-container configuration:") | ||
| log.Infoln(" Add 'launch-opts: \"-p 8250:8250\"' to ~/.config/ocm-container/ocm-container.yaml") | ||
| log.Infoln("Then restart your ocm-container for the change to take effect.") | ||
|
|
||
| // In container: skip browser launch and listen on all interfaces (0.0.0.0) | ||
| // so the callback can be reached from the host browser via localhost:8250 | ||
| loginArgs = []string{"login", "-method=oidc", "skip_browser=true", "listenaddress=0.0.0.0"} | ||
| return setupVaultTokenContainer() | ||
| } | ||
| loginCmd := exec.Command("vault", loginArgs...) | ||
|
|
||
| // Show output when using skip_browser so user can see the authentication URL | ||
| if ocmutils.IsRunningInOcmContainer() { | ||
| loginCmd.Stdout = os.Stderr | ||
| loginCmd.Stderr = os.Stderr | ||
| } else { | ||
| loginCmd.Stdout = nil | ||
| loginCmd.Stderr = nil | ||
| } | ||
| return setupVaultTokenLocal() | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // setupVaultTokenLocal handles vault OIDC login outside of a container. | ||
| func setupVaultTokenLocal() error { | ||
| ctx, cancel := context.WithTimeout(context.Background(), vaultLoginTimeout) | ||
| defer cancel() | ||
|
|
||
| loginCmd := exec.CommandContext(ctx, "vault", "login", "-method=oidc", "-no-print") | ||
| loginCmd.Stdout = nil | ||
| loginCmd.Stderr = nil | ||
|
|
||
| if err = loginCmd.Run(); err != nil { | ||
| if ocmutils.IsRunningInOcmContainer() { | ||
| return fmt.Errorf("vault login failed: %v\n\n"+ | ||
| "If authentication timed out or the callback failed, this is likely because:\n"+ | ||
| " 1. Port 8250 is not exposed in your ocm-container configuration\n"+ | ||
| " 2. Your ocm-container was not restarted after adding the port\n\n"+ | ||
| "To fix:\n"+ | ||
| " - Add 'launch-opts: \"-p 8250:8250\"' to ~/.config/ocm-container/ocm-container.yaml\n"+ | ||
| " - Exit and restart your ocm-container\n"+ | ||
| " - Try the authentication again", err) | ||
| } | ||
| return fmt.Errorf("error running 'vault login': %v", err) | ||
| if err := loginCmd.Run(); err != nil { | ||
| if ctx.Err() == context.DeadlineExceeded { | ||
| return fmt.Errorf("vault login timed out after %s", vaultLoginTimeout) | ||
| } | ||
| return fmt.Errorf("error running 'vault login': %v", err) | ||
| } | ||
|
|
||
| log.Infoln("Acquired vault token") | ||
| return nil | ||
| } | ||
|
|
||
| // buildOIDCArgs builds the vault login args for container mode, | ||
| // including the dynamic callback port if available. | ||
| func buildOIDCArgs(noStore bool, callbackPort string) []string { | ||
| args := []string{"login", "-method=oidc"} | ||
| if noStore { | ||
| args = append(args, "-no-store", "-field=token") | ||
| } | ||
| args = append(args, "skip_browser=true", "listenaddress=0.0.0.0") | ||
|
|
||
| if callbackPort != "" { | ||
| args = append(args, | ||
| fmt.Sprintf("port=%s", defaultVaultOIDCPort), | ||
| fmt.Sprintf("callbackport=%s", callbackPort), | ||
| ) | ||
| log.Infof("Using dynamic vault OIDC callback port: %s", callbackPort) | ||
| } else { | ||
| log.Infoln("No dynamic callback port found, using default port 8250.") | ||
| log.Infoln("If running multiple containers, ensure ocm-container has the ports feature enabled.") | ||
| } | ||
|
|
||
| return args | ||
| } | ||
|
|
||
| // isTokenFileError checks whether a vault login error is related to | ||
| // writing the token file (e.g., bind mount rename failures). | ||
| func isTokenFileError(err error) bool { | ||
| msg := err.Error() | ||
| return strings.Contains(msg, "rename") || | ||
| strings.Contains(msg, "device or resource busy") || | ||
| strings.Contains(msg, "read-only file system") || | ||
| strings.Contains(msg, "permission denied") | ||
| } | ||
|
|
||
| // setupVaultTokenContainer handles vault OIDC login inside an ocm-container. | ||
| // It first tries a normal vault login that writes ~/.vault-token. If that | ||
| // fails due to a token file write error (e.g., read-only bind mount), it | ||
| // falls back to capturing the token in-process via VAULT_TOKEN. | ||
| func setupVaultTokenContainer() error { | ||
| callbackPort := readCallbackPort() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), vaultLoginTimeout) | ||
| defer cancel() | ||
|
|
||
| log.Infoln("Complete the login via the URL printed below.") | ||
|
|
||
| // First attempt: normal login that writes ~/.vault-token | ||
| loginArgs := buildOIDCArgs(false, callbackPort) | ||
| loginCmd := exec.CommandContext(ctx, "vault", loginArgs...) | ||
| loginCmd.Stdout = os.Stderr | ||
| loginCmd.Stderr = os.Stderr | ||
|
|
||
| if err := loginCmd.Run(); err == nil { | ||
| log.Infoln("Acquired vault token") | ||
| return nil | ||
| } else if ctx.Err() == context.DeadlineExceeded { | ||
| return fmt.Errorf("vault login timed out after %s", vaultLoginTimeout) | ||
| } else if !isTokenFileError(err) { | ||
| return fmt.Errorf("vault login failed: %v\n\n"+ | ||
| "If authentication timed out or the callback failed:\n"+ | ||
|
Member
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. Can you make this less ocm-container focused? Seems like it would be a general UX issue?
Member
Author
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. @dustman9000 Same as the other one: this is an ocm-container only problem & this code isn't reachable by normal |
||
| " 1. Ensure ocm-container has the vault port enabled in the ports feature\n"+ | ||
| " 2. Restart your ocm-container for the change to take effect\n"+ | ||
| " 3. Try the authentication again", err) | ||
| } | ||
|
|
||
| // Token file write failed (bind mount rename issue). Fall back to | ||
| // capturing the token in-process. | ||
| log.Infof("Token file write failed (%v), capturing token in-process instead.", ctx.Err()) | ||
| log.Infoln("Complete the login via the URL printed below.") | ||
|
|
||
| ctx2, cancel2 := context.WithTimeout(context.Background(), vaultLoginTimeout) | ||
| defer cancel2() | ||
|
|
||
| loginArgs = buildOIDCArgs(true, callbackPort) | ||
| loginCmd = exec.CommandContext(ctx2, "vault", loginArgs...) | ||
|
|
||
| var tokenBuf bytes.Buffer | ||
| loginCmd.Stdout = &tokenBuf | ||
| loginCmd.Stderr = os.Stderr | ||
|
|
||
| if err := loginCmd.Run(); err != nil { | ||
| if ctx2.Err() == context.DeadlineExceeded { | ||
| return fmt.Errorf("vault login timed out after %s", vaultLoginTimeout) | ||
| } | ||
| return fmt.Errorf("vault login failed: %v\n\n"+ | ||
| "If authentication timed out or the callback failed:\n"+ | ||
| " 1. Ensure ocm-container has the vault port enabled in the ports feature\n"+ | ||
|
Member
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. Same UX issue here, please make more generic.
Member
Author
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. @dustman9000 this is an ocm-container only problem, and this function is not reachable unless you are in ocm-container. Normal |
||
| " 2. Restart your ocm-container for the change to take effect\n"+ | ||
| " 3. Try the authentication again", err) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| token := strings.TrimSpace(tokenBuf.String()) | ||
| if token == "" { | ||
| return fmt.Errorf("vault login succeeded but returned empty token") | ||
| } | ||
|
|
||
| if err := os.Setenv("VAULT_TOKEN", token); err != nil { | ||
| return fmt.Errorf("error setting VAULT_TOKEN: %v", err) | ||
| } | ||
|
|
||
| log.Infoln("Acquired vault token (in-process)") | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.