Add transparent SSO login and config
diff --git a/auth/login.go b/auth/login.go
index 4d5d82e..c3638a7 100644
--- a/auth/login.go
+++ b/auth/login.go
@@ -19,6 +19,9 @@
 
 import (
 	"bytes"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"flag"
@@ -26,11 +29,14 @@
 	"io"
 	"log"
 	"net/http"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 
 	"github.com/apache/openserverless-cli/config"
+	"github.com/pkg/browser"
 	"github.com/zalando/go-keyring"
 )
 
@@ -40,6 +46,28 @@
 	ApiHost string
 }
 
+type oidcDiscovery struct {
+	TokenEndpoint               string `json:"token_endpoint"`
+	DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
+}
+
+type deviceAuthorizationResponse struct {
+	DeviceCode              string `json:"device_code"`
+	UserCode                string `json:"user_code"`
+	VerificationURI         string `json:"verification_uri"`
+	VerificationURIComplete string `json:"verification_uri_complete"`
+	ExpiresIn               int    `json:"expires_in"`
+	Interval                int    `json:"interval"`
+	Error                   string `json:"error"`
+	ErrorDescription        string `json:"error_description"`
+}
+
+type oidcTokenResponse struct {
+	AccessToken      string `json:"access_token"`
+	Error            string `json:"error"`
+	ErrorDescription string `json:"error_description"`
+}
+
 const usage = `Usage:
 ops -login <apihost> [<user>]
 
@@ -51,8 +79,10 @@
   -h, --help   Show usage`
 
 const whiskLoginPath = "/api/v1/web/whisk-system/nuv/login"
+const oidcLoginPath = "/system/api/v1/auth/oidc"
 const defaultUser = "nuvolaris"
 const opsSecretServiceName = "nuvolaris"
+const oidcTokenEnvOptIn = "OPS_DEV_ALLOW_OIDC_TOKEN_ENV"
 
 func LoginCmd() (*LoginResult, error) {
 
@@ -90,9 +120,9 @@
 	if apihost == "" {
 		apihost = args[0]
 	}
-	url := apihost + whiskLoginPath
-
 	apihost = ensureSchema(apihost)
+	passwordLoginURL := apihost + whiskLoginPath
+	oidcLoginURL := apihost + oidcLoginPath
 
 	// try to get the user from the environment
 	user := os.Getenv("OPS_USER")
@@ -111,28 +141,52 @@
 		}
 	}
 
