diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index 3f8b6537..b692794b 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -60,7 +60,7 @@ A Web restart, including one caused by `oac apply`, signs everyone out of the co ## Core key -Each installation has one administrator credential, the Core key. The installer generates a 64-character random key in `data/secrets/web/core.key`. Read it with `oac core-key --show`; the file is owned by the container user. The Core key: +Each installation has one administrator credential, the Core key. The installer generates a key with the `oac_admin_` prefix followed by 64 random lowercase hexadecimal characters in `data/secrets/web/core.key`. Read it with `oac core-key --show`; the file is owned by the container user. The Core key: - signs in to Web. The browser gets an HttpOnly session cookie, never the key; - authorizes Core API (`/core/v1`) requests sent as `Authorization: Bearer `; diff --git a/docs/zh/getting-started/operations.md b/docs/zh/getting-started/operations.md index 4a3755df..facb1309 100644 --- a/docs/zh/getting-started/operations.md +++ b/docs/zh/getting-started/operations.md @@ -1,7 +1,7 @@ --- title: "管理你的安装" source: docs/getting-started/operations.md -source_hash: e87c7a1e577b8732929bb1c1fead1d54abfe6b607d8c4aed43faf38dca405ed5 +source_hash: e60a6e96cd61692b6adc7664874ba0ed2ce489982e7da2fe2347631ee2a94c0a --- 安装运维人员负责 Core 主机、存储和可用性。节点主机运行各自的服务;参阅[节点](nodes.md)。设置见[配置参考](../configuration.md)。 @@ -62,7 +62,7 @@ Web 重启(包括 `oac apply` 引起的重启)会让所有控制台用户退 ## Core 密钥 {#core-key} -每个安装有一个管理员凭据,即 Core 密钥。安装程序在 `data/secrets/web/core.key` 生成 64 字符随机密钥。用 `oac core-key --show` 读取;该文件属于容器用户。Core 密钥: +每个安装有一个管理员凭据,即 Core 密钥。安装程序在 `data/secrets/web/core.key` 生成以 `oac_admin_` 为前缀、后接 64 个随机小写十六进制字符的密钥。用 `oac core-key --show` 读取;该文件属于容器用户。Core 密钥: - 用于登录 Web。浏览器获得 HttpOnly 会话 cookie,不持有密钥; - 通过 `Authorization: Bearer ` 授权 Core API(`/core/v1`)请求; diff --git a/services/core/cmd/oac/init.go b/services/core/cmd/oac/init.go index 3d9f3a68..bb5a5405 100644 --- a/services/core/cmd/oac/init.go +++ b/services/core/cmd/oac/init.go @@ -254,7 +254,13 @@ func initialize(root string, release releaseIdentity, fetch func() (map[string][ name string generate func() string }{ - {"secrets/web/core.key", func() string { return randomHex(32) }}, + {"secrets/web/core.key", func() string { + key, err := generateCoreKey() + if err != nil { + panic(err) + } + return key + }}, {"secrets/database/password", func() string { return randomHex(32) }}, {"secrets/core/credential.key", func() string { return base64.StdEncoding.EncodeToString(randomBytes(32)) }}, {"secrets/core/installation.id", func() string { return uuid.NewString() }}, diff --git a/services/core/cmd/oac/init_test.go b/services/core/cmd/oac/init_test.go index 69a9c7d4..c4b89db7 100644 --- a/services/core/cmd/oac/init_test.go +++ b/services/core/cmd/oac/init_test.go @@ -67,9 +67,7 @@ func TestInitializeKeepsIdentityAndKeysAcrossRestarts(t *testing.T) { } saved := snapshot(t, root) key := strings.TrimSpace(saved["secrets/web/core.key"]) - if len(key) != 64 { - t.Fatalf("core key has %d characters", len(key)) - } + assertCoreKeyFormat(t, key) var digests []string if err := json.Unmarshal([]byte(saved["secrets/core/core-key-digests.json"]), &digests); err != nil || len(digests) != 1 || digests[0] != keyDigest(key) { t.Fatalf("digests = %v, %v", digests, err) diff --git a/services/core/cmd/oac/main.go b/services/core/cmd/oac/main.go index f18683d9..e2159239 100644 --- a/services/core/cmd/oac/main.go +++ b/services/core/cmd/oac/main.go @@ -115,12 +115,19 @@ func coreKeyCommand(ctx context.Context, in installation, runner Runner, args [] return errors.New("Usage: oac core-key [--show]") } -func rotateCoreKey(ctx context.Context, in installation, runner Runner) error { +func generateCoreKey() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { + return "", err + } + return "oac_admin_" + hex.EncodeToString(buf), nil +} + +func rotateCoreKey(ctx context.Context, in installation, runner Runner) error { + key, err := generateCoreKey() + if err != nil { return err } - key := hex.EncodeToString(buf) keyPath := filepath.Join(in.data, "secrets", "web", "core.key") digestPath := filepath.Join(in.data, "secrets", "core", "core-key-digests.json") if err := writeSecret(keyPath, key+"\n"); err != nil { diff --git a/services/core/cmd/oac/oac_test.go b/services/core/cmd/oac/oac_test.go index fb2789c1..ee865c21 100644 --- a/services/core/cmd/oac/oac_test.go +++ b/services/core/cmd/oac/oac_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "regexp" "strings" "testing" ) @@ -53,6 +54,7 @@ func TestRotateCoreKeyDigestDoesNotEchoTheKey(t *testing.T) { t.Fatal(err) } key := strings.TrimSpace(string(raw)) + assertCoreKeyFormat(t, key) digest, err := os.ReadFile(filepath.Join(in.data, "secrets", "core", "core-key-digests.json")) if err != nil || !strings.Contains(string(digest), keyDigest(key)) || strings.Contains(string(digest), key) { t.Fatalf("digest %s key leaked %v", digest, err) @@ -70,3 +72,10 @@ func (s scriptedRunner) Run(_ context.Context, args ...string) error { return s. func (s scriptedRunner) Output(context.Context, ...string) ([]byte, error) { return nil, nil } + +func assertCoreKeyFormat(t *testing.T, key string) { + t.Helper() + if !regexp.MustCompile(`^oac_admin_[0-9a-f]{64}$`).MatchString(key) { + t.Fatal("core key must have the oac_admin_ prefix and 64 lowercase hexadecimal characters") + } +} diff --git a/services/web/auth.go b/services/web/auth.go index 36bc2dac..16f23f64 100644 --- a/services/web/auth.go +++ b/services/web/auth.go @@ -18,7 +18,7 @@ const sessionCookie = "core_console_session" const sessionLifetime = 12 * time.Hour // minimumCoreKeyLength keeps guessing infeasible even though a correct key is -// never rate limited. Installer-generated keys have 64 characters. +// never rate limited. Installer-generated keys carry 256 bits of random entropy. const minimumCoreKeyLength = 32 // consoleAuth signs the browser in with the Core key. Sessions live only in