Skip to content

Go module

The module github.com/truvity/sluis verifies a caller, exchanges tokens and evaluates policy. Packages: identity, identity/resource, tokens, policy, policy/compile, config, backend (extending). TypeScript: @truvity/sluis.

Both verifiers of trust return one Verified: Subject, Email, Name, GivenName, FamilyName, ClientID, Groups, ServiceAccount, Recovery. See service to service.

import (
    "github.com/truvity/sluis/identity"
    "github.com/truvity/sluis/policy"
    "github.com/truvity/sluis/tokens"
)

Issuer anchor

Use this anchor for a console behind a proxy that forwards a bearer, such as an oauth2-proxy.

issuer := &identity.Issuer{
    URL:      "https://access.example",
    Audience: "console.example",   // this console's client id at the issuer
}

mux := http.NewServeMux()
mux.Handle(identity.WhoAmIPath, identity.WhoAmI(version))   // GET /.access/whoami
mux.Handle("/admin/", identity.Require("all:console:operator")(admin))

http.ListenAndServe(":8080", identity.Middleware(issuer)(mux))

Middleware never refuses. Require refuses without naming a group that would work; with no group it admits any vouched caller. The WhoAmI body is in contracts.

Cluster anchor

Use this anchor for a workload calling a service in the same cluster.

cluster := &identity.Cluster{
    Review:   kube.ReviewToken,          // yours, or client-go's
    Audience: "the-service",
    Name:     "prod",
    Groups:   []string{"prod:k8s:admin"},
}

http.ListenAndServe(":8080", identity.Middleware(cluster, issuer)(mux))

You supply Review and Groups; a TokenReview carries no policy. Verifiers run in order and the first answer wins. A verifier that cannot reach the issuer stops the chain.

MCP server

Protect answers the RFC 9728 challenge. See MCP.

res, _ := resource.New(resource.Config{IssuerURL: "https://access.example", ResourceURL: "https://mcp.example.com/metrics", Scope: "openid"})
mux.Handle(res.Path(), res.Metadata())
mux.Handle("/", res.Protect(mcp))

Token exchange

exchanger := &tokens.Exchanger{Issuer: "https://access.example", ClientID: "local-dev"}
token, err := exchanger.Exchange(ctx, subject, tokens.TypeJWT, "aws:111122223333:power")

Use tokens.TypeJWT for an outside proof, such as a GitHub job token. Use tokens.TypeAccessToken for a sign-in of this issuer: only the access token of a live session at a public client with sign_in_exchange: true. Send the client in HTTP Basic. A refusal is tokens.ErrRefused.

For a GitHub App installation token, name the App (contract):

exchanger := &tokens.Exchanger{Issuer: "https://access.example", ClientID: "github-app:publisher"}
minted, err := exchanger.GitHubInstallationToken(ctx, subject, tokens.TypeJWT, "publisher",
    []string{"app"}, map[string]string{"contents": "read"})
// minted.AccessToken, minted.Expires, minted.Repositories, minted.Permissions
tokens.WriteExecCredential(os.Stdout, apiVersion, token)   // kubectl reads this
creds, _ := tokens.AssumeRoleWithWebIdentity(ctx, nil, roleARN, who, token.AccessToken)
tokens.WriteCredentialProcess(os.Stdout, creds)            // the AWS SDKs read this

Policy

declared, _ := policy.LoadDeclared("/etc/sluis/policy")  // a file or a directory
set, _ := policy.NewSet(declared)                         // validated once, at load

result := set.Evaluate(policy.Input{
    Email:           "[email protected]",
    DirectoryGroups: groups,      // what the directory confirmed
    Authoritative:   true,        // whether that answer may be acted on
})

Input also takes GitHub, ServiceAccount and AWS. LoadDeclared refuses team_id, domains and owner under a Slack workspace, and owner under a GitHub organisation.

accessor returns
Set.SlackWorkspaceDeclared(key) whether the policy declares that Slack workspace key
Set.SlackWorkspaceKeys() every declared Slack workspace key, sorted
Set.Declared() the declared Policy in force, read-only, to validate a definition of your own against
Policy.PeopleByAddress() every address in the people table, normalised, mapped to its person's key

Configuration

import "github.com/truvity/sluis/config"

in, err := config.LoadInstallation("installation.yaml")
service, policy, err := config.Render(in)   // sluis.yaml and policy.yaml, as bytes

config holds the service (v3) and policy (v2) document types and the installation. Render backs sluisctl render.

Decided in: 0038.