-	// if still not set, use the default user
-	if user == "" {
-		fmt.Println("Using the default user:", defaultUser)
-		user = defaultUser
+	var creds map[string]string
+	ssoEnabled := isTruthy(os.Getenv("SSO_ENABLED"))
+	oidcToken, err := oidcAccessToken()
+	if err != nil {
+		return nil, err
 	}
-
-	fmt.Println("Logging in", apihost, "as", user)
-
-	password := os.Getenv("OPS_PASSWORD")
-	if password == "" {
-		fmt.Print("Enter Password: ")
-		pwd, err := AskPassword()
+	if oidcToken == "" && ssoEnabled {
+		oidcToken, err = oidcDeviceAccessToken()
 		if err != nil {
 			return nil, err
 		}
-		password = pwd
-		fmt.Println()
 	}
+	if oidcToken != "" {
+		fmt.Println("Logging in", apihost, "with OIDC")
+		creds, err = doOIDCLogin(oidcLoginURL, oidcToken)
+		if err != nil {
+			return nil, err
+		}
+		user = loginFromCredentials(creds, user)
+	} else {
+		if ssoEnabled {
+			return nil, errors.New("SSO is enabled but OIDC login did not return an access token")
+		}
+		// if still not set, use the default user
+		if user == "" {
+			fmt.Println("Using the default user:", defaultUser)
+			user = defaultUser
+		}
 
-	creds, err := doLogin(url, user, password)
-	if err != nil {
-		return nil, err
+		fmt.Println("Logging in", apihost, "as", user)
+
+		password := os.Getenv("OPS_PASSWORD")
+		if password == "" {
+			fmt.Print("Enter Password: ")
+			pwd, err := AskPassword()
+			if err != nil {
+				return nil, err
+			}
+			password = pwd
+			fmt.Println()
+		}
+
+		creds, err = doLogin(passwordLoginURL, user, password)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	if _, ok := creds["AUTH"]; !ok {
@@ -183,6 +237,227 @@
 	}, nil
 }
 
+func oidcAccessToken() (string, error) {
+	allowTokenEnv := isTruthy(os.Getenv(oidcTokenEnvOptIn))
+	for _, name := range []string{"KEYCLOAK_ACCESS_TOKEN", "OPS_OIDC_ACCESS_TOKEN", "OIDC_ACCESS_TOKEN"} {
+		if value := strings.TrimSpace(os.Getenv(name)); value != "" {
+			if !allowTokenEnv {
+				return "", fmt.Errorf("OIDC token login via environment is disabled; use browser/device SSO login or set %s=true only for local tests", oidcTokenEnvOptIn)
+			}
+			return value, nil
+		}
+	}
+
+	stat, err := os.Stdin.Stat()
+	if err != nil || stat.Mode()&os.ModeCharDevice != 0 {
+		return "", nil
+	}
+
+	token, err := io.ReadAll(os.Stdin)
+	if err != nil {
+		return "", nil
+	}
+	tokenText := strings.TrimSpace(string(token))
+	if tokenText != "" && !allowTokenEnv {
+		return "", fmt.Errorf("OIDC token login via stdin is disabled; use browser/device SSO login or set %s=true only for local tests", oidcTokenEnvOptIn)
+	}
+	return tokenText, nil
+}
+
+func oidcDeviceAccessToken() (string, error) {
+	issuer := strings.TrimRight(firstNonEmpty(os.Getenv("SSO_OIDC_ISSUER_URL"), os.Getenv("OIDC_ISSUER_URL")), "/")
+	clientID := firstNonEmpty(os.Getenv("SSO_OIDC_AUDIENCE"), os.Getenv("OIDC_AUDIENCE"))
+	if issuer == "" {
+		return "", errors.New("SSO is enabled but SSO_OIDC_ISSUER_URL is not configured")
+	}
+	if clientID == "" {
+		return "", errors.New("SSO is enabled but SSO_OIDC_AUDIENCE is not configured")
+	}
+
+	discovery, err := fetchOIDCDiscovery(issuer)
+	if err != nil {
+		return "", err
+	}
+	if discovery.DeviceAuthorizationEndpoint == "" {
+		return "", errors.New("OIDC provider does not expose device_authorization_endpoint")
+	}
+	if discovery.TokenEndpoint == "" {
+		return "", errors.New("OIDC provider does not expose token_endpoint")
+	}
+
+	codeVerifier, codeChallenge, err := pkceChallenge()
+	if err != nil {
+		return "", err
+	}
+
+	device, err := startOIDCDeviceAuthorization(discovery.DeviceAuthorizationEndpoint, clientID, codeChallenge)
+	if err != nil {
+		return "", err
+	}
+
+	verificationURL := device.VerificationURIComplete
+	if verificationURL == "" {
+		verificationURL = device.VerificationURI
+	}
+	fmt.Println()
+	fmt.Println("SSO is enabled for this cluster.")
+	fmt.Println("Open this URL in your browser to login:")
+	fmt.Println(verificationURL)
+	if device.UserCode != "" {
+		fmt.Println("Code:", device.UserCode)
+	}
+	fmt.Println("Waiting for authentication...")
+
+	if verificationURL != "" && !isTruthy(os.Getenv("OPS_SSO_DISABLE_BROWSER")) {
+		_ = browser.OpenURL(verificationURL)
+	}
+
+	return pollOIDCDeviceToken(discovery.TokenEndpoint, clientID, device, codeVerifier)
+}
+
+func fetchOIDCDiscovery(issuer string) (*oidcDiscovery, error) {
+	resp, err := http.Get(issuer + "/.well-known/openid-configuration")
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("OIDC discovery failed (%d): %s", resp.StatusCode, string(body))
+	}
+
+	var discovery oidcDiscovery
+	if err := json.NewDecoder(resp.Body).Decode(&discovery); err != nil {
+		return nil, errors.New("failed to decode OIDC discovery response")
+	}
+	return &discovery, nil
+}
+
+func startOIDCDeviceAuthorization(endpoint, clientID, codeChallenge string) (*deviceAuthorizationResponse, error) {
+	form := url.Values{}
+	form.Set("client_id", clientID)
+	form.Set("scope", "openid email profile")
+	form.Set("code_challenge", codeChallenge)
+	form.Set("code_challenge_method", "S256")
+
+	resp, err := http.PostForm(endpoint, form)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	var device deviceAuthorizationResponse
+	if err := json.NewDecoder(resp.Body).Decode(&device); err != nil {
+		return nil, errors.New("failed to decode OIDC device authorization response")
+	}
+	if resp.StatusCode != http.StatusOK {
+		if device.Error != "" {
+			return nil, fmt.Errorf("OIDC device authorization failed (%d): %s: %s", resp.StatusCode, device.Error, device.ErrorDescription)
+		}
+		return nil, fmt.Errorf("OIDC device authorization failed with status code %d", resp.StatusCode)
+	}
+	if device.DeviceCode == "" {
+		return nil, errors.New("OIDC device authorization response missing device_code")
+	}
+	if device.Interval <= 0 {
+		device.Interval = 5
+	}
+	if device.ExpiresIn <= 0 {
+		device.ExpiresIn = 600
+	}
+	return &device, nil
+}
+
+func pollOIDCDeviceToken(tokenEndpoint, clientID string, device *deviceAuthorizationResponse, codeVerifier string) (string, error) {
+	deadline := time.Now().Add(time.Duration(device.ExpiresIn) * time.Second)
+	interval := time.Duration(device.Interval) * time.Second
+
+	for {
+		if time.Now().After(deadline) {
+			return "", errors.New("OIDC device login expired")
+		}
+		time.Sleep(interval)
+
+		form := url.Values{}
+		form.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
+		form.Set("client_id", clientID)
+		form.Set("device_code", device.DeviceCode)
+		form.Set("code_verifier", codeVerifier)
+
+		resp, err := http.PostForm(tokenEndpoint, form)
+		if err != nil {
+			return "", err
+		}
+
+		var token oidcTokenResponse
+		decodeErr := json.NewDecoder(resp.Body).Decode(&token)
+		resp.Body.Close()
+		if decodeErr != nil {
+			return "", errors.New("failed to decode OIDC token response")
+		}
+
+		if resp.StatusCode == http.StatusOK && token.AccessToken != "" {
+			return token.AccessToken, nil
+		}
+
+		switch token.Error {
+		case "authorization_pending":
+			continue
+		case "slow_down":
+			interval += 5 * time.Second
+			continue
+		case "access_denied":
+			return "", errors.New("OIDC device login denied")
+		case "expired_token":
+			return "", errors.New("OIDC device login expired")
+		default:
+			if token.Error != "" {
+				return "", fmt.Errorf("OIDC token polling failed: %s: %s", token.Error, token.ErrorDescription)
+			}
+			return "", fmt.Errorf("OIDC token polling failed with status code %d", resp.StatusCode)
+		}
+	}
+}
+
+func pkceChallenge() (string, string, error) {
+	random := make([]byte, 32)
+	if _, err := rand.Read(random); err != nil {
+		return "", "", err
+	}
+	verifier := base64.RawURLEncoding.EncodeToString(random)
+	sum := sha256.Sum256([]byte(verifier))
+	challenge := base64.RawURLEncoding.EncodeToString(sum[:])
+	return verifier, challenge, nil
+}
+
+func firstNonEmpty(values ...string) string {
+	for _, value := range values {
+		if strings.TrimSpace(value) != "" {
+			return strings.TrimSpace(value)
+		}
+	}
+	return ""
+}
+
+func isTruthy(value string) bool {
+	switch strings.ToLower(strings.TrimSpace(value)) {
+	case "1", "true", "yes", "y", "on":
+		return true
+	default:
+		return false
+	}
+}
+
+func loginFromCredentials(creds map[string]string, fallback string) string {
+	for _, key := range []string{"NAMESPACE", "LOGIN", "USER", "USERNAME"} {
+		if value := strings.TrimSpace(creds[key]); value != "" {
+			return value
+		}
+	}
+	return fallback
+}
+
 func ensureSchema(apihost string) string {
 	if !strings.HasPrefix(apihost, "http://") && !strings.HasPrefix(apihost, "https://") {
 		if apihost == "localhost" {
@@ -227,6 +502,54 @@
 	return creds, nil
 }
 
+func doOIDCLogin(url, accessToken string) (map[string]string, error) {
+	token := strings.TrimSpace(accessToken)
+	if token == "" {
+		return nil, errors.New("missing OIDC access token")
+	}
+
+	data := map[string]string{
+		"access_token": strings.TrimPrefix(token, "Bearer "),
+	}
+	loginJson, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(loginJson))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	if strings.HasPrefix(token, "Bearer ") {
+		req.Header.Set("Authorization", token)
+	} else {
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return nil, fmt.Errorf("OIDC login failed with status code %d", resp.StatusCode)
+		}
+		return nil, fmt.Errorf("OIDC login failed (%d): %s", resp.StatusCode, string(body))
+	}
+
+	var creds map[string]string
+	err = json.NewDecoder(resp.Body).Decode(&creds)
+	if err != nil {
+		return nil, errors.New("failed to decode response from OIDC login request")
+	}
+
+	return creds, nil
+}
+
 func storeCredentials(creds map[string]string) error {
 	for k, v := range creds {
 		err := keyring.Set(opsSecretServiceName, k, v)
diff --git a/auth/login_test.go b/auth/login_test.go
index a47daab..ea7c1d8 100644
--- a/auth/login_test.go
+++ b/auth/login_test.go
@@ -19,6 +19,7 @@
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -183,6 +184,134 @@
 		require.Equal(t, "missing AUTH token from login response", err.Error())
 		require.Nil(t, loginResult)
 	})
+
+	t.Run("with OIDC access token calls admin-api bridge", func(t *testing.T) {
+		tmpDir := t.TempDir()
+		t.Setenv("OPS_HOME", tmpDir)
+		t.Setenv("KEYCLOAK_ACCESS_TOKEN", "oidc-token")
+		t.Setenv("OPS_DEV_ALLOW_OIDC_TOKEN_ENV", "true")
+		t.Setenv("OPS_PASSWORD", "")
+		t.Setenv("OPS_USER", "")
+		t.Setenv("OPS_APIHOST", "")
+
+		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, "/system/api/v1/auth/oidc", r.URL.Path)
+			require.Equal(t, "Bearer oidc-token", r.Header.Get("Authorization"))
+
+			var requestBody map[string]string
+			require.NoError(t, json.NewDecoder(r.Body).Decode(&requestBody))
+			require.Equal(t, "oidc-token", requestBody["access_token"])
+
+			_, _ = w.Write([]byte(`{"AUTH":"oidc-auth","NAMESPACE":"developerlab8e8a9915"}`))
+		}))
+		defer mockServer.Close()
+
+		os.Args = []string{"login", mockServer.URL}
+		loginResult, err := LoginCmd()
+		require.NoError(t, err)
+		require.NotNil(t, loginResult)
+		require.Equal(t, "developerlab8e8a9915", loginResult.Login)
+		require.Equal(t, "oidc-auth", loginResult.Auth)
+
+		configMap, err := config.NewConfigMapBuilder().
+			WithConfigJson(filepath.Join(tmpDir, "config.json")).
+			Build()
+		require.NoError(t, err)
+
+		v, err := configMap.Get("STATUS_LOGGED_USER")
+		require.NoError(t, err)
+		require.Equal(t, "developerlab8e8a9915", v)
+	})
+
+	t.Run("rejects OIDC access token from env without explicit dev opt-in", func(t *testing.T) {
+		tmpDir := t.TempDir()
+		t.Setenv("OPS_HOME", tmpDir)
+		t.Setenv("KEYCLOAK_ACCESS_TOKEN", "oidc-token")
+		t.Setenv("OPS_DEV_ALLOW_OIDC_TOKEN_ENV", "")
+		t.Setenv("OPS_PASSWORD", "")
+		t.Setenv("OPS_USER", "")
+		t.Setenv("OPS_APIHOST", "")
+
+		os.Args = []string{"login", "http://localhost:5000"}
+		loginResult, err := LoginCmd()
+		require.Error(t, err)
+		require.Nil(t, loginResult)
+		require.Contains(t, err.Error(), "OIDC token login via environment is disabled")
+	})
+
+	t.Run("SSO enabled starts OIDC device flow", func(t *testing.T) {
+		tmpDir := t.TempDir()
+		t.Setenv("OPS_HOME", tmpDir)
+		t.Setenv("SSO_ENABLED", "true")
+		t.Setenv("SSO_OIDC_AUDIENCE", "openserverless-admin-api")
+		t.Setenv("OPS_SSO_DISABLE_BROWSER", "true")
+		t.Setenv("OPS_PASSWORD", "")
+		t.Setenv("OPS_USER", "")
+		t.Setenv("OPS_APIHOST", "")
+
+		var mockServer *httptest.Server
+		mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			switch r.URL.Path {
+			case "/realms/lab/.well-known/openid-configuration":
+				_, _ = w.Write([]byte(fmt.Sprintf(`{
+					"device_authorization_endpoint": "%s/realms/lab/protocol/openid-connect/auth/device",
+					"token_endpoint": "%s/realms/lab/protocol/openid-connect/token"
+				}`, mockServer.URL, mockServer.URL)))
+			case "/realms/lab/protocol/openid-connect/auth/device":
+				require.NoError(t, r.ParseForm())
+				require.Equal(t, "openserverless-admin-api", r.Form.Get("client_id"))
+				require.Equal(t, "S256", r.Form.Get("code_challenge_method"))
+				require.NotEmpty(t, r.Form.Get("code_challenge"))
+				_, _ = w.Write([]byte(`{
+					"device_code": "device-code",
+					"user_code": "ABCD-EFGH",
+					"verification_uri": "http://localhost/device",
+					"verification_uri_complete": "http://localhost/device?user_code=ABCD-EFGH",
+					"expires_in": 10,
+					"interval": 1
+				}`))
+			case "/realms/lab/protocol/openid-connect/token":
+				require.NoError(t, r.ParseForm())
+				require.Equal(t, "urn:ietf:params:oauth:grant-type:device_code", r.Form.Get("grant_type"))
+				require.Equal(t, "openserverless-admin-api", r.Form.Get("client_id"))
+				require.Equal(t, "device-code", r.Form.Get("device_code"))
+				require.NotEmpty(t, r.Form.Get("code_verifier"))
+				_, _ = w.Write([]byte(`{"access_token":"device-access-token"}`))
+			case "/system/api/v1/auth/oidc":
+				require.Equal(t, "Bearer device-access-token", r.Header.Get("Authorization"))
+				_, _ = w.Write([]byte(`{"AUTH":"oidc-auth","NAMESPACE":"michelem"}`))
+			default:
+				http.NotFound(w, r)
+			}
+		}))
+		defer mockServer.Close()
+
+		t.Setenv("SSO_OIDC_ISSUER_URL", mockServer.URL+"/realms/lab")
+
+		os.Args = []string{"login", mockServer.URL}
+		loginResult, err := LoginCmd()
+		require.NoError(t, err)
+		require.NotNil(t, loginResult)
+		require.Equal(t, "michelem", loginResult.Login)
+		require.Equal(t, "oidc-auth", loginResult.Auth)
+	})
+
+	t.Run("SSO enabled fails without issuer", func(t *testing.T) {
+		tmpDir := t.TempDir()
+		t.Setenv("OPS_HOME", tmpDir)
+		t.Setenv("SSO_ENABLED", "true")
+		t.Setenv("SSO_OIDC_ISSUER_URL", "")
+		t.Setenv("SSO_OIDC_AUDIENCE", "openserverless-admin-api")
+		t.Setenv("OPS_PASSWORD", "")
+		t.Setenv("OPS_USER", "")
+		t.Setenv("OPS_APIHOST", "")
+
+		os.Args = []string{"login", "http://localhost:5000"}
+		loginResult, err := LoginCmd()
+		require.Error(t, err)
+		require.Nil(t, loginResult)
+		require.Contains(t, err.Error(), "SSO_OIDC_ISSUER_URL")
+	})
 }
 
 func Test_doLogin(t *testing.T) {
@@ -195,6 +324,26 @@
 	require.Equal(t, "test", cred["fakeCred"])
 }
 
