diff --git a/README.md b/README.md index 04c1d1cbc..d60329b5d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ The main code repositories of Dubbo Admin include: ## Quick Start Please refer to [official website](https://cn.dubbo.apache.org/zh-cn/overview/home/). +For the Kubernetes manifests, see [Kubernetes deployment](release/kubernetes/dubbo-system/README.md). + ## Roadmap Please refer to [RoadMap](https://github.com/apache/dubbo-admin/discussions/1300). diff --git a/app/dubbo-admin/dubbo-admin-oauth-example.yaml b/app/dubbo-admin/dubbo-admin-oauth-example.yaml index 4b600a2ec..9b1cfceb1 100644 --- a/app/dubbo-admin/dubbo-admin-oauth-example.yaml +++ b/app/dubbo-admin/dubbo-admin-oauth-example.yaml @@ -23,7 +23,8 @@ console: user: admin password: replace-with-admin-password expirationTime: 3600 - sessionSecret: replace-with-a-random-secret-at-least-32-bytes + # Required. Generate a unique value with: openssl rand -base64 32 + sessionSecret: "" sessionCookieSecure: true providers: github: diff --git a/app/dubbo-admin/dubbo-admin.yaml b/app/dubbo-admin/dubbo-admin.yaml index a9588337e..a17ea2bbb 100644 --- a/app/dubbo-admin/dubbo-admin.yaml +++ b/app/dubbo-admin/dubbo-admin.yaml @@ -50,10 +50,13 @@ console: instance: http://101.34.253.152:30300/d/f5f48f75-13ec-489b-88ae-635ae38d8618?kiosk=1&theme=light service: http://101.34.253.152:30300/d/b2e178fb-ada3-4d5e-9f54-de99e7f07662?kiosk=1&theme=light # [Optional] typical user-pwd authentication, default user-pwd is admin-admin. - auth: - user: admin - password: dubbo@2025 - expirationTime: 3600 + auth: + user: admin + password: dubbo@2025 + # Required. Set a deployment-specific random value, for example: + # openssl rand -base64 32 + sessionSecret: "" + expirationTime: 3600 # [Optional] config for data storage, default is memory store: diff --git a/docs/server-develop.md b/docs/server-develop.md index 0c71ef67d..bbef9accd 100644 --- a/docs/server-develop.md +++ b/docs/server-develop.md @@ -43,6 +43,7 @@ If you're using GoLand, you can run it locally by following steps: 2. Fill the block with the config that screenshot shows below: ![ide_configuration.png](./static/images/ide-config.png) 3. Modify the config file(app/dubbo-admin/dubbo-admin.yaml), make sure that the discovery, engine, store is configured. + Configure `console.auth.sessionSecret` with a unique random value before starting the server. For deployments that use a secret manager or Kubernetes Secret, the value can instead be provided through `DUBBO_ADMIN_SESSION_SECRET`. Generate a value with `openssl rand -base64 32`; the server refuses to start when the value is missing or shorter than 32 bytes. Traffic-rule version history records RuleVersion audit entries for history, diff, and rollback material. Supported governance rule mutations fail closed if the version ledger is unavailable or cannot record the mutation. 4. Run the application, you can open the browser and visit localhost:8888/admin if everything works. diff --git a/pkg/config/app/admin.go b/pkg/config/app/admin.go index c2c05f295..fcfb071c6 100644 --- a/pkg/config/app/admin.go +++ b/pkg/config/app/admin.go @@ -67,7 +67,6 @@ var DefaultAdminConfig = func() AdminConfig { Engine: engine.DefaultResourceEngineConfig(), Observability: observability.DefaultObservabilityConfig(), Diagnostics: diagnostics.DefaultDiagnosticsConfig(), - Console: console.DefaultConsoleConfig(), EventBus: &eventBusCfg, RuleVersioning: versioning.Default(), } @@ -79,7 +78,9 @@ func (c *AdminConfig) Sanitize() { d.Sanitize() } c.Store.Sanitize() - c.Console.Sanitize() + if c.Console != nil { + c.Console.Sanitize() + } c.Observability.Sanitize() c.Diagnostics.Sanitize() c.Log.Sanitize() @@ -90,6 +91,9 @@ func (c *AdminConfig) Sanitize() { } func (c *AdminConfig) PreProcess() error { + if c.Console == nil { + return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty") + } discoveryPreProcess := func() error { for _, d := range c.Discovery { if err := d.PreProcess(); err != nil { @@ -114,6 +118,9 @@ func (c *AdminConfig) PreProcess() error { } func (c *AdminConfig) PostProcess() error { + if c.Console == nil { + return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty") + } discoveryPostProcess := func() error { for _, d := range c.Discovery { if err := d.PostProcess(); err != nil { @@ -138,6 +145,9 @@ func (c *AdminConfig) PostProcess() error { } func (c *AdminConfig) Validate() error { + if c.Console == nil { + return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty") + } if c.Log == nil { c.Log = log.DefaultLogConfig() } else if err := c.Log.Validate(); err != nil { @@ -153,9 +163,7 @@ func (c *AdminConfig) Validate() error { } else if err := c.Diagnostics.Validate(); err != nil { return bizerror.Wrap(err, bizerror.ConfigError, "diagnostics config validation failed") } - if c.Console == nil { - c.Console = console.DefaultConsoleConfig() - } else if err := c.Console.Validate(); err != nil { + if err := c.Console.Validate(); err != nil { return bizerror.Wrap(err, bizerror.ConfigError, "console config validation failed") } if c.Observability == nil { diff --git a/pkg/config/app/admin_test.go b/pkg/config/app/admin_test.go index ceaf84d89..a196aa2f3 100644 --- a/pkg/config/app/admin_test.go +++ b/pkg/config/app/admin_test.go @@ -18,11 +18,15 @@ package app import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/apache/dubbo-admin/pkg/config" + configauth "github.com/apache/dubbo-admin/pkg/config/console/auth" "github.com/apache/dubbo-admin/pkg/config/versioning" ) @@ -35,3 +39,81 @@ func TestAdminConfigSanitizeRetainsDefaultRuleVersioning(t *testing.T) { require.NotNil(t, cfg.RuleVersioning) assert.Equal(t, versioning.DefaultMaxVersionsPerRule, cfg.RuleVersioning.MaxVersionsPerRule) } + +func TestAdminConfigValidateRejectsMissingConsole(t *testing.T) { + t.Setenv(configauth.SessionSecretEnvVar, "0123456789abcdef0123456789abcdef") + cfg := DefaultAdminConfig() + + err := cfg.Validate() + require.ErrorContains(t, err, "console config is needed") +} + +func TestConfigLoadFailsClosedForMissingOrEmptyConsole(t *testing.T) { + t.Setenv(configauth.SessionSecretEnvVar, "0123456789abcdef0123456789abcdef") + tests := []struct { + name string + contents string + wantError string + }{ + {name: "missing", contents: "discovery: []\n", wantError: "console config is needed"}, + {name: "null", contents: "console: null\n", wantError: "console config is needed"}, + {name: "empty object", contents: "console: {}\n", wantError: "invalid gin mode"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(tt.contents), 0600)) + + cfg := DefaultAdminConfig() + err := config.Load(path, &cfg) + require.ErrorContains(t, err, tt.wantError) + }) + } +} + +func TestConfigLoadUsesSessionSecretFromEnvironment(t *testing.T) { + secret := "0123456789abcdef0123456789abcdef" + t.Setenv(configauth.SessionSecretEnvVar, secret) + path := filepath.Join(t.TempDir(), "config.yaml") + contents := `console: + ginMode: release + port: 8888 + auth: + user: admin + password: test-password + expirationTime: 3600 + sessionSecret: "" +discovery: + - id: test + name: test + type: mock +` + require.NoError(t, os.WriteFile(path, []byte(contents), 0600)) + + cfg := DefaultAdminConfig() + require.NoError(t, config.Load(path, &cfg)) + assert.Equal(t, secret, cfg.Console.Auth.SessionSecret) +} + +func TestConfigLoadRejectsMissingSessionSecret(t *testing.T) { + t.Setenv(configauth.SessionSecretEnvVar, "") + path := filepath.Join(t.TempDir(), "config.yaml") + contents := `console: + ginMode: release + port: 8888 + auth: + user: admin + password: test-password + expirationTime: 3600 + sessionSecret: "" +discovery: + - id: test + name: test + type: mock +` + require.NoError(t, os.WriteFile(path, []byte(contents), 0600)) + + cfg := DefaultAdminConfig() + err := config.Load(path, &cfg) + require.ErrorContains(t, err, "sessionSecret") +} diff --git a/pkg/config/console/auth/config.go b/pkg/config/console/auth/config.go index fd16d9b9d..c0e91e5f4 100644 --- a/pkg/config/console/auth/config.go +++ b/pkg/config/console/auth/config.go @@ -22,6 +22,7 @@ import ( "fmt" "net" "net/url" + "os" "regexp" "slices" "strings" @@ -30,8 +31,9 @@ import ( ) const ( - DefaultExpirationTime = 7200 - DefaultSessionSecret = "secret" + DefaultExpirationTime = 7200 + MinimumSessionSecretLength = 32 + SessionSecretEnvVar = "DUBBO_ADMIN_SESSION_SECRET" MethodPassword = "password" ProviderTypeGitHub = "github" @@ -89,8 +91,11 @@ func (c *Config) Validate() error { if c.ExpirationTime <= 0 || c.ExpirationTime >= 24*60*60 { return errors.New("auth: expirationTime should be greater than 0 and less than 86400") } - if c.SessionSecret == "" { - c.SessionSecret = DefaultSessionSecret + if strings.TrimSpace(c.SessionSecret) == "" { + c.SessionSecret = os.Getenv(SessionSecretEnvVar) + } + if strings.TrimSpace(c.SessionSecret) == "" || len([]byte(c.SessionSecret)) < MinimumSessionSecretLength { + return fmt.Errorf("auth: sessionSecret must contain at least %d bytes", MinimumSessionSecretLength) } for id, provider := range c.Providers { if err := validateProvider(id, &provider); err != nil { diff --git a/pkg/config/console/auth/config_test.go b/pkg/config/console/auth/config_test.go index 6ed1005fa..f795d7bbf 100644 --- a/pkg/config/console/auth/config_test.go +++ b/pkg/config/console/auth/config_test.go @@ -23,10 +23,15 @@ import ( ) func validConfig() *Config { - return &Config{User: "admin", Password: "secret", ExpirationTime: 3600} + return &Config{ + User: "admin", + Password: "secret", + ExpirationTime: 3600, + SessionSecret: strings.Repeat("s", MinimumSessionSecretLength), + } } -func TestConfigValidateDefaultsPasswordOnly(t *testing.T) { +func TestConfigValidatePasswordOnly(t *testing.T) { cfg := validConfig() if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) @@ -34,8 +39,37 @@ func TestConfigValidateDefaultsPasswordOnly(t *testing.T) { if len(cfg.Methods) != 1 || cfg.Methods[0] != MethodPassword { t.Fatalf("Methods = %v, want [%s]", cfg.Methods, MethodPassword) } - if cfg.SessionSecret != DefaultSessionSecret { - t.Fatalf("SessionSecret = %q, want legacy default", cfg.SessionSecret) + if cfg.SessionSecret != strings.Repeat("s", MinimumSessionSecretLength) { + t.Fatalf("SessionSecret = %q, want configured secret", cfg.SessionSecret) + } +} + +func TestConfigValidateRequiresSessionSecret(t *testing.T) { + t.Setenv(SessionSecretEnvVar, "") + cfg := validConfig() + cfg.SessionSecret = "" + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "sessionSecret") { + t.Fatalf("Validate() error = %v, want missing sessionSecret error", err) + } +} + +func TestConfigValidateLoadsSessionSecretFromEnvironment(t *testing.T) { + t.Setenv(SessionSecretEnvVar, strings.Repeat("e", MinimumSessionSecretLength)) + cfg := validConfig() + cfg.SessionSecret = "" + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + if cfg.SessionSecret != strings.Repeat("e", MinimumSessionSecretLength) { + t.Fatalf("SessionSecret = %q, want environment value", cfg.SessionSecret) + } +} + +func TestConfigValidateRejectsShortSessionSecret(t *testing.T) { + cfg := validConfig() + cfg.SessionSecret = "short" + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "32 bytes") { + t.Fatalf("Validate() error = %v, want minimum sessionSecret length error", err) } } diff --git a/pkg/config/console/config.go b/pkg/config/console/config.go index 0c04703da..80556984c 100644 --- a/pkg/config/console/config.go +++ b/pkg/config/console/config.go @@ -78,10 +78,6 @@ func (c *Config) Validate() error { if err := c.Auth.Validate(); err != nil { return err } - // Release deployments with external providers must reject the legacy default session secret and other short cookie-signing keys. - if c.GinMode == ReleaseMode && len(c.Auth.Providers) > 0 && len([]byte(c.Auth.SessionSecret)) < 32 { - return bizerror.New(bizerror.ConfigError, "auth sessionSecret must contain at least 32 bytes when providers are enabled in release mode") - } return nil } @@ -136,7 +132,6 @@ func DefaultConsoleConfig() *Config { User: "admin", Password: "admin", ExpirationTime: 3600, - SessionSecret: auth.DefaultSessionSecret, }, } } diff --git a/pkg/config/console/config_test.go b/pkg/config/console/config_test.go index 34469db2a..2ce5fdbbc 100644 --- a/pkg/config/console/config_test.go +++ b/pkg/config/console/config_test.go @@ -25,6 +25,7 @@ import ( ) func TestReleaseProviderRequiresStrongSessionSecret(t *testing.T) { + t.Setenv(auth.SessionSecretEnvVar, "") cfg := DefaultConsoleConfig() cfg.Auth.Providers = map[string]auth.ProviderConfig{ "github": { @@ -43,6 +44,19 @@ func TestReleaseProviderRequiresStrongSessionSecret(t *testing.T) { } } +func TestPasswordAuthenticationRequiresStrongSessionSecret(t *testing.T) { + t.Setenv(auth.SessionSecretEnvVar, "") + cfg := DefaultConsoleConfig() + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "sessionSecret") { + t.Fatalf("Validate() error = %v, want sessionSecret error", err) + } + + cfg.Auth.SessionSecret = "a-long-deployment-specific-session-secret" + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate() with strong secret error = %v", err) + } +} + func TestReleaseProviderRejectsShortSessionSecret(t *testing.T) { cfg := DefaultConsoleConfig() cfg.Auth.Providers = map[string]auth.ProviderConfig{ @@ -57,16 +71,17 @@ func TestReleaseProviderRejectsShortSessionSecret(t *testing.T) { } } -func TestDebugProviderAllowsLegacySessionSecret(t *testing.T) { +func TestDebugProviderRejectsShortSessionSecret(t *testing.T) { cfg := DefaultConsoleConfig() cfg.GinMode = DebugMode + cfg.Auth.SessionSecret = "short" cfg.Auth.Providers = map[string]auth.ProviderConfig{ "github": { Type: auth.ProviderTypeGitHub, ClientID: "id", ClientSecret: "secret", RedirectURL: "http://localhost:8888/api/v1/auth/providers/github/callback", PostLoginRedirectURL: "http://localhost:8881/admin/", }, } - if err := cfg.Validate(); err != nil { - t.Fatalf("Validate() error = %v", err) + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "32 bytes") { + t.Fatalf("Validate() error = %v, want minimum sessionSecret length error", err) } } diff --git a/pkg/config/display_test.go b/pkg/config/display_test.go index 4694dbe8f..84da710f9 100644 --- a/pkg/config/display_test.go +++ b/pkg/config/display_test.go @@ -22,11 +22,13 @@ import ( "github.com/apache/dubbo-admin/pkg/config" "github.com/apache/dubbo-admin/pkg/config/app" + "github.com/apache/dubbo-admin/pkg/config/console" configauth "github.com/apache/dubbo-admin/pkg/config/console/auth" ) func TestConfigForDisplaySanitizesConsoleAuthenticationSecrets(t *testing.T) { cfg := app.DefaultAdminConfig() + cfg.Console = console.DefaultConsoleConfig() cfg.Console.Auth.Password = "password-secret" cfg.Console.Auth.SessionSecret = "session-secret" cfg.Console.Auth.Providers = map[string]configauth.ProviderConfig{ diff --git a/release/kubernetes/dubbo-system/README.md b/release/kubernetes/dubbo-system/README.md new file mode 100644 index 000000000..b22938423 --- /dev/null +++ b/release/kubernetes/dubbo-system/README.md @@ -0,0 +1,46 @@ + + +# Kubernetes deployment + +The bundled `apache/dubbo-admin:0.7.0` image predates the session-secret fix. +Until a fixed official image is published, build this branch and make its image +available to your cluster. From the repository root, deploy it with: + +```sh +DUBBO_ADMIN_IMAGE= ./release/kubernetes/dubbo-system/deploy.sh +``` + +The script requires an explicit image built from the fixed source, renders it +into the Deployment before applying anything, and rejects the known old +`0.7.0` tag and `latest`. It cannot verify the contents of a supplied image; +the operator must provide a fixed build. +It then creates the `dubbo-system` namespace if needed, generates a random +`session-secret` in the `dubbo-admin-auth` Kubernetes Secret on first install, +and applies the manifests. It keeps the existing Secret on later runs, so the +signing key remains stable across upgrades. An existing Secret without +`session-secret`, or with a value shorter than 32 bytes, stops deployment. + +This command requires `kubectl` access to create the namespace, Secret, and +manifest resources, plus `openssl` for the initial key generation. An image +reference pinned by `@sha256:` is also accepted. Do not apply these +manifests directly with `kubectl apply -f`: that bypasses the image check and +uses the old image. + +When publishing the fix, update the manifest and script to use a release built +from the fixed source by default. Merely injecting a Secret into the old image +does not change its cookie-signing behavior. diff --git a/release/kubernetes/dubbo-system/deploy.sh b/release/kubernetes/dubbo-system/deploy.sh new file mode 100755 index 000000000..e634ecc0a --- /dev/null +++ b/release/kubernetes/dubbo-system/deploy.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# 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. + +set -euo pipefail + +namespace=dubbo-system +secret_name=dubbo-admin-auth +manifest_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +image="${DUBBO_ADMIN_IMAGE:-}" + +if [[ -z "$image" ]]; then + printf 'Set DUBBO_ADMIN_IMAGE to an image built with the session-secret fix; the bundled 0.7.0 image is vulnerable.\n' >&2 + exit 1 +fi + +if [[ "$image" == *@sha256:* ]]; then + image_name="${image%@sha256:*}" + image_digest="${image##*@sha256:}" + if [[ ! "$image_name" =~ ^[A-Za-z0-9._:/-]+$ || ! "$image_digest" =~ ^[a-fA-F0-9]{64}$ ]]; then + printf 'DUBBO_ADMIN_IMAGE must be a valid image reference with a sha256 digest.\n' >&2 + exit 1 + fi + image_override=" digest: sha256:$image_digest" +else + image_name="${image%:*}" + image_tag="${image##*:}" + if [[ "$image_name" == "$image" || ! "$image_name" =~ ^[A-Za-z0-9._:/-]+$ || ! "$image_tag" =~ ^[A-Za-z0-9_.-]+$ || "$image_tag" == 0.7.0 || "$image_tag" == latest ]]; then + printf 'DUBBO_ADMIN_IMAGE must use an explicit fixed-image tag other than 0.7.0 or latest.\n' >&2 + exit 1 + fi + image_override=" newTag: $image_tag" +fi + +for command in kubectl openssl; do + if ! command -v "$command" >/dev/null 2>&1; then + printf 'Required command not found: %s\n' "$command" >&2 + exit 1 + fi +done + +render_dir="$(mktemp -d)" +trap 'rm -rf "$render_dir"' EXIT +cp "$manifest_dir"/*.yaml "$render_dir"/ +{ + printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n' + for manifest in "$manifest_dir"/*.yaml; do + printf ' - %s\n' "$(basename "$manifest")" + done + printf 'images:\n - name: apache/dubbo-admin\n newName: %s\n%s\n' "$image_name" "$image_override" +} > "$render_dir/kustomization.yaml" +rendered_manifest="$(kubectl kustomize "$render_dir")" + +if ! kubectl get namespace "$namespace" >/dev/null 2>&1; then + kubectl create namespace "$namespace" +fi + +if kubectl -n "$namespace" get secret "$secret_name" >/dev/null 2>&1; then + secret_value="$(kubectl -n "$namespace" get secret "$secret_name" -o jsonpath='{.data.session-secret}')" + if [[ -z "$secret_value" ]]; then + printf 'Secret %s/%s is missing session-secret. Fix it before deploying.\n' "$namespace" "$secret_name" >&2 + exit 1 + fi + secret_length="$(printf '%s' "$secret_value" | openssl base64 -d -A | wc -c | tr -d '[:space:]')" + if (( secret_length < 32 )); then + printf 'Secret %s/%s has a session-secret shorter than 32 bytes. Replace it before deploying.\n' "$namespace" "$secret_name" >&2 + exit 1 + fi +else + generated_secret="$(openssl rand -hex 32)" + printf '%s' "$generated_secret" | kubectl -n "$namespace" create secret generic "$secret_name" --from-file=session-secret=/dev/stdin +fi + +printf '%s\n' "$rendered_manifest" | kubectl apply -f - diff --git a/release/kubernetes/dubbo-system/dubbo-admin.yaml b/release/kubernetes/dubbo-system/dubbo-admin.yaml index 8d5ffa14a..7f0db0223 100644 --- a/release/kubernetes/dubbo-system/dubbo-admin.yaml +++ b/release/kubernetes/dubbo-system/dubbo-admin.yaml @@ -12,8 +12,10 @@ # 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. - -apiVersion: apps/v1 +# Run ./deploy.sh with DUBBO_ADMIN_IMAGE set to a fixed image. Do not apply +# this file directly: the pinned 0.7.0 image predates the session-secret fix. + +apiVersion: apps/v1 kind: Deployment metadata: name: dubbo-admin @@ -39,9 +41,15 @@ spec: serviceAccountName: dubbo-admin containers: - name: dubbo-admin - image: apache/dubbo-admin:0.7.0 - imagePullPolicy: IfNotPresent - volumeMounts: + image: apache/dubbo-admin:0.7.0 + imagePullPolicy: IfNotPresent + env: + - name: DUBBO_ADMIN_SESSION_SECRET + valueFrom: + secretKeyRef: + name: dubbo-admin-auth + key: session-secret + volumeMounts: - mountPath: /etc/dubbo-admin name: dubbo-admin-config readOnly: true @@ -97,6 +105,8 @@ data: grafana: http://grafana.monitoring.svc:3000 prometheus: http://prometheus-k8s.monitoring.svc:9090/ console: + port: 8888 + ginMode: release metricDashboards: application: http://grafana.monitoring.svc:3000/d/a0b114ca-edf7-4dfe-ac2c-34a4fc545fed?kiosk=1&theme=light instance: http://grafana.monitoring.svc:3000/d/dcf5defe-d198-4704-9edf-6520838880e9?kiosk=1&theme=light @@ -108,6 +118,7 @@ data: auth: user: admin password: dubbo@2025 + # sessionSecret is injected through DUBBO_ADMIN_SESSION_SECRET. expirationTime: 3600 ruleVersioning: maxVersionsPerRule: 20