Preserve admin API configuration when disabling SSO
diff --git a/config/sso_tool.go b/config/sso_tool.go
index 573b038..58da2c6 100644
--- a/config/sso_tool.go
+++ b/config/sso_tool.go
@@ -40,6 +40,62 @@
 
 var runSSOCommand commandRunner = realCommandRunner
 
+type ssoEnvFromSource struct {
+	Prefix       string                   `json:"prefix,omitempty"`
+	ConfigMapRef *ssoLocalObjectReference `json:"configMapRef,omitempty"`
+	SecretRef    *ssoLocalObjectReference `json:"secretRef,omitempty"`
+}
+
+type ssoLocalObjectReference struct {
+	Name     string `json:"name"`
+	Optional *bool  `json:"optional,omitempty"`
+}
+
+type ssoWorkload struct {
+	Spec struct {
+		Template struct {
+			Spec struct {
+				Containers []struct {
+					Name    string             `json:"name"`
+					EnvFrom []ssoEnvFromSource `json:"envFrom,omitempty"`
+				} `json:"containers"`
+			} `json:"spec"`
+		} `json:"template"`
+	} `json:"spec"`
+}
+
+type jsonPatchOperation struct {
+	Op    string      `json:"op"`
+	Path  string      `json:"path"`
+	Value interface{} `json:"value,omitempty"`
+}
+
+var managedLocalSSOKeys = []string{
+	"SSO_ENABLED",
+	"SSO_PROVIDER",
+	"SSO_OIDC_ISSUER_URL",
+	"SSO_OIDC_JWKS_URL",
+	"SSO_OIDC_AUDIENCE",
+	"SSO_OIDC_CLIENT_ID",
+	"SSO_OIDC_REQUIRED_GROUP",
+	"SSO_OIDC_USERNAME_CLAIM",
+	"SSO_OIDC_GROUPS_CLAIM",
+	"SSO_OIDC_CLIENT_SECRET_CONFIGURED",
+	"SSO_CLIENT_MODE",
+	"SSO_AUTOPROVISION_ON_LOGIN",
+	"SSO_AUTOPROVISION_TIMEOUT_SECONDS",
+	"SSO_AUTOPROVISION_POLL_SECONDS",
+	"SSO_AUTOPROVISION_DEFAULT_SERVICES",
+	"SSO_NAMESPACE_PRESERVE_VALID",
+	"SSO_NAMESPACE_HASH_LENGTH",
+	"SSO_NAMESPACE_MAX_LENGTH",
+	"SSO_KUBE_NAMESPACE",
+	"SSO_KUBE_CONFIGMAP",
+	"SSO_KUBE_SECRET",
+	"SSO_KUBE_STATEFULSET",
+	"SSO_KUBE_CONTAINER",
+}
+
 type ssoOptions struct {
 	IssuerURL              string
 	JWKSURL                string
@@ -77,6 +133,14 @@
 
 Configure OpenServerless SSO/OIDC integration for admin-api.
 
+Managed Kubernetes resources:
+  ConfigMap NAME            OIDC_* and SSO_* values created by this command
+  Secret NAME               OIDC_CLIENT_SECRET, when --client-secret is used
+  admin-api container       Exact envFrom references to those two resources
+
+The command does not manage direct env entries, other envFrom references,
+volumes, volumeMounts, or workload annotations. Disable leaves them unchanged.
+
 Options:
   --username-claim CLAIM   OIDC username claim. Default: preferred_username
   --groups-claim CLAIM     OIDC groups claim. Default: groups
@@ -133,7 +197,7 @@
 		}
 	}
 