+func Test_doOIDCLogin(t *testing.T) {
+	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, "/system/api/v1/auth/oidc", r.URL.Path)
+		require.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
+
+		var requestBody map[string]string
+		require.NoError(t, json.NewDecoder(r.Body).Decode(&requestBody))
+		require.Equal(t, "test-token", requestBody["access_token"])
+
+		_, _ = w.Write([]byte(`{"AUTH":"test-auth","NAMESPACE":"devel"}`))
+	}))
+	defer mockServer.Close()
+
+	cred, err := doOIDCLogin(mockServer.URL+"/system/api/v1/auth/oidc", "test-token")
+	require.NoError(t, err)
+	require.NotNil(t, cred)
+	require.Equal(t, "test-auth", cred["AUTH"])
+	require.Equal(t, "devel", cred["NAMESPACE"])
+}
+
 func Test_storeCredentials(t *testing.T) {
 	keyring.MockInit()
 
diff --git a/config/config_tool.go b/config/config_tool.go
index 46f9ff8..1b4758a 100644
--- a/config/config_tool.go
+++ b/config/config_tool.go
@@ -81,6 +81,10 @@
 		return nil
 	}
 
+	if input[0] == "sso" {
+		return ConfigSSOTool(configMap, input[1:])
+	}
+
 	var cErr error
 	noAssigns := inputWithoutAssigns(input)
 
