Server API

概述

SPIRE Server 提供 gRPC API 用于管理身份和配置。API 分为以下服务:

  • Agent API: Agent 证明和管理

  • Entry API: 注册条目管理

  • Bundle API: 信任包管理

  • SVID API: SVID 颁发

  • TrustDomain API: 联邦管理

认证和授权

认证方式

  1. Unix Socket: 本地管理

  2. mTLS: Agent 和远程管理

  3. Admin Token: 可选的管理令牌

授权

SPIRE 使用基于策略的授权(OPA Rego):

package spire

default allow = false

# 允许管理员执行所有操作
allow {
    input.caller.admin == true
}

# 允许 Agent 更新自己的条目
allow {
    input.method == "entry.Entry/BatchUpdateEntry"
    input.caller.downstream == true
}

Entry API

创建条目

import (
    "context"
    entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1"
    "github.com/spiffe/spire-api-sdk/proto/spire/api/types"
)

func createEntry(client entryv1.EntryClient) error {
    _, err := client.BatchCreateEntry(context.Background(), &entryv1.BatchCreateEntryRequest{
        Entries: []*types.Entry{
            {
                SpiffeId: &types.SPIFFEID{
                    TrustDomain: "example.org",
                    Path:        "/myworkload",
                },
                ParentId: &types.SPIFFEID{
                    TrustDomain: "example.org",
                    Path:        "/myagent",
                },
                Selectors: []*types.Selector{
                    {Type: "unix", Value: "uid:1000"},
                },
                X509SvidTtl: 3600,
            },
        },
    })
    return err
}

列出条目

func listEntries(client entryv1.EntryClient) ([]*types.Entry, error) {
    resp, err := client.ListEntries(context.Background(), &entryv1.ListEntriesRequest{
        Filter: &entryv1.ListEntriesRequest_Filter{
            ByParentId: &types.SPIFFEID{
                TrustDomain: "example.org",
                Path:        "/myagent",
            },
        },
    })
    if err != nil {
        return nil, err
    }
    return resp.Entries, nil
}

更新条目

func updateEntry(client entryv1.EntryClient, entryID string) error {
    _, err := client.BatchUpdateEntry(context.Background(), &entryv1.BatchUpdateEntryRequest{
        Entries: []*types.Entry{
            {
                Id: entryID,
                Selectors: []*types.Selector{
                    {Type: "unix", Value: "uid:2000"},
                },
            },
        },
    })
    return err
}

删除条目

func deleteEntry(client entryv1.EntryClient, entryID string) error {
    _, err := client.BatchDeleteEntry(context.Background(), &entryv1.BatchDeleteEntryRequest{
        Ids: []string{entryID},
    })
    return err
}

Agent API

列出 Agent

import agentv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/agent/v1"

func listAgents(client agentv1.AgentClient) ([]*types.Agent, error) {
    resp, err := client.ListAgents(context.Background(), &agentv1.ListAgentsRequest{})
    if err != nil {
        return nil, err
    }
    return resp.Agents, nil
}

创建 Join Token

func createJoinToken(client agentv1.AgentClient) (string, error) {
    resp, err := client.CreateJoinToken(context.Background(), &agentv1.CreateJoinTokenRequest{
        Ttl: 600, // 10 分钟
        AgentId: &types.SPIFFEID{
            TrustDomain: "example.org",
            Path:        "/myagent",
        },
    })
    if err != nil {
        return "", err
    }
    return resp.Value, nil
}

驱逐 Agent

func evictAgent(client agentv1.AgentClient, spiffeID *types.SPIFFEID) error {
    _, err := client.DeleteAgent(context.Background(), &agentv1.DeleteAgentRequest{
        Id: spiffeID,
    })
    return err
}

Bundle API

获取本地 Bundle

import bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1"

func getBundle(client bundlev1.BundleClient) (*types.Bundle, error) {
    return client.GetBundle(context.Background(), &bundlev1.GetBundleRequest{})
}

设置联邦 Bundle

func setFederatedBundle(client bundlev1.BundleClient, bundle *types.Bundle) error {
    _, err := client.BatchSetFederatedBundle(context.Background(), &bundlev1.BatchSetFederatedBundleRequest{
        Bundle: []*types.Bundle{bundle},
    })
    return err
}

SVID API

颁发 X.509-SVID

import svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1"

func mintX509SVID(client svidv1.SVIDClient) (*types.X509SVID, error) {
    resp, err := client.MintX509SVID(context.Background(), &svidv1.MintX509SVIDRequest{
        Csr: csrBytes, // PKCS#10 CSR
        Ttl: 3600,
    })
    if err != nil {
        return nil, err
    }
    return resp.Svid, nil
}

颁发 JWT-SVID

func mintJWTSVID(client svidv1.SVIDClient) (string, error) {
    resp, err := client.MintJWTSVID(context.Background(), &svidv1.MintJWTSVIDRequest{
        Id: &types.SPIFFEID{
            TrustDomain: "example.org",
            Path:        "/myworkload",
        },
        Audience: []string{"https://api.example.org"},
        Ttl:      300,
    })
    if err != nil {
        return "", err
    }
    return resp.Svid.Token, nil
}

连接示例

通过 Unix Socket

import (
    "net"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func connectViaSocket(socketPath string) (*grpc.ClientConn, error) {
    return grpc.Dial(
        socketPath,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
            return net.Dial("unix", addr)
        }),
    )
}

通过 mTLS

import (
    "crypto/tls"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func connectViaMTLS(address string, tlsConfig *tls.Config) (*grpc.ClientConn, error) {
    return grpc.Dial(
        address,
        grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
    )
}

完整客户端示例

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1"
    "github.com/spiffe/spire-api-sdk/proto/spire/api/types"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // 连接 Server
    conn, err := grpc.Dial(
        "/tmp/spire-server/private/api.sock",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
            return net.Dial("unix", addr)
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 创建客户端
    client := entryv1.NewEntryClient(conn)

    // 列出条目
    resp, err := client.ListEntries(context.Background(), &entryv1.ListEntriesRequest{})
    if err != nil {
        log.Fatal(err)
    }

    for _, entry := range resp.Entries {
        fmt.Printf("Entry: %s -> %s\n", entry.ParentId, entry.SpiffeId)
    }
}

错误处理

API 使用 gRPC 状态码:

状态码

描述

OK

成功

INVALID_ARGUMENT

无效参数

NOT_FOUND

资源不存在

ALREADY_EXISTS

资源已存在

PERMISSION_DENIED

权限不足

INTERNAL

内部错误

import "google.golang.org/grpc/status"

resp, err := client.GetEntry(ctx, req)
if err != nil {
    st, ok := status.FromError(err)
    if ok {
        switch st.Code() {
        case codes.NotFound:
            log.Println("条目不存在")
        case codes.PermissionDenied:
            log.Println("权限不足")
        default:
            log.Printf("错误: %v", st.Message())
        }
    }
}

参考资源