-	if err := patchSSOEnvFrom(opts); err != nil {
+	if _, err := reconcileSSOEnvFrom(opts, true); err != nil {
 		return err
 	}
 
@@ -310,7 +374,8 @@
 	if err := removeLocalSSOConfig(configMap); err != nil {
 		return err
 	}
-	if err := removeSSOEnvFrom(opts); err != nil {
+	workloadChanged, err := reconcileSSOEnvFrom(opts, false)
+	if err != nil {
 		return err
 	}
 	if err := deleteSSOConfigMap(opts); err != nil {
@@ -319,8 +384,8 @@
 	if err := deleteSSOSecret(opts); err != nil {
 		return err
 	}
-	if !opts.NoRollout {
-		if err := rolloutSSOWorkload(opts); err != nil {
+	if !opts.NoRollout && workloadChanged {
+		if err := waitForSSOWorkloadRollout(opts); err != nil {
 			return err
 		}
 	}
@@ -330,8 +395,9 @@
 }
 
 func removeLocalSSOConfig(configMap ConfigMap) error {
-	for key := range configMap.Flatten() {
-		if !strings.HasPrefix(key, "SSO_") {
+	values := configMap.Flatten()
+	for _, key := range managedLocalSSOKeys {
+		if _, exists := values[key]; !exists {
 			continue
 		}
 		if err := configMap.Delete(key); err != nil && !strings.Contains(err.Error(), "does not exist in config.json") {
@@ -396,64 +462,138 @@
 	return err
 }
 
-func patchSSOEnvFrom(opts ssoOptions) error {
-	envFrom := []map[string]interface{}{
-		{
-			"configMapRef": map[string]string{
-				"name": opts.ConfigMapName,
-			},
-		},
+func reconcileSSOEnvFrom(opts ssoOptions, enabled bool) (bool, error) {
+	output, err := runSSOCommand("kubectl", []string{
+		"-n", opts.Namespace,
+		"get", "statefulset", opts.WorkloadName,
+		"-o", "json",
+	}, nil)
+	if err != nil {
+		return false, err
 	}
-	if opts.ClientSecret != "" {
-		envFrom = append(envFrom, map[string]interface{}{
-			"secretRef": map[string]string{
-				"name": opts.SecretName,
-			},
+
+	var workload ssoWorkload
+	if err := json.Unmarshal(output, &workload); err != nil {
+		return false, fmt.Errorf("decode statefulset %s/%s: %w", opts.Namespace, opts.WorkloadName, err)
+	}
+
+	containerIndex := -1
+	var current []ssoEnvFromSource
+	for index, container := range workload.Spec.Template.Spec.Containers {
+		if container.Name == opts.ContainerName {
+			containerIndex = index
+			current = container.EnvFrom
+			break
+		}
+	}
+	if containerIndex < 0 {
+		return false, fmt.Errorf("container %s not found in statefulset %s/%s", opts.ContainerName, opts.Namespace, opts.WorkloadName)
+	}
+
+	desired := make([]ssoEnvFromSource, 0, 2)
+	if enabled {
+		desired = append(desired, ssoEnvFromSource{
+			ConfigMapRef: &ssoLocalObjectReference{Name: opts.ConfigMapName},
+		})
+		if opts.ClientSecret != "" {
+			desired = append(desired, ssoEnvFromSource{
+				SecretRef: &ssoLocalObjectReference{Name: opts.SecretName},
+			})
+		}
+	}
+
+	seenDesired := make(map[string]bool)
+	removeIndexes := make([]int, 0)
+	for index, source := range current {
+		key, managed := managedSSOEnvFromKey(source, opts)
+		if !managed {
+			continue
+		}
+		if enabled && desiredSSOEnvFromKey(key, desired) && !seenDesired[key] {
+			seenDesired[key] = true
+			continue
+		}
+		removeIndexes = append(removeIndexes, index)
+	}
+
+	missing := make([]ssoEnvFromSource, 0, len(desired))
+	for _, source := range desired {
+		key, _ := managedSSOEnvFromKey(source, opts)
+		if !seenDesired[key] {
+			missing = append(missing, source)
+		}
+	}
+	if len(removeIndexes) == 0 && len(missing) == 0 {
+		return false, nil
+	}
+
+	basePath := fmt.Sprintf("/spec/template/spec/containers/%d/envFrom", containerIndex)
+	operations := make([]jsonPatchOperation, 0, len(removeIndexes)+len(missing))
+	for index := len(removeIndexes) - 1; index >= 0; index-- {
+		removeIndex := removeIndexes[index]
+		operations = append(operations, jsonPatchOperation{
+			Op:    "test",
+			Path:  fmt.Sprintf("%s/%d", basePath, removeIndex),
+			Value: current[removeIndex],
+		})
+		operations = append(operations, jsonPatchOperation{
+			Op:   "remove",
+			Path: fmt.Sprintf("%s/%d", basePath, removeIndex),
 		})
 	}
-	patch := map[string]interface{}{
-		"spec": map[string]interface{}{
-			"template": map[string]interface{}{
-				"spec": map[string]interface{}{
-					"containers": []map[string]interface{}{
-						{
-							"name":    opts.ContainerName,
-							"envFrom": envFrom,
-						},
-					},
-				},
-			},
-		},
+	if len(current) == 0 {
+		operations = append(operations, jsonPatchOperation{
+			Op:    "add",
+			Path:  basePath,
+			Value: missing,
+		})
+	} else {
+		for _, source := range missing {
+			operations = append(operations, jsonPatchOperation{
+				Op:    "add",
+				Path:  basePath + "/-",
+				Value: source,
+			})
+		}
 	}
-	payload, err := json.Marshal(patch)
+
+	payload, err := json.Marshal(operations)
 	if err != nil {
-		return err
+		return false, err
 	}
-	_, err = runSSOCommand("kubectl", []string{"-n", opts.Namespace, "patch", "statefulset", opts.WorkloadName, "--type=strategic", "-p", string(payload)}, nil)
-	return err
+	_, err = runSSOCommand("kubectl", []string{
+		"-n", opts.Namespace,
+		"patch", "statefulset", opts.WorkloadName,
+		"--type=json", "-p", string(payload),
+	}, nil)
+	return err == nil, 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,
-						},
-					},
-				},
-			},
-		},
+func managedSSOEnvFromKey(source ssoEnvFromSource, opts ssoOptions) (string, bool) {
+	if source.Prefix != "" {
+		return "", false
 	}
-	payload, err := json.Marshal(patch)
-	if err != nil {
-		return err
+	if source.ConfigMapRef != nil && source.SecretRef == nil &&
+		source.ConfigMapRef.Name == opts.ConfigMapName && source.ConfigMapRef.Optional == nil {
+		return "configmap:" + opts.ConfigMapName, true
 	}
-	_, err = runSSOCommand("kubectl", []string{"-n", opts.Namespace, "patch", "statefulset", opts.WorkloadName, "--type=strategic", "-p", string(payload)}, nil)
-	return err
+	if source.SecretRef != nil && source.ConfigMapRef == nil &&
+		source.SecretRef.Name == opts.SecretName && source.SecretRef.Optional == nil {
+		return "secret:" + opts.SecretName, true
+	}
+	return "", false
+}
+
+func desiredSSOEnvFromKey(key string, desired []ssoEnvFromSource) bool {
+	for _, source := range desired {
+		if source.ConfigMapRef != nil && key == "configmap:"+source.ConfigMapRef.Name {
+			return true
+		}
+		if source.SecretRef != nil && key == "secret:"+source.SecretRef.Name {
+			return true
+		}
+	}
+	return false
 }
 
 func deleteSSOConfigMap(opts ssoOptions) error {
@@ -474,6 +614,11 @@
 	return err
 }
 
+func waitForSSOWorkloadRollout(opts ssoOptions) error {
+	_, err := runSSOCommand("kubectl", []string{"-n", opts.Namespace, "rollout", "status", "statefulset/" + opts.WorkloadName, "--timeout=180s"}, nil)
+	return err
+}
+
 func ssoClientMode(opts ssoOptions) string {
 	if opts.ClientSecret != "" {
 		return "confidential"
@@ -525,8 +670,24 @@
 		}
 		return output, fmt.Errorf("%s %s failed: %w", name, strings.Join(args, " "), err)
 	}
-	if len(output) > 0 {
+	if len(output) > 0 && !isSSOWorkloadJSONGet(name, args) {
 		fmt.Print(string(output))
 	}
 	return output, nil
 }
+
+func isSSOWorkloadJSONGet(name string, args []string) bool {
+	if name != "kubectl" {
+		return false
+	}
+	for index := 0; index+1 < len(args); index++ {
+		if args[index] == "get" && index+2 < len(args) && args[index+1] == "statefulset" {
+			for outputIndex := index + 2; outputIndex+1 < len(args); outputIndex++ {
+				if args[outputIndex] == "-o" && args[outputIndex+1] == "json" {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
diff --git a/config/sso_tool_test.go b/config/sso_tool_test.go
index 910ef48..9f10229 100644
--- a/config/sso_tool_test.go
+++ b/config/sso_tool_test.go
@@ -21,6 +21,7 @@
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -33,6 +34,103 @@
 	stdin string
 }
 
+func ssoWorkloadJSON(envFrom ...ssoEnvFromSource) []byte {
+	workload := map[string]interface{}{
+		"spec": map[string]interface{}{
+			"template": map[string]interface{}{
+				"spec": map[string]interface{}{
+					"containers": []map[string]interface{}{
+						{
+							"name":    defaultSSOContainer,
+							"envFrom": envFrom,
+						},
+					},
+				},
+			},
+		},
+	}
+	payload, _ := json.Marshal(workload)
+	return payload
+}
+
+func isSSOWorkloadGet(args []string) bool {
+	return len(args) >= 7 && args[2] == "get" && args[3] == "statefulset" && args[6] == "json"
+}
+
+func managedConfigMapSource() ssoEnvFromSource {
+	return ssoEnvFromSource{ConfigMapRef: &ssoLocalObjectReference{Name: defaultSSOConfigMap}}
+}
+
+func managedSecretSource() ssoEnvFromSource {
+	return ssoEnvFromSource{SecretRef: &ssoLocalObjectReference{Name: defaultSSOSecret}}
+}
+
+type fakeSSOWorkloadState struct {
+	Env          []map[string]string
+	EnvFrom      []ssoEnvFromSource
+	VolumeMounts []map[string]string
+	Volumes      []map[string]interface{}
+}
+
+func (state *fakeSSOWorkloadState) workloadJSON() []byte {
+	workload := map[string]interface{}{
+		"spec": map[string]interface{}{
+			"template": map[string]interface{}{
+				"spec": map[string]interface{}{
+					"containers": []map[string]interface{}{
+						{
+							"name":         defaultSSOContainer,
+							"env":          state.Env,
+							"envFrom":      state.EnvFrom,
+							"volumeMounts": state.VolumeMounts,
+						},
+					},
+					"volumes": state.Volumes,
+				},
+			},
+		},
+	}
+	payload, _ := json.Marshal(workload)
+	return payload
+}
+
+func (state *fakeSSOWorkloadState) applyJSONPatch(payload string) error {
+	var operations []struct {
+		Op    string          `json:"op"`
+		Path  string          `json:"path"`
+		Value json.RawMessage `json:"value"`
+	}
+	if err := json.Unmarshal([]byte(payload), &operations); err != nil {
+		return err
+	}
+	for _, operation := range operations {
+		switch operation.Op {
+		case "remove":
+			parts := strings.Split(operation.Path, "/")
+			index, err := strconv.Atoi(parts[len(parts)-1])
+			if err != nil {
+				return err
+			}
+			state.EnvFrom = append(state.EnvFrom[:index], state.EnvFrom[index+1:]...)
+		case "add":
+			if strings.HasSuffix(operation.Path, "/-") {
+				var source ssoEnvFromSource
+				if err := json.Unmarshal(operation.Value, &source); err != nil {
+					return err
+				}
+				state.EnvFrom = append(state.EnvFrom, source)
+				continue
+			}
+			var sources []ssoEnvFromSource
+			if err := json.Unmarshal(operation.Value, &sources); err != nil {
+				return err
+			}
+			state.EnvFrom = sources
+		}
+	}
+	return nil
+}
+
 func TestConfigSSOToolKeycloak(t *testing.T) {
 	tmpDir := t.TempDir()
 	configPath := filepath.Join(tmpDir, "config.json")
@@ -43,6 +141,9 @@
 	oldRunner := runSSOCommand
 	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
 		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(), nil
+		}
 		return []byte("ok"), nil
 	}
 	defer func() { runSSOCommand = oldRunner }()
@@ -65,9 +166,10 @@
 	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.Len(t, commands, 3)
 	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)
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[1].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
 
 	var cmObj map[string]interface{}
 	require.NoError(t, json.Unmarshal([]byte(commands[0].stdin), &cmObj))
@@ -96,6 +198,9 @@
 	oldRunner := runSSOCommand
 	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
 		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(), nil
+		}
 		return []byte("ok"), nil
 	}
 	defer func() { runSSOCommand = oldRunner }()
@@ -110,11 +215,12 @@
 	})
 	require.NoError(t, err)
 
-	require.Len(t, commands, 4)
+	require.Len(t, commands, 5)
 	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)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "restart", "statefulset/nuvolaris-system-api"}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[3].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[1].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[2].args[7]}, commands[2].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "restart", "statefulset/nuvolaris-system-api"}, commands[3].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[4].args)
 }
 
 func TestConfigSSOToolKeycloakWithClientSecret(t *testing.T) {
@@ -127,6 +233,9 @@
 	oldRunner := runSSOCommand
 	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
 		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(), nil
+		}
 		return []byte("ok"), nil
 	}
 	defer func() { runSSOCommand = oldRunner }()
@@ -155,7 +264,7 @@
 	require.Equal(t, "true", flat["SSO_OIDC_CLIENT_SECRET_CONFIGURED"])
 	require.Equal(t, "custom-sso-secret", flat["SSO_KUBE_SECRET"])
 
-	require.Len(t, commands, 3)
+	require.Len(t, commands, 4)
 
 	var cmObj map[string]interface{}
 	require.NoError(t, json.Unmarshal([]byte(commands[0].stdin), &cmObj))
@@ -172,8 +281,9 @@
 	secretData := secretObj["stringData"].(map[string]interface{})
 	require.Equal(t, "super-secret", secretData["OIDC_CLIENT_SECRET"])
 
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=strategic", "-p", commands[2].args[7]}, commands[2].args)
-	require.Contains(t, commands[2].args[7], "custom-sso-secret")
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[2].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[3].args[7]}, commands[3].args)
+	require.Contains(t, commands[3].args[7], "custom-sso-secret")
 }
 
 func TestConfigSSOToolKeycloakRequiresValues(t *testing.T) {
@@ -196,7 +306,8 @@
       "issuer": {
         "url": "http://issuer"
       }
-    }
+    },
+    "external": "preserve-me"
   }
 }`), 0644))
 
@@ -207,6 +318,9 @@
 	oldRunner := runSSOCommand
 	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
 		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(managedConfigMapSource(), managedSecretSource()), nil
+		}
 		return []byte("ok"), nil
 	}
 	defer func() { runSSOCommand = oldRunner }()
@@ -216,13 +330,19 @@
 
 	gotConfig, err := readConfig(configPath, fromConfigJson)
 	require.NoError(t, err)
-	require.Empty(t, gotConfig)
-	require.Len(t, commands, 3)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[1].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, map[string]interface{}{
+		"sso": map[string]interface{}{
+			"external": "preserve-me",
+		},
+	}, gotConfig)
+	require.Len(t, commands, 4)
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
 }
 
-func TestConfigSSOToolDisableRollsOutAdminAPIByDefault(t *testing.T) {
+func TestConfigSSOToolDisableWaitsForAdminAPIRolloutByDefault(t *testing.T) {
 	tmpDir := t.TempDir()
 	configPath := filepath.Join(tmpDir, "config.json")
 	cm, err := NewConfigMapBuilder().WithConfigJson(configPath).Build()
@@ -232,6 +352,9 @@
 	oldRunner := runSSOCommand
 	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
 		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(managedConfigMapSource(), managedSecretSource()), nil
+		}
 		return []byte("ok"), nil
 	}
 	defer func() { runSSOCommand = oldRunner }()
@@ -240,9 +363,141 @@
 	require.NoError(t, err)
 
 	require.Len(t, commands, 5)
-	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=strategic", "-p", commands[0].args[7]}, commands[0].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "patch", "statefulset", "nuvolaris-system-api", "--type=json", "-p", commands[1].args[7]}, commands[1].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[2].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[3].args)
+	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[4].args)
+}
+
+func TestConfigSSOToolDisableAlreadyAbsentDoesNotPatchOrRollOut(t *testing.T) {
+	tmpDir := t.TempDir()
+	configPath := filepath.Join(tmpDir, "config.json")
+	cm, err := NewConfigMapBuilder().WithConfigJson(configPath).Build()
+	require.NoError(t, err)
+
+	foreign := ssoEnvFromSource{
+		ConfigMapRef: &ssoLocalObjectReference{Name: "application-config"},
+	}
+	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)})
+		if isSSOWorkloadGet(args) {
+			return ssoWorkloadJSON(foreign), nil
+		}
+		return []byte("ok"), nil
+	}
+	defer func() { runSSOCommand = oldRunner }()
+
+	require.NoError(t, ConfigSSOTool(cm, []string{"disable"}))
+	require.Len(t, commands, 3)
+	require.Equal(t, []string{"-n", "nuvolaris", "get", "statefulset", "nuvolaris-system-api", "-o", "json"}, commands[0].args)
 	require.Equal(t, []string{"-n", "nuvolaris", "delete", "configmap", "openserverless-sso-config", "--ignore-not-found"}, commands[1].args)
 	require.Equal(t, []string{"-n", "nuvolaris", "delete", "secret", "openserverless-sso-secret", "--ignore-not-found"}, commands[2].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "restart", "statefulset/nuvolaris-system-api"}, commands[3].args)
-	require.Equal(t, []string{"-n", "nuvolaris", "rollout", "status", "statefulset/nuvolaris-system-api", "--timeout=180s"}, commands[4].args)
+}
+
+func TestConfigSSOToolPreservesForeignWorkloadFieldsAcrossEnableDisableEnable(t *testing.T) {
+	tmpDir := t.TempDir()
+	configPath := filepath.Join(tmpDir, "config.json")
+	cm, err := NewConfigMapBuilder().WithConfigJson(configPath).Build()
+	require.NoError(t, err)
+
+	state := fakeSSOWorkloadState{
+		Env: []map[string]string{
+			{"name": "APPLICATION_MODE", "value": "production"},
+		},
+		EnvFrom: []ssoEnvFromSource{
+			{ConfigMapRef: &ssoLocalObjectReference{Name: "application-config"}},
+			{SecretRef: &ssoLocalObjectReference{Name: "database-credentials"}},
+		},
+		VolumeMounts: []map[string]string{
+			{"name": "application-data", "mountPath": "/data"},
+		},
+		Volumes: []map[string]interface{}{
+			{"name": "application-data", "emptyDir": map[string]interface{}{}},
+		},
+	}
+	originalEnv, err := json.Marshal(state.Env)
+	require.NoError(t, err)
+	originalMounts, err := json.Marshal(state.VolumeMounts)
+	require.NoError(t, err)
+	originalVolumes, err := json.Marshal(state.Volumes)
+	require.NoError(t, err)
+
+	var commands []recordedCommand
+	var patchPayloads []string
+	oldRunner := runSSOCommand
+	runSSOCommand = func(name string, args []string, stdin []byte) ([]byte, error) {
+		commands = append(commands, recordedCommand{name: name, args: args, stdin: string(stdin)})
+		if isSSOWorkloadGet(args) {
+			return state.workloadJSON(), nil
+		}
+		if len(args) >= 8 && args[2] == "patch" && args[5] == "--type=json" {
+			patchPayloads = append(patchPayloads, args[7])
+			if err := state.applyJSONPatch(args[7]); err != nil {
+				return nil, err
+			}
+		}
+		return []byte("ok"), nil
+	}
+	defer func() { runSSOCommand = oldRunner }()
+
+	enableArgs := []string{
+		"keycloak",
+		"--enable",
+		"--issuer-url", "https://keycloak.example.test/realms/openserverless",
+		"--jwks-url", "https://keycloak.example.test/realms/openserverless/protocol/openid-connect/certs",
+		"--client-id", "openserverless-admin-api",
+		"--client-secret", "test-secret",
+		"--required-group", "openserverless-users",
+		"--no-rollout",
+	}
+	require.NoError(t, ConfigSSOTool(cm, enableArgs))
+	require.Equal(t, []ssoEnvFromSource{
+		{ConfigMapRef: &ssoLocalObjectReference{Name: "application-config"}},
+		{SecretRef: &ssoLocalObjectReference{Name: "database-credentials"}},
+		managedConfigMapSource(),
+		managedSecretSource(),
+	}, state.EnvFrom)
+
+	require.NoError(t, ConfigSSOTool(cm, []string{"disable", "--no-rollout"}))
+	require.Equal(t, []ssoEnvFromSource{
+		{ConfigMapRef: &ssoLocalObjectReference{Name: "application-config"}},
+		{SecretRef: &ssoLocalObjectReference{Name: "database-credentials"}},
+	}, state.EnvFrom)
+	require.Len(t, patchPayloads, 2)
+	require.JSONEq(t, `[
+  {"op":"test","path":"/spec/template/spec/containers/0/envFrom/3","value":{"secretRef":{"name":"openserverless-sso-secret"}}},
+  {"op":"remove","path":"/spec/template/spec/containers/0/envFrom/3"},
+  {"op":"test","path":"/spec/template/spec/containers/0/envFrom/2","value":{"configMapRef":{"name":"openserverless-sso-config"}}},
+  {"op":"remove","path":"/spec/template/spec/containers/0/envFrom/2"}
+]`, patchPayloads[1])
+
+	// Repeating disable is idempotent: resources are deleted with
+	// --ignore-not-found and no StatefulSet patch or rollout is produced.
+	require.NoError(t, ConfigSSOTool(cm, []string{"disable", "--no-rollout"}))
+	require.Len(t, patchPayloads, 2)
+
+	require.NoError(t, ConfigSSOTool(cm, enableArgs))
+	require.Equal(t, []ssoEnvFromSource{
+		{ConfigMapRef: &ssoLocalObjectReference{Name: "application-config"}},
+		{SecretRef: &ssoLocalObjectReference{Name: "database-credentials"}},
+		managedConfigMapSource(),
+		managedSecretSource(),
+	}, state.EnvFrom)
+	require.Len(t, patchPayloads, 3)
+
+	currentEnv, err := json.Marshal(state.Env)
+	require.NoError(t, err)
+	currentMounts, err := json.Marshal(state.VolumeMounts)
+	require.NoError(t, err)
+	currentVolumes, err := json.Marshal(state.Volumes)
+	require.NoError(t, err)
+	require.JSONEq(t, string(originalEnv), string(currentEnv))
+	require.JSONEq(t, string(originalMounts), string(currentMounts))
+	require.JSONEq(t, string(originalVolumes), string(currentVolumes))
+	for _, command := range commands {
+		require.NotContains(t, command.args, "rollout")
+	}
 }
diff --git a/docs/SSO.md b/docs/SSO.md
new file mode 100644
index 0000000..a91f7c9
--- /dev/null
+++ b/docs/SSO.md
@@ -0,0 +1,70 @@
+<!--
+  ~ 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.
+  -->
+
+# SSO configuration ownership
+
+`ops config sso keycloak --enable` manages the following configuration. The
+resource names can be changed with `--configmap`, `--secret`, `--statefulset`,
+and `--container`.
+
+## Kubernetes resources
+
+The command creates and owns a dedicated ConfigMap. Its default name is
+`openserverless-sso-config`, and it contains exactly these keys:
+
+- `OIDC_ISSUER_URL`
+- `OIDC_JWKS_URL`
+- `OIDC_AUDIENCE`
+- `OIDC_CLIENT_ID`
+- `OIDC_REQUIRED_GROUP`
+- `OIDC_USERNAME_CLAIM`
+- `OIDC_GROUPS_CLAIM`
+- `SSO_AUTOPROVISION_ON_LOGIN`
+- `SSO_AUTOPROVISION_TIMEOUT_SECONDS`
+- `SSO_AUTOPROVISION_POLL_SECONDS`
+- `SSO_AUTOPROVISION_DEFAULT_SERVICES`
+- `SSO_NAMESPACE_PRESERVE_VALID`
+- `SSO_NAMESPACE_HASH_LENGTH`
+- `SSO_NAMESPACE_MAX_LENGTH`
+
+When `--client-secret` is supplied, the command also creates and owns a
+dedicated Secret. Its default name is `openserverless-sso-secret`, and its only
+managed key is `OIDC_CLIENT_SECRET`.
+
+The command adds exact, prefix-free `envFrom` references for the managed
+ConfigMap and, when applicable, the managed Secret to the selected admin-api
+container. It does not create direct `env` entries, volumes, volume mounts, or
+annotations.
+
+## Disable behavior
+
+`ops config sso disable` removes only the exact `envFrom` references described
+above and deletes the two dedicated resources with Kubernetes
+`--ignore-not-found`. Other `envFrom` entries, all direct `env` entries,
+volumes, volume mounts, and existing annotations remain unchanged.
+
+The command is idempotent. If neither managed reference is present, no patch or
+rollout command is issued for the StatefulSet. Missing ConfigMaps and Secrets
+are not errors. Removing an `envFrom` entry changes the pod template, so
+Kubernetes starts the necessary rollout itself. By default the command waits
+for that rollout; `--no-rollout` skips the wait and does not issue an additional
+restart.
+
+The local `~/.ops/config.json` cleanup is also limited to the keys written by
+the SSO command. Unrecognized `SSO_*` keys are preserved.