diff --git a/config/sso_tool.go b/config/sso_tool.go
new file mode 100644
index 0000000..3a60324
--- /dev/null
+++ b/config/sso_tool.go
@@ -0,0 +1,435 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package config
+
+import (
+	"bytes"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"os"
+	"os/exec"
+	"sort"
+	"strings"
+)
+
+const (
+	defaultSSONamespace = "nuvolaris"
+	defaultSSOConfigMap = "openserverless-sso-config"
+	defaultSSOWorkload  = "nuvolaris-system-api"
+	defaultSSOContainer = "nuvolaris-system-api"
+)
+
+type commandRunner func(name string, args []string, stdin []byte) ([]byte, error)
+
+var runSSOCommand commandRunner = realCommandRunner
+
+type ssoOptions struct {
+	IssuerURL              string
+	JWKSURL                string
+	Audience               string
+	RequiredGroup          string
+	UsernameClaim          string
+	GroupsClaim            string
+	AutoProvision          bool
+	AutoProvisionTimeout   string
+	AutoProvisionPoll      string
+	AutoProvisionServices  string
+	NamespacePreserveValid bool
+	NamespaceHashLength    string
+	NamespaceMaxLength     string
+	Namespace              string
+	ConfigMapName          string
+	WorkloadName           string
+	ContainerName          string
+	NoRollout              bool
+}
+
+func printSSOUsage() {
+	fmt.Print(`Usage:
+ops config sso keycloak --enable --issuer-url URL --jwks-url URL --audience AUDIENCE --required-group GROUP [options]
+ops config sso show
+ops config sso disable [options]
+
+Legacy embedded form:
+ops -config sso keycloak --enable --issuer-url URL --jwks-url URL --audience AUDIENCE --required-group GROUP [options]
+ops -config sso show
+ops -config sso disable [options]
+
+Configure OpenServerless SSO/OIDC integration for admin-api.
+
+Options:
+  --username-claim CLAIM   OIDC username claim. Default: preferred_username
+  --groups-claim CLAIM     OIDC groups claim. Default: groups
+  --namespace NS           Kubernetes namespace. Default: nuvolaris
+  --configmap NAME         Kubernetes ConfigMap name. Default: openserverless-sso-config
+  --statefulset NAME       admin-api StatefulSet name. Default: nuvolaris-system-api
+  --container NAME         admin-api container name. Default: nuvolaris-system-api
+  --no-rollout             Do not restart or wait for admin-api rollout
+`)
+}
+
+func ConfigSSOTool(configMap ConfigMap, args []string) error {
+	if len(args) == 0 {
+		printSSOUsage()
+		return nil
+	}
+
+	switch args[0] {
+	case "keycloak":
+		return configureKeycloakSSO(configMap, args[1:])
+	case "show":
+		printSSOConfig(configMap)
+		return nil
+	case "disable":
+		return disableSSO(configMap, args[1:])
+	case "-h", "--help", "help":
+		printSSOUsage()
+		return nil
+	default:
+		return fmt.Errorf("unknown sso command: %s", args[0])
+	}
+}
+
+func configureKeycloakSSO(configMap ConfigMap, args []string) error {
+	opts, err := parseKeycloakSSOArgs(args)
+	if err != nil {
+		return err
+	}
+
+	if err := saveSSOConfig(configMap, opts); err != nil {
+		return err
+	}
+
+	if err := applySSOConfigMap(opts); err != nil {
+		return err
+	}
+
+	if err := patchSSOEnvFrom(opts); err != nil {
+		return err
+	}
+
+	if !opts.NoRollout {
+		if err := rolloutSSOWorkload(opts); err != nil {
+			return err
+		}
+	}
+
+	fmt.Println("SSO configuration applied to admin-api.")
+	fmt.Printf("ConfigMap: %s/%s\n", opts.Namespace, opts.ConfigMapName)
+	return nil
+}
+
+func parseKeycloakSSOArgs(args []string) (ssoOptions, error) {
+	opts := ssoOptions{
+		UsernameClaim:          "preferred_username",
+		GroupsClaim:            "groups",
+		AutoProvision:          true,
+		AutoProvisionTimeout:   "120",
+		AutoProvisionPoll:      "2",
+		AutoProvisionServices:  "all",
+		NamespacePreserveValid: true,
+		NamespaceHashLength:    "8",
+		NamespaceMaxLength:     "61",
+		Namespace:              defaultSSONamespace,
+		ConfigMapName:          defaultSSOConfigMap,
+		WorkloadName:           defaultSSOWorkload,
+		ContainerName:          defaultSSOContainer,
+	}
+
+	flags := flag.NewFlagSet("sso keycloak", flag.ContinueOnError)
+	flags.SetOutput(os.Stderr)
+	enable := flags.Bool("enable", false, "enable SSO")
+	flags.StringVar(&opts.IssuerURL, "issuer-url", "", "OIDC issuer URL")
+	flags.StringVar(&opts.JWKSURL, "jwks-url", "", "OIDC JWKS URL")
+	flags.StringVar(&opts.Audience, "audience", "", "OIDC audience")
+	flags.StringVar(&opts.RequiredGroup, "required-group", "", "required OIDC group")
+	flags.StringVar(&opts.UsernameClaim, "username-claim", opts.UsernameClaim, "OIDC username claim")
+	flags.StringVar(&opts.GroupsClaim, "groups-claim", opts.GroupsClaim, "OIDC groups claim")
+	flags.StringVar(&opts.Namespace, "namespace", opts.Namespace, "Kubernetes namespace")
+	flags.StringVar(&opts.ConfigMapName, "configmap", opts.ConfigMapName, "Kubernetes ConfigMap name")
+	flags.StringVar(&opts.WorkloadName, "statefulset", opts.WorkloadName, "admin-api StatefulSet name")
+	flags.StringVar(&opts.ContainerName, "container", opts.ContainerName, "admin-api container name")
+	flags.BoolVar(&opts.NoRollout, "no-rollout", false, "skip rollout restart/status")
+
+	if err := flags.Parse(args); err != nil {
+		return opts, err
+	}
+	if !*enable {
+		return opts, fmt.Errorf("missing --enable")
+	}
+	if flags.NArg() > 0 {
+		return opts, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
+	}
+	if opts.IssuerURL == "" {
+		return opts, fmt.Errorf("missing --issuer-url")
+	}
+	if opts.JWKSURL == "" {
+		return opts, fmt.Errorf("missing --jwks-url")
+	}
+	if opts.Audience == "" {
+		return opts, fmt.Errorf("missing --audience")
+	}
+	if opts.RequiredGroup == "" {
+		return opts, fmt.Errorf("missing --required-group")
+	}
+	if opts.UsernameClaim == "" {
+		return opts, fmt.Errorf("missing --username-claim")
+	}
+	if opts.GroupsClaim == "" {
+		return opts, fmt.Errorf("missing --groups-claim")
+	}
+	return opts, nil
+}
+
+func parseDisableSSOArgs(args []string) (ssoOptions, error) {
+	opts := ssoOptions{
+		Namespace:     defaultSSONamespace,
+		ConfigMapName: defaultSSOConfigMap,
+		WorkloadName:  defaultSSOWorkload,
+		ContainerName: defaultSSOContainer,
+	}
+
+	flags := flag.NewFlagSet("sso disable", flag.ContinueOnError)
+	flags.SetOutput(os.Stderr)
+	flags.StringVar(&opts.Namespace, "namespace", opts.Namespace, "Kubernetes namespace")
+	flags.StringVar(&opts.ConfigMapName, "configmap", opts.ConfigMapName, "Kubernetes ConfigMap name")
+	flags.StringVar(&opts.WorkloadName, "statefulset", opts.WorkloadName, "admin-api StatefulSet name")
+	flags.StringVar(&opts.ContainerName, "container", opts.ContainerName, "admin-api container name")
+	flags.BoolVar(&opts.NoRollout, "no-rollout", false, "skip rollout restart/status")
+
+	if err := flags.Parse(args); err != nil {
+		return opts, err
+	}
+	if flags.NArg() > 0 {
+		return opts, fmt.Errorf("unexpected arguments: %s", strings.Join(flags.Args(), " "))
+	}
+	return opts, nil
+}
+
+func saveSSOConfig(configMap ConfigMap, opts ssoOptions) error {
+	values := map[string]string{
+		"SSO_ENABLED":                        "true",
+		"SSO_PROVIDER":                       "keycloak",
+		"SSO_OIDC_ISSUER_URL":                opts.IssuerURL,
+		"SSO_OIDC_JWKS_URL":                  opts.JWKSURL,
+		"SSO_OIDC_AUDIENCE":                  opts.Audience,
+		"SSO_OIDC_REQUIRED_GROUP":            opts.RequiredGroup,
+		"SSO_OIDC_USERNAME_CLAIM":            opts.UsernameClaim,
+		"SSO_OIDC_GROUPS_CLAIM":              opts.GroupsClaim,
+		"SSO_AUTOPROVISION_ON_LOGIN":         fmt.Sprintf("%t", opts.AutoProvision),
+		"SSO_AUTOPROVISION_TIMEOUT_SECONDS":  opts.AutoProvisionTimeout,
+		"SSO_AUTOPROVISION_POLL_SECONDS":     opts.AutoProvisionPoll,
+		"SSO_AUTOPROVISION_DEFAULT_SERVICES": opts.AutoProvisionServices,
+		"SSO_NAMESPACE_PRESERVE_VALID":       fmt.Sprintf("%t", opts.NamespacePreserveValid),
+		"SSO_NAMESPACE_HASH_LENGTH":          opts.NamespaceHashLength,
+		"SSO_NAMESPACE_MAX_LENGTH":           opts.NamespaceMaxLength,
+		"SSO_KUBE_NAMESPACE":                 opts.Namespace,
+		"SSO_KUBE_CONFIGMAP":                 opts.ConfigMapName,
+		"SSO_KUBE_STATEFULSET":               opts.WorkloadName,
+		"SSO_KUBE_CONTAINER":                 opts.ContainerName,
+	}
+	for key, value := range values {
+		if err := configMap.Insert(key, value); err != nil {
+			return err
+		}
+	}
+	return configMap.SaveConfig()
+}
+
+func printSSOConfig(configMap ConfigMap) {
+	values := configMap.Flatten()
+	keys := make([]string, 0)
+	for key := range values {
+		if strings.HasPrefix(key, "SSO_") {
+			keys = append(keys, key)
+		}
+	}
+	sort.Strings(keys)
+	for _, key := range keys {
+		fmt.Printf("%s=%s\n", key, values[key])
+	}
+}
+
+func disableSSO(configMap ConfigMap, args []string) error {
+	opts, err := parseDisableSSOArgs(args)
+	if err != nil {
+		return err
+	}
+
+	if err := removeLocalSSOConfig(configMap); err != nil {
+		return err
+	}
+	if err := removeSSOEnvFrom(opts); err != nil {
+		return err
+	}
+	if err := deleteSSOConfigMap(opts); err != nil {
+		return err
+	}
+	if !opts.NoRollout {
+		if err := rolloutSSOWorkload(opts); err != nil {
+			return err
+		}
+	}
+
+	fmt.Println("SSO configuration disabled for admin-api.")
+	return nil
+}
+
+func removeLocalSSOConfig(configMap ConfigMap) error {
+	for key := range configMap.Flatten() {
+		if !strings.HasPrefix(key, "SSO_") {
+			continue
+		}
+		if err := configMap.Delete(key); err != nil && !strings.Contains(err.Error(), "does not exist in config.json") {
+			return err
+		}
+	}
+	return configMap.SaveConfig()
+}
+
+func applySSOConfigMap(opts ssoOptions) error {
+	data := map[string]string{
+		"OIDC_ISSUER_URL":                    opts.IssuerURL,
+		"OIDC_JWKS_URL":                      opts.JWKSURL,
+		"OIDC_AUDIENCE":                      opts.Audience,
+		"OIDC_REQUIRED_GROUP":                opts.RequiredGroup,
+		"OIDC_USERNAME_CLAIM":                opts.UsernameClaim,
+		"OIDC_GROUPS_CLAIM":                  opts.GroupsClaim,
+		"SSO_AUTOPROVISION_ON_LOGIN":         fmt.Sprintf("%t", opts.AutoProvision),
+		"SSO_AUTOPROVISION_TIMEOUT_SECONDS":  opts.AutoProvisionTimeout,
+		"SSO_AUTOPROVISION_POLL_SECONDS":     opts.AutoProvisionPoll,
+		"SSO_AUTOPROVISION_DEFAULT_SERVICES": opts.AutoProvisionServices,
+		"SSO_NAMESPACE_PRESERVE_VALID":       fmt.Sprintf("%t", opts.NamespacePreserveValid),
+		"SSO_NAMESPACE_HASH_LENGTH":          opts.NamespaceHashLength,
+		"SSO_NAMESPACE_MAX_LENGTH":           opts.NamespaceMaxLength,
+	}
+	obj := map[string]interface{}{
+		"apiVersion": "v1",
+		"kind":       "ConfigMap",
+		"metadata": map[string]string{
+			"name":      opts.ConfigMapName,
+			"namespace": opts.Namespace,
+		},
+		"data": data,
+	}
+	payload, err := json.Marshal(obj)
+	if err != nil {
+		return err
+	}
+	_, err = runSSOCommand("kubectl", []string{"apply", "-f", "-"}, payload)
+	return err
+}
+
+func patchSSOEnvFrom(opts ssoOptions) error {
+	patch := map[string]interface{}{
+		"spec": map[string]interface{}{
+			"template": map[string]interface{}{
+				"spec": map[string]interface{}{
+					"containers": []map[string]interface{}{
+						{
+							"name": opts.ContainerName,
+							"envFrom": []map[string]interface{}{
+								{
+									"configMapRef": map[string]string{
+										"name": opts.ConfigMapName,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	payload, err := json.Marshal(patch)
+	if err != nil {
+		return err
+	}
+	_, err = runSSOCommand("kubectl", []string{"-n", opts.Namespace, "patch", "statefulset", opts.WorkloadName, "--type=strategic", "-p", string(payload)}, nil)
+	return err
+}
+
+func removeSSOEnvFrom(opts ssoOptions) error {
+	patch := map[string]interface{}{
+		"spec": map[string]interface{}{
+			"template": map[string]interface{}{
+				"spec": map[string]interface{}{
+					"containers": []map[string]interface{}{
+						{
+							"name":    opts.ContainerName,
+							"envFrom": nil,
+						},
+					},
+				},
+			},
+		},
+	}
+	payload, err := json.Marshal(patch)
+	if err != nil {
+		return err
+	}
+	_, err = runSSOCommand("kubectl", []string{"-n", opts.Namespace, "patch", "statefulset", opts.WorkloadName, "--type=strategic", "-p", string(payload)}, nil)
+	return err
+}
+
+func deleteSSOConfigMap(opts ssoOptions) error {
+	_, err := runSSOCommand("kubectl", []string{"-n", opts.Namespace, "delete", "configmap", opts.ConfigMapName, "--ignore-not-found"}, nil)
+	return err
+}
+
+func rolloutSSOWorkload(opts ssoOptions) error {
+	if _, err := runSSOCommand("kubectl", []string{"-n", opts.Namespace, "rollout", "restart", "statefulset/" + opts.WorkloadName}, nil); err != nil {
+		return err
+	}
+	_, err := runSSOCommand("kubectl", []string{"-n", opts.Namespace, "rollout", "status", "statefulset/" + opts.WorkloadName, "--timeout=180s"}, nil)
+	return err
+}
+
+func realCommandRunner(name string, args []string, stdin []byte) ([]byte, error) {
+	cmdName := name
+	if _, err := exec.LookPath(cmdName); err != nil && name == "kubectl" {
+		if home, homeErr := os.UserHomeDir(); homeErr == nil {
+			opsKubectl := home + "/.ops/linux-amd64/bin/kubectl"
+			if _, statErr := os.Stat(opsKubectl); statErr == nil {
+				cmdName = opsKubectl
+			}
+		}
+	}
+
+	cmd := exec.Command(cmdName, args...)
+	if stdin != nil {
+		cmd.Stdin = bytes.NewReader(stdin)
+	}
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err := cmd.Run()
+	output := append(stdout.Bytes(), stderr.Bytes()...)
+	if err != nil {
+		if len(output) > 0 {
+			return output, fmt.Errorf("%s %s failed: %w: %s", name, strings.Join(args, " "), err, strings.TrimSpace(string(output)))
+		}
+		return output, fmt.Errorf("%s %s failed: %w", name, strings.Join(args, " "), err)
+	}
+	if len(output) > 0 {
+		fmt.Print(string(output))
+	}
+	return output, nil
+}
diff --git a/config/sso_tool_test.go b/config/sso_tool_test.go
new file mode 100644
index 0000000..86dc38b
--- /dev/null
+++ b/config/sso_tool_test.go
@@ -0,0 +1,131 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package config
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+type recordedCommand struct {
+	name  string
+	args  []string
+	stdin string
+}
+
+func TestConfigSSOToolKeycloak(t *testing.T) {
+	tmpDir := t.TempDir()
+	configPath := filepath.Join(tmpDir, "config.json")
+	cm, err := NewConfigMapBuilder().WithConfigJson(configPath).Build()
+	require.NoError(t, err)
+
+	var commands []recordedCommand
+	oldRunner := runSSOCommand
+	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
+		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		return []byte("ok"), nil
+	}
+	defer func() { runSSOCommand = oldRunner }()
+
+	err = ConfigSSOTool(cm, []string{
+		"keycloak",
+		"--enable",
+		"--issuer-url", "http://localhost:8080/realms/openserverless-lab",
+		"--jwks-url", "http://172.18.0.1:8080/realms/openserverless-lab/protocol/openid-connect/certs",
+		"--audience", "openserverless-admin-api",
+		"--required-group", "openserverless-users",
+		"--no-rollout",
+	})
+	require.NoError(t, err)
+
+	gotConfig, err := readConfig(configPath, fromConfigJson)
+	require.NoError(t, err)
+	require.Equal(t, true, gotConfig["sso"].(map[string]interface{})["enabled"])
+	require.Equal(t, "keycloak", gotConfig["sso"].(map[string]interface{})["provider"])
+	require.Equal(t, true, gotConfig["sso"].(map[string]interface{})["autoprovision"].(map[string]interface{})["on"].(map[string]interface{})["login"])
+	require.Equal(t, float64(120), gotConfig["sso"].(map[string]interface{})["autoprovision"].(map[string]interface{})["timeout"].(map[string]interface{})["seconds"])
+
+	require.Len(t, commands, 2)
+	require.Equal(t, []string{"apply", "-f", "-"}, commands[0].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=strategic", "-p", commands[1].args[7]}, commands[1].args)
+
+	var cmObj map[string]interface{}
+	require.NoError(t, json.Unmarshal([]byte(commands[0].stdin), &cmObj))
+	require.Equal(t, "ConfigMap", cmObj["kind"])
+	data := cmObj["data"].(map[string]interface{})
+	require.Equal(t, "openserverless-admin-api", data["OIDC_AUDIENCE"])
+	require.Equal(t, "preferred_username", data["OIDC_USERNAME_CLAIM"])
+	require.Equal(t, "groups", data["OIDC_GROUPS_CLAIM"])
+	require.Equal(t, "true", data["SSO_AUTOPROVISION_ON_LOGIN"])
+	require.Equal(t, "120", data["SSO_AUTOPROVISION_TIMEOUT_SECONDS"])
+	require.Equal(t, "2", data["SSO_AUTOPROVISION_POLL_SECONDS"])
+	require.Equal(t, "all", data["SSO_AUTOPROVISION_DEFAULT_SERVICES"])
+	require.Equal(t, "true", data["SSO_NAMESPACE_PRESERVE_VALID"])
+	require.Equal(t, "8", data["SSO_NAMESPACE_HASH_LENGTH"])
+	require.Equal(t, "61", data["SSO_NAMESPACE_MAX_LENGTH"])
+}
+
+func TestConfigSSOToolKeycloakRequiresValues(t *testing.T) {
+	cm, err := NewConfigMapBuilder().Build()
+	require.NoError(t, err)
+
+	err = ConfigSSOTool(cm, []string{"keycloak", "--enable"})
+	require.Error(t, err)
+	require.True(t, strings.Contains(err.Error(), "missing --issuer-url"))
+}
+
+func TestConfigSSOToolDisable(t *testing.T) {
+	tmpDir := t.TempDir()
+	configPath := filepath.Join(tmpDir, "config.json")
+	require.NoError(t, os.WriteFile(configPath, []byte(`{
+  "sso": {
+    "enabled": "true",
+    "provider": "keycloak",
+    "oidc": {
+      "issuer": {
+        "url": "http://issuer"
+      }
+    }
+  }
+}`), 0644))
+
+	cm, err := NewConfigMapBuilder().WithConfigJson(configPath).Build()
+	require.NoError(t, err)
+
+	var commands []recordedCommand
+	oldRunner := runSSOCommand
+	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
+		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		return []byte("ok"), nil
+	}
+	defer func() { runSSOCommand = oldRunner }()
+
+	err = ConfigSSOTool(cm, []string{"disable", "--no-rollout"})
+	require.NoError(t, err)
+
+	gotConfig, err := readConfig(configPath, fromConfigJson)
+	require.NoError(t, err)
+	require.Empty(t, gotConfig)
+	require.Len(t, commands, 2)
+	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[1].args)
+}
diff --git a/main.go b/main.go
index 50c1a3d..cf19df8 100644
--- a/main.go
+++ b/main.go
@@ -220,7 +220,7 @@
 		}
 
 		fmt.Println("Successfully logged in as " + loginResult.Login + ".")
-		if err := wskPropertySet(loginResult.ApiHost, loginResult.Auth); err != nil {
+		if err := wskPropertySet(loginResult.ApiHost, loginResult.Auth, loginResult.Login); err != nil {
 			log.Fatalf("error: %s", err.Error())
 		}
 		fmt.Println("OpenServerless host and auth set successfully. You are now ready to use ops!")
@@ -406,10 +406,24 @@
 		log.Fatalf("failed preflight check: %s", err.Error())
 	}
 
+	args := os.Args
+
+	// Keep the historical `ops config` task surface intact; only the new SSO
+	// subtree is handled by the embedded config tool.
+	if isPlainConfigSSOCommand(args) {
+		fullargs := append([]string{"config"}, args[2:]...)
+		exitCode := executeTools(fullargs, opsHome)
+		os.Exit(exitCode)
+	}
+
 	// first argument with prefix "-" is considered an embedded tool
 	// using "-" or "--" or "-task" invokes the embedded task
 	// CLI: ops -<tool> (embedded tool)
-	args := os.Args
+	if len(args) > 1 && isPlainEmbeddedToolAlias(args[1]) {
+		fullargs := append([]string{args[1]}, args[2:]...)
+		exitCode := executeTools(fullargs, opsHome)
+		os.Exit(exitCode)
+	}
 	if len(args) > 1 && len(args[1]) > 0 && args[1][0] == '-' {
 		cmd := args[1][1:]
 		if cmd == "t" || cmd == "tasks" {
@@ -470,21 +484,80 @@
 		if err := os.Setenv(k, v); err != nil {
 			return err
 		}
-		debug("env var set", k, v)
+		debug("env var set", k, configValueForDebug(k, v))
 	}
 
 	return nil
 }
 
-func wskPropertySet(apihost, auth string) error {
+func configValueForDebug(key, value string) string {
+	if value == "" {
+		return ""
+	}
+	normalizedKey := strings.ToLower(key)
+	for _, marker := range []string{"auth", "password", "secret", "token", "credential", "access_key", "secret_key", "redis_url", "mongodb_url", "postgres_url"} {
+		if strings.Contains(normalizedKey, marker) {
+			return "<redacted>"
+		}
+	}
+	return value
+}
+
+func wskPropertySet(apihost, auth, namespace string) error {
 	args := []string{"property", "set", "--apihost", apihost, "--auth", auth}
 	cmd := append([]string{"wsk"}, args...)
 	if err := tools.Wsk(cmd); err != nil {
 		return err
 	}
+	if namespace != "" {
+		return wskNamespaceSet(namespace)
+	}
 	return nil
 }
 
+func wskNamespaceSet(namespace string) error {
+	wskProps := os.Getenv("WSK_CONFIG_FILE")
+	if wskProps == "" {
+		home, err := homedir.Expand("~")
+		if err != nil {
+			return err
+		}
+		wskProps = filepath.Join(home, ".wskprops")
+	}
+
+	content, err := os.ReadFile(wskProps)
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+
+	lines := []string{}
+	found := false
+	for _, line := range strings.Split(string(content), "\n") {
+		if line == "" {
+			continue
+		}
+		if strings.HasPrefix(line, "NAMESPACE=") {
+			lines = append(lines, "NAMESPACE="+namespace)
+			found = true
+		} else {
+			lines = append(lines, line)
+		}
+	}
+	if !found {
+		lines = append(lines, "NAMESPACE="+namespace)
+	}
+
+	return os.WriteFile(wskProps, []byte(strings.Join(lines, "\n")+"\n"), 0600)
+}
+
+func isPlainEmbeddedToolAlias(name string) bool {
+	return false
+}
+
+func isPlainConfigSSOCommand(args []string) bool {
+	return len(args) > 2 && args[1] == "config" && args[2] == "sso"
+}
+
 func runOps(baseDir string, args []string) error {
 	err := Ops(baseDir, args[1:])
 	if err == nil {
diff --git a/main_test.go b/main_test.go
index 14694cc..32c7a64 100644
--- a/main_test.go
+++ b/main_test.go
@@ -129,3 +129,43 @@
 		require.Equal(t, expected4, output4)
 	})
 }
+
+func TestWskNamespaceSet(t *testing.T) {
+	wskProps := filepath.Join(t.TempDir(), ".wskprops")
+	require.NoError(t, os.WriteFile(wskProps, []byte("AUTH=uuid:key\nAPIHOST=http://localhost:5000\n"), 0600))
+	t.Setenv("WSK_CONFIG_FILE", wskProps)
+
+	require.NoError(t, wskNamespaceSet("developerlab8e8a9915"))
+	content, err := os.ReadFile(wskProps)
+	require.NoError(t, err)
+	require.Equal(t, "AUTH=uuid:key\nAPIHOST=http://localhost:5000\nNAMESPACE=developerlab8e8a9915\n", string(content))
+
+	require.NoError(t, wskNamespaceSet("devel"))
+	content, err = os.ReadFile(wskProps)
+	require.NoError(t, err)
+	require.Equal(t, "AUTH=uuid:key\nAPIHOST=http://localhost:5000\nNAMESPACE=devel\n", string(content))
+}
+
+func TestPlainEmbeddedToolAlias(t *testing.T) {
+	require.False(t, isPlainEmbeddedToolAlias("config"))
+	require.False(t, isPlainEmbeddedToolAlias("admin"))
+	require.False(t, isPlainEmbeddedToolAlias("ide"))
+	require.False(t, isPlainEmbeddedToolAlias("login"))
+}
+
+func TestIsPlainConfigSSOCommand(t *testing.T) {
+	require.True(t, isPlainConfigSSOCommand([]string{"ops", "config", "sso"}))
+	require.True(t, isPlainConfigSSOCommand([]string{"ops", "config", "sso", "show"}))
+	require.False(t, isPlainConfigSSOCommand([]string{"ops", "config"}))
+	require.False(t, isPlainConfigSSOCommand([]string{"ops", "config", "--help"}))
+	require.False(t, isPlainConfigSSOCommand([]string{"ops", "-config", "sso"}))
+}
+
+func TestConfigValueForDebugRedactsSensitiveValues(t *testing.T) {
+	require.Equal(t, "<redacted>", configValueForDebug("AUTH", "uuid:key"))
+	require.Equal(t, "<redacted>", configValueForDebug("POSTGRES_PASSWORD", "secret"))
+	require.Equal(t, "<redacted>", configValueForDebug("S3_ACCESS_KEY", "access"))
+	require.Equal(t, "<redacted>", configValueForDebug("OIDC_ACCESS_TOKEN", "token"))
+	require.Equal(t, "http://localhost:8080", configValueForDebug("OIDC_ISSUER_URL", "http://localhost:8080"))
+	require.Equal(t, "", configValueForDebug("AUTH", ""))
+}
diff --git a/ops_test.go b/ops_test.go
index 8df8e28..7223233 100644
--- a/ops_test.go
+++ b/ops_test.go
@@ -33,8 +33,6 @@
 	olaris, _ := filepath.Abs(joinpath("tests", "olaris"))
 	err := Ops(olaris, split("testcmd"))
 	fmt.Println(err)
-	// Output:
-	// -
 
 	/*
 		pr(2, err)