Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions common/auth/kerberos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package auth

type (
// Kerberos describes GSSAPI/Kerberos authentication configuration.
//
// At present only the PostgreSQL persistence plugin consumes this
// struct (and only when using the pgx driver, postgres12_pgx),
// since lib/pq does not implement GSSAPI.
Kerberos struct {
Enabled bool `yaml:"enabled"`

// Username is the Kerberos principal name (e.g. "temporal").
// Combined with Realm it forms the full principal used to
// obtain a service ticket. Required when KeytabFile is set.
Username string `yaml:"username"`

// Realm is the Kerberos realm (e.g. "CORP.EXAMPLE.COM").
// Required when KeytabFile is set.
Realm string `yaml:"realm"`

// KeytabFile is a path to a keytab containing credentials for
// Username@Realm. When set, credentials are loaded from the
// keytab; otherwise a credential cache is used.
KeytabFile string `yaml:"keytabFile"`

// CredentialCacheFile is a path to a Kerberos credential
// cache (ccache). When KeytabFile is empty, this is used to
// locate existing credentials (e.g. populated by kinit).
// If empty, the value of $KRB5CCNAME is honored and falls
// back to the OS default (/tmp/krb5cc_<uid>).
CredentialCacheFile string `yaml:"credentialCacheFile"`

// ConfigFile is a path to krb5.conf. Defaults to
// /etc/krb5.conf when empty.
ConfigFile string `yaml:"configFile"`

// ServiceName is the PostgreSQL server's Kerberos service
// name (the "krbsrvname" DSN parameter). Defaults to
// "postgres" when empty, matching libpq.
ServiceName string `yaml:"serviceName"`

// SPN is an explicit service principal name to request
// (the "krbspn" DSN parameter). When set it takes precedence
// over ServiceName + host-derived SPN. Use for cases such
// as SQL Server-style SPNs or cross-realm trusts.
//
// Format: "service/host". The realm is taken from
// ConfigFile's default_realm; an optional "@REALM" suffix
// is accepted and stripped for compatibility with libpq
// convention.
SPN string `yaml:"spn"`

// DisableFAST turns off FAST (Flexible Authentication Secure
// Tunneling) pre-authentication. Required against some
// Active Directory KDCs that reject FAST-armored AS-REQs.
DisableFAST bool `yaml:"disableFAST"`
}
)
4 changes: 4 additions & 0 deletions common/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ type (
TaskScanPartitions int `yaml:"taskScanPartitions"`
// TLS is the configuration for TLS connections
TLS *auth.TLS `yaml:"tls"`
// Kerberos is the configuration for GSSAPI/Kerberos
// authentication. Currently only honored by the PostgreSQL
// plugin when using the pgx driver (postgres12_pgx).
Kerberos *auth.Kerberos `yaml:"kerberos"`
}

// CustomDatastoreConfig is the configuration for connecting to a custom datastore that is not supported by temporal core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type Driver interface {
IsDupEntryError(error) bool
IsDupDatabaseError(error) bool
IsConnNeedsRefreshError(error) bool
// SupportsGSSAPI reports whether the underlying driver can perform
// GSSAPI/Kerberos authentication. Only the pgx driver does today;
// lib/pq does not implement the protocol exchange.
SupportsGSSAPI() bool
}

func isConnNeedsRefreshError(code, message string) bool {
Expand Down
4 changes: 4 additions & 0 deletions common/persistence/sql/sqlplugin/postgresql/driver/pgx.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (

type PGXDriver struct{}

var _ Driver = (*PGXDriver)(nil)

const pgxDriverName = "pgx"

func (p *PGXDriver) CreateConnection(dsn string) (*sqlx.DB, error) {
Expand Down Expand Up @@ -49,6 +51,8 @@ func (p *PGXDriver) IsDupDatabaseError(err error) bool {
return ok && pqErr.Code == dupDatabaseCode
}

func (p *PGXDriver) SupportsGSSAPI() bool { return true }

func (p *PGXDriver) IsConnNeedsRefreshError(err error) bool {
pqErr, ok := err.(*pgconn.PgError)
if !ok {
Expand Down
4 changes: 4 additions & 0 deletions common/persistence/sql/sqlplugin/postgresql/driver/pq.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

type PQDriver struct{}

var _ Driver = (*PQDriver)(nil)

const pqDriverName = "postgres"

func (p *PQDriver) CreateConnection(dsn string) (*sqlx.DB, error) {
Expand Down Expand Up @@ -43,6 +45,8 @@ func (p *PQDriver) IsDupDatabaseError(err error) bool {
return ok && pqErr.Code == dupDatabaseCode
}

func (p *PQDriver) SupportsGSSAPI() bool { return false }

func (p *PQDriver) IsConnNeedsRefreshError(err error) bool {
pqErr, ok := err.(*pq.Error)
if !ok {
Expand Down
7 changes: 7 additions & 0 deletions common/persistence/sql/sqlplugin/postgresql/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ func (p *plugin) CreateDB(
logger log.Logger,
metricsHandler metrics.Handler,
) (sqlplugin.GenericDB, error) {
if cfg.Kerberos != nil && cfg.Kerberos.Enabled && !p.driver.SupportsGSSAPI() {
return nil, fmt.Errorf(
"postgres plugin %q does not support kerberos authentication; "+
"use plugin %q (pgx driver) instead",
PluginName, PluginNamePGX,
)
}
connect := func() (*sqlx.DB, error) {
if cfg.Connect != nil {
return cfg.Connect(cfg)
Expand Down
161 changes: 161 additions & 0 deletions common/persistence/sql/sqlplugin/postgresql/session/kerberos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package session

import (
"errors"
"fmt"
"os"
"os/user"
"strings"

"github.com/jackc/pgx/v5/pgconn"
krb5client "github.com/jcmturner/gokrb5/v8/client"
krb5config "github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/spnego"

"go.temporal.io/server/common/auth"
)

const defaultKrb5ConfigPath = "/etc/krb5.conf"

// kerberosGSS is a per-connection GSSAPI provider that satisfies the
// pgconn.GSS interface. A fresh instance is returned by
// kerberosGSSFactory on every connection attempt so that SPNEGO
// state is not shared across connections.
type kerberosGSS struct {
cfg *auth.Kerberos
spnegoCli *spnego.SPNEGO
}

var _ pgconn.GSS = (*kerberosGSS)(nil)

// kerberosGSSFactory returns a closure that constructs a new
// kerberosGSS instance. It is registered with pgx via
// pgconn.RegisterGSSProvider. The config pointer is captured so
// changes to it will not affect already-running connections, but
// subsequent connections will see updated values.
func kerberosGSSFactory(cfg *auth.Kerberos) func() (*kerberosGSS, error) {
return func() (*kerberosGSS, error) {
if cfg == nil || !cfg.Enabled {
return nil, errors.New("kerberos provider invoked with disabled config")
}
return &kerberosGSS{cfg: cfg}, nil
}
}

// GetInitToken builds the initial SPNEGO token for service/host.
// pgx calls this when no explicit SPN is configured.
func (g *kerberosGSS) GetInitToken(host, service string) ([]byte, error) {
if service == "" {
service = "postgres"
}
return g.GetInitTokenFromSPN(service + "/" + host)
}

// GetInitTokenFromSPN builds the initial SPNEGO token for the given
// service principal name. The SPN must be in "service/host" form;
// any "@REALM" suffix is stripped because gokrb5's SPNEGO client
// derives the realm from krb5.conf rather than from the SPN string.
func (g *kerberosGSS) GetInitTokenFromSPN(spn string) ([]byte, error) {
if i := strings.LastIndex(spn, "@"); i >= 0 {
spn = spn[:i]
}
cl, err := newKrb5Client(g.cfg)
if err != nil {
return nil, err
}
g.spnegoCli = spnego.SPNEGOClient(cl, spn)
if err := g.spnegoCli.AcquireCred(); err != nil {
return nil, fmt.Errorf("kerberos: acquire credential for %q: %w", spn, err)
}
token, err := g.spnegoCli.InitSecContext()
if err != nil {
return nil, fmt.Errorf("kerberos: init security context for %q: %w", spn, err)
}
buf, err := token.Marshal()
if err != nil {
return nil, fmt.Errorf("kerberos: marshal SPNEGO token: %w", err)
}
return buf, nil
}

// Continue is called by pgx after the server's reply to the initial
// token. PostgreSQL does not require an additional client-side
// round-trip: the server's GSSContinue response carries the
// mutual-auth token (possibly empty) and is immediately followed by
// AuthenticationOk. We therefore mark the exchange complete after
// the first server reply.
func (g *kerberosGSS) Continue(_ []byte) (done bool, outToken []byte, err error) {
if g.spnegoCli == nil {
return false, nil, errors.New("kerberos: Continue called before init token")
}
return true, nil, nil
}

func newKrb5Client(cfg *auth.Kerberos) (*krb5client.Client, error) {
krbCfg, err := loadKrb5Config(cfg.ConfigFile)
if err != nil {
return nil, err
}
settings := []func(*krb5client.Settings){}
if cfg.DisableFAST {
settings = append(settings, krb5client.DisablePAFXFAST(true))
}

if cfg.KeytabFile != "" {
if cfg.Username == "" || cfg.Realm == "" {
return nil, errors.New("kerberos: username and realm are required when keytabFile is set")
}
kt, err := keytab.Load(cfg.KeytabFile)
if err != nil {
return nil, fmt.Errorf("kerberos: load keytab %q: %w", cfg.KeytabFile, err)
}
cl := krb5client.NewWithKeytab(cfg.Username, cfg.Realm, kt, krbCfg, settings...)
return cl, nil
}

ccPath, err := resolveCCachePath(cfg.CredentialCacheFile)
if err != nil {
return nil, err
}
ccache, err := credentials.LoadCCache(ccPath)
if err != nil {
return nil, fmt.Errorf("kerberos: load credential cache %q: %w", ccPath, err)
}
cl, err := krb5client.NewFromCCache(ccache, krbCfg, settings...)
if err != nil {
return nil, fmt.Errorf("kerberos: create client from ccache: %w", err)
}
return cl, nil
}

func loadKrb5Config(path string) (*krb5config.Config, error) {
if path == "" {
path = defaultKrb5ConfigPath
}
cfg, err := krb5config.Load(path)
if err != nil {
return nil, fmt.Errorf("kerberos: load krb5 config %q: %w", path, err)
}
return cfg, nil
}

// resolveCCachePath picks a credential cache path using the same
// precedence as libkrb5: explicit config > $KRB5CCNAME > default
// /tmp/krb5cc_<uid>. The "FILE:" prefix permitted by MIT Kerberos is
// accepted and stripped.
func resolveCCachePath(explicit string) (string, error) {
candidate := explicit
if candidate == "" {
candidate = os.Getenv("KRB5CCNAME")
}
if candidate == "" {
u, err := user.Current()
if err != nil {
return "", fmt.Errorf("kerberos: determine current user for default ccache: %w", err)
}
return "/tmp/krb5cc_" + u.Uid, nil
}
return strings.TrimPrefix(candidate, "FILE:"), nil
}
Loading