feat: OpenBao Kubernetes 认证与短期会话续期 #13
+2
-2
@@ -76,9 +76,9 @@ func setupOpenBaoAuthentication(manager ctrl.Manager, options openBaoOptions) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 与 reconcile 共用 GetConfig:支持 --kubeconfig/KUBECONFIG 和 in-cluster,绝不推断 Pod 文件位置。
|
// 复用 manager 已装配的 Kubernetes client,不重复加载配置或创建客户端。
|
||||||
session, err := openbao.NewKubernetesSession(
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -175,11 +175,13 @@ Database 状态中的稳定位置和已确认步骤、供应 service/controller
|
|||||||
## OpenBao Kubernetes 认证会话
|
## OpenBao Kubernetes 认证会话
|
||||||
|
|
||||||
`adapter/openbao.KubernetesSession` 复用官方 Kubernetes auth helper 和 `LifetimeWatcher`
|
`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。
|
`KUBECONFIG` 支持 systemd 或其他集群外运行方式,集群内使用 in-cluster 配置,不要求存在 Pod。
|
||||||
Kubernetes 身份的签发和更新由部署管理及 client-go 的认证机制负责,不另建 kubeconfig 读取器。
|
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;检查返回值非空且未过期,再交给
|
audience 匹配 OpenBao role、期望有效期 600 秒的短期 JWT;检查返回值非空且未过期,再交给
|
||||||
官方 Kubernetes auth helper。JWT 不缓存,不读取投射文件,也不回退静态 OpenBao token;
|
官方 Kubernetes auth helper。JWT 不缓存,不读取投射文件,也不回退静态 OpenBao token;
|
||||||
实际 JWT 有效期由 API server 决定。RBAC 拒绝或 TokenRequest 失败时不会继续 Bao 登录。
|
实际 JWT 有效期由 API server 决定。RBAC 拒绝或 TokenRequest 失败时不会继续 Bao 登录。
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ import (
|
|||||||
kubernetesauth "github.com/openbao/openbao/api/auth/kubernetes/v2"
|
kubernetesauth "github.com/openbao/openbao/api/auth/kubernetes/v2"
|
||||||
bao "github.com/openbao/openbao/api/v2"
|
bao "github.com/openbao/openbao/api/v2"
|
||||||
authenticationv1 "k8s.io/api/authentication/v1"
|
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"
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrAuthenticationConfiguration = errors.New("invalid OpenBao Kubernetes authentication configuration")
|
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 的部署位置。
|
// 每次登录通过 manager 的 Kubernetes 身份申请新的 SA JWT,不依赖 controller 的部署位置。
|
||||||
// 续期调度由官方 LifetimeWatcher 负责,不实现自己的 lease 算法。
|
// 续期调度由官方 LifetimeWatcher 负责,不实现自己的 lease 算法。
|
||||||
type KubernetesSession struct {
|
type KubernetesSession struct {
|
||||||
client *bao.Client
|
client *bao.Client
|
||||||
mount string
|
mount string
|
||||||
role string
|
role string
|
||||||
accounts corev1.ServiceAccountInterface
|
kubernetes kubeclient.Client
|
||||||
identity KubernetesIdentity
|
identity KubernetesIdentity
|
||||||
running sync.Mutex
|
running sync.Mutex
|
||||||
ready atomic.Bool
|
ready atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesIdentity 是部署固定的登录目标,不由 Tenant 选择。
|
// KubernetesIdentity 是部署固定的登录目标,不由 Tenant 选择。
|
||||||
@@ -55,22 +54,26 @@ type KubernetesIdentity struct {
|
|||||||
Audience string
|
Audience string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKubernetesSession(client *bao.Client, config *rest.Config, mount, role string, identity KubernetesIdentity) (*KubernetesSession, error) {
|
func NewKubernetesSession(
|
||||||
if client == nil || config == nil || !validPath(mount) || !pathSegment.MatchString(role) ||
|
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.IsDNS1123Label(identity.Namespace)) != 0 ||
|
||||||
len(validation.IsDNS1123Subdomain(identity.ServiceAccount)) != 0 || strings.TrimSpace(identity.Audience) == "" {
|
len(validation.IsDNS1123Subdomain(identity.ServiceAccount)) != 0 || strings.TrimSpace(identity.Audience) == "" {
|
||||||
return nil, ErrAuthenticationConfiguration
|
return nil, ErrAuthenticationConfiguration
|
||||||
}
|
}
|
||||||
api, err := corev1.NewForConfig(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrAuthenticationConfiguration
|
|
||||||
}
|
|
||||||
client.ClearToken()
|
client.ClearToken()
|
||||||
client.SetMaxRetries(0)
|
client.SetMaxRetries(0)
|
||||||
client.SetClientTimeout(15 * time.Second)
|
client.SetClientTimeout(15 * time.Second)
|
||||||
return &KubernetesSession{
|
return &KubernetesSession{
|
||||||
client: client, mount: mount, role: role,
|
client: client,
|
||||||
accounts: api.ServiceAccounts(identity.Namespace), identity: identity,
|
mount: mount,
|
||||||
|
role: role,
|
||||||
|
kubernetes: kubernetes,
|
||||||
|
identity: identity,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,13 +116,19 @@ func (s *KubernetesSession) login(ctx context.Context) *bao.Secret {
|
|||||||
// JWT 只用于本次登录,不缓存或自行解析 kubeconfig 中的凭据。
|
// JWT 只用于本次登录,不缓存或自行解析 kubeconfig 中的凭据。
|
||||||
// client-go 负责 kubeconfig/in-cluster 身份与凭据更新;API server 按 RBAC 签发。
|
// client-go 负责 kubeconfig/in-cluster 身份与凭据更新;API server 按 RBAC 签发。
|
||||||
expirationSeconds := int64(600)
|
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{
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
Audiences: []string{s.identity.Audience},
|
Audiences: []string{s.identity.Audience},
|
||||||
ExpirationSeconds: &expirationSeconds,
|
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()) {
|
!token.Status.ExpirationTimestamp.After(time.Now()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func TestKubernetesSessionWithRealTokenReview(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("cannot construct external controller manager")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
authenticationv1 "k8s.io/api/authentication/v1"
|
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"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
"git.ddupan.top/panxiao81/ayatori/internal/database/adapter/openbao"
|
"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"}
|
var testIdentity = openbao.KubernetesIdentity{Namespace: "bao-controller", ServiceAccount: authRole, Audience: "openbao"}
|
||||||
|
|
||||||
func authenticationConfig(address string) *rest.Config {
|
func authenticationClient(t *testing.T, address string) kubeclient.Client {
|
||||||
return &rest.Config{Host: address, ContentType: "application/json"}
|
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 {
|
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()
|
defer server.Close()
|
||||||
client := fixtureClient(t, server.URL)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +171,7 @@ func TestKubernetesSessionRequestsNewTokenAfterFailure(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
client := fixtureClient(t, server.URL)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -193,7 +207,7 @@ func TestKubernetesSessionRejectsUnboundedLease(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
client := fixtureClient(t, server.URL)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user