diff --git a/cmd/openbao.go b/cmd/openbao.go index 360e771..5615ec9 100644 --- a/cmd/openbao.go +++ b/cmd/openbao.go @@ -76,9 +76,9 @@ func setupOpenBaoAuthentication(manager ctrl.Manager, options openBaoOptions) er if err != nil { return err } - // 与 reconcile 共用 GetConfig:支持 --kubeconfig/KUBECONFIG 和 in-cluster,绝不推断 Pod 文件位置。 + // 复用 manager 已装配的 Kubernetes client,不重复加载配置或创建客户端。 session, err := openbao.NewKubernetesSession( - client, manager.GetConfig(), options.mount, options.role, options.identity, + client, manager.GetClient(), options.mount, options.role, options.identity, ) if err != nil { return err diff --git a/docs/database/README.md b/docs/database/README.md index b384397..e6acf3d 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -175,11 +175,13 @@ Database 状态中的稳定位置和已确认步骤、供应 service/controller ## OpenBao Kubernetes 认证会话 `adapter/openbao.KubernetesSession` 复用官方 Kubernetes auth helper 和 `LifetimeWatcher` -(均为 v2.7.0)。认证与 reconcile 共用 manager 的 Kubernetes 配置:标准 `--kubeconfig` / +(均为 v2.7.0)。认证直接注入 `manager.GetClient()`,与 reconcile 共用已装配的 Kubernetes +client,不从配置另建客户端。标准 `--kubeconfig` / `KUBECONFIG` 支持 systemd 或其他集群外运行方式,集群内使用 in-cluster 配置,不要求存在 Pod。 Kubernetes 身份的签发和更新由部署管理及 client-go 的认证机制负责,不另建 kubeconfig 读取器。 -每次登录前,使用该身份调用固定 namespace/name 的 ServiceAccount TokenRequest,申请 +每次登录前,通过该 client 的 `SubResource("token").Create` 调用固定 namespace/name 的 +ServiceAccount TokenRequest;写入直连 API server,不读取 cache 或要求额外的 SA get 权限。申请 audience 匹配 OpenBao role、期望有效期 600 秒的短期 JWT;检查返回值非空且未过期,再交给 官方 Kubernetes auth helper。JWT 不缓存,不读取投射文件,也不回退静态 OpenBao token; 实际 JWT 有效期由 API server 决定。RBAC 拒绝或 TokenRequest 失败时不会继续 Bao 登录。 diff --git a/internal/database/adapter/openbao/authentication.go b/internal/database/adapter/openbao/authentication.go index 7b9a0ed..6f0123c 100644 --- a/internal/database/adapter/openbao/authentication.go +++ b/internal/database/adapter/openbao/authentication.go @@ -27,10 +27,9 @@ import ( kubernetesauth "github.com/openbao/openbao/api/auth/kubernetes/v2" bao "github.com/openbao/openbao/api/v2" authenticationv1 "k8s.io/api/authentication/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/rest" + kubeclient "sigs.k8s.io/controller-runtime/pkg/client" ) var ErrAuthenticationConfiguration = errors.New("invalid OpenBao Kubernetes authentication configuration") @@ -39,13 +38,13 @@ var ErrAuthenticationConfiguration = errors.New("invalid OpenBao Kubernetes auth // 每次登录通过 manager 的 Kubernetes 身份申请新的 SA JWT,不依赖 controller 的部署位置。 // 续期调度由官方 LifetimeWatcher 负责,不实现自己的 lease 算法。 type KubernetesSession struct { - client *bao.Client - mount string - role string - accounts corev1.ServiceAccountInterface - identity KubernetesIdentity - running sync.Mutex - ready atomic.Bool + client *bao.Client + mount string + role string + kubernetes kubeclient.Client + identity KubernetesIdentity + running sync.Mutex + ready atomic.Bool } // KubernetesIdentity 是部署固定的登录目标,不由 Tenant 选择。 @@ -55,22 +54,26 @@ type KubernetesIdentity struct { Audience string } -func NewKubernetesSession(client *bao.Client, config *rest.Config, mount, role string, identity KubernetesIdentity) (*KubernetesSession, error) { - if client == nil || config == nil || !validPath(mount) || !pathSegment.MatchString(role) || +func NewKubernetesSession( + client *bao.Client, + kubernetes kubeclient.Client, + mount, role string, + identity KubernetesIdentity, +) (*KubernetesSession, error) { + if client == nil || kubernetes == nil || !validPath(mount) || !pathSegment.MatchString(role) || len(validation.IsDNS1123Label(identity.Namespace)) != 0 || len(validation.IsDNS1123Subdomain(identity.ServiceAccount)) != 0 || strings.TrimSpace(identity.Audience) == "" { return nil, ErrAuthenticationConfiguration } - api, err := corev1.NewForConfig(config) - if err != nil { - return nil, ErrAuthenticationConfiguration - } client.ClearToken() client.SetMaxRetries(0) client.SetClientTimeout(15 * time.Second) return &KubernetesSession{ - client: client, mount: mount, role: role, - accounts: api.ServiceAccounts(identity.Namespace), identity: identity, + client: client, + mount: mount, + role: role, + kubernetes: kubernetes, + identity: identity, }, nil } @@ -113,13 +116,19 @@ func (s *KubernetesSession) login(ctx context.Context) *bao.Secret { // JWT 只用于本次登录,不缓存或自行解析 kubeconfig 中的凭据。 // client-go 负责 kubeconfig/in-cluster 身份与凭据更新;API server 按 RBAC 签发。 expirationSeconds := int64(600) - token, err := s.accounts.CreateToken(ctx, s.identity.ServiceAccount, &authenticationv1.TokenRequest{ + account := &corev1.ServiceAccount{ + Namespace: s.identity.Namespace, + Name: s.identity.ServiceAccount, + } + token := &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ Audiences: []string{s.identity.Audience}, ExpirationSeconds: &expirationSeconds, }, - }, metav1.CreateOptions{}) - if err != nil || token == nil || strings.TrimSpace(token.Status.Token) == "" || + } + // 子资源写入直接请求 API server,不读 cache,也不需要额外的 ServiceAccount get 权限。 + err := s.kubernetes.SubResource("token").Create(ctx, account, token) + if err != nil || strings.TrimSpace(token.Status.Token) == "" || !token.Status.ExpirationTimestamp.After(time.Now()) { return nil } diff --git a/internal/database/adapter/openbao/authentication_integration_test.go b/internal/database/adapter/openbao/authentication_integration_test.go index bd40390..f48f7a3 100644 --- a/internal/database/adapter/openbao/authentication_integration_test.go +++ b/internal/database/adapter/openbao/authentication_integration_test.go @@ -266,7 +266,7 @@ func TestKubernetesSessionWithRealTokenReview(t *testing.T) { if err != nil { t.Fatal("cannot construct external controller manager") } - session, err := openbao.NewKubernetesSession(client, manager.GetConfig(), "kubernetes", authRole, testIdentity) + session, err := openbao.NewKubernetesSession(client, manager.GetClient(), "kubernetes", authRole, testIdentity) if err != nil { t.Fatal(err) } diff --git a/internal/database/adapter/openbao/authentication_test.go b/internal/database/adapter/openbao/authentication_test.go index 6c2bec5..4e262c8 100644 --- a/internal/database/adapter/openbao/authentication_test.go +++ b/internal/database/adapter/openbao/authentication_test.go @@ -26,8 +26,12 @@ import ( "time" authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" + kubeclient "sigs.k8s.io/controller-runtime/pkg/client" "git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao" ) @@ -38,8 +42,18 @@ const ( var testIdentity = openbao.KubernetesIdentity{Namespace: "bao-controller", ServiceAccount: authRole, Audience: "openbao"} -func authenticationConfig(address string) *rest.Config { - return &rest.Config{Host: address, ContentType: "application/json"} +func authenticationClient(t *testing.T, address string) kubeclient.Client { + t.Helper() + // HTTP 单元 fixture 只提供 TokenRequest;静态映射避免额外模拟 discovery API。 + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion}) + mapper.Add(corev1.SchemeGroupVersion.WithKind("ServiceAccount"), meta.RESTScopeNamespace) + client, err := kubeclient.New(&rest.Config{Host: address, ContentType: "application/json"}, kubeclient.Options{ + Mapper: mapper, + }) + if err != nil { + t.Fatal("cannot construct fixture Kubernetes client") + } + return client } func authenticationServer(t *testing.T, token func() (string, bool), login http.HandlerFunc) *httptest.Server { @@ -81,7 +95,7 @@ func TestKubernetesSessionDoesNotFallbackFromMissingToken(t *testing.T) { }) defer server.Close() client := fixtureClient(t, server.URL) - session, err := openbao.NewKubernetesSession(client, authenticationConfig(server.URL), "kubernetes", authRole, testIdentity) + session, err := openbao.NewKubernetesSession(client, authenticationClient(t, server.URL), "kubernetes", authRole, testIdentity) if err != nil { t.Fatal(err) } @@ -157,7 +171,7 @@ func TestKubernetesSessionRequestsNewTokenAfterFailure(t *testing.T) { }) defer server.Close() client := fixtureClient(t, server.URL) - session, err := openbao.NewKubernetesSession(client, authenticationConfig(server.URL), "kubernetes", authRole, testIdentity) + session, err := openbao.NewKubernetesSession(client, authenticationClient(t, server.URL), "kubernetes", authRole, testIdentity) if err != nil { t.Fatal(err) } @@ -193,7 +207,7 @@ func TestKubernetesSessionRejectsUnboundedLease(t *testing.T) { }) defer server.Close() client := fixtureClient(t, server.URL) - session, err := openbao.NewKubernetesSession(client, authenticationConfig(server.URL), "kubernetes", authRole, testIdentity) + session, err := openbao.NewKubernetesSession(client, authenticationClient(t, server.URL), "kubernetes", authRole, testIdentity) if err != nil { t.Fatal(err) }