Skip to content

Commit dd29280

Browse files
authored
GSS-API frontend auth - Kerberos (#657)
1 parent 3416139 commit dd29280

16 files changed

+489
-6
lines changed

docker/router/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM spqr-base-image
22

3-
RUN apt-get update && apt-get install -y postgresql-client
3+
RUN apt-get update && apt-get install -y --no-install-recommends krb5-user postgresql-client
44
COPY ./docker/router/ssl/localhost.crt /etc/spqr/ssl/server.crt
55
COPY ./docker/router/ssl/localhost.key /etc/spqr/ssl/server.key
66
ENTRYPOINT CONFIG_PATH=${ROUTER_CONFIG=/spqr/docker/router/cfg.yaml} COORD_CONFIG_PATH=${COORDINATOR_CONFIG=/spqr/docker/coordinator/cfg.yaml} && CUR_HOST=$(cat ${CONFIG_PATH} | grep "host:") && sed -i "s/${CUR_HOST}/${ROUTER_HOST=${CUR_HOST}}/g" ${CONFIG_PATH} && /spqr/spqr-router run --config ${CONFIG_PATH} --coordinator-config ${COORD_CONFIG_PATH} >> ${ROUTER_LOG}

docs/Authentication.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ Methods supported for frontend auth, the way they`re specified in config:
1111
- `md5`
1212
- `scram`, same as `scram-sha-256`
1313
- `ldap`
14+
- `gss`, using Kerberos
1415

1516
For more information about authentication config, see [pkg/config/auth.go](../pkg/config/auth.go)

go.mod

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/golang/mock v1.6.0
1414
github.com/google/uuid v1.6.0
1515
github.com/jackc/pgx/v5 v5.5.5
16+
github.com/jcmturner/gokrb5/v8 v8.4.4
1617
github.com/jmoiron/sqlx v1.4.0
1718
github.com/juju/errors v1.0.0
1819
github.com/lib/pq v1.10.9
@@ -60,11 +61,17 @@ require (
6061
github.com/golang/protobuf v1.5.4 // indirect
6162
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
6263
github.com/hashicorp/go-memdb v1.3.4 // indirect
64+
github.com/hashicorp/go-uuid v1.0.3 // indirect
6365
github.com/hashicorp/golang-lru v1.0.2 // indirect
6466
github.com/inconshreveable/mousetrap v1.1.0 // indirect
6567
github.com/jackc/pgpassfile v1.0.0 // indirect
6668
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
6769
github.com/jackc/puddle/v2 v2.2.1 // indirect
70+
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
71+
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
72+
github.com/jcmturner/gofork v1.7.6 // indirect
73+
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
74+
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
6875
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
6976
github.com/kr/pretty v0.3.1 // indirect
7077
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
8080
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
8181
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8282
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
83+
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
8384
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
85+
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
8486
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
8587
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
8688
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=

pkg/auth/auth.go

+28
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,34 @@ func AuthFrontend(cl client.Client, rule *config.FrontendRule) error {
394394
return err
395395
}
396396
return nil
397+
case config.AuthGSS:
398+
if rule.AuthRule.GssConfig == nil {
399+
return fmt.Errorf("GSS configuration are not set for GSS auth method")
400+
}
401+
if cl.Usr() != rule.Usr {
402+
return fmt.Errorf("user from client %v != %v missmatch user in config", cl.Usr(), rule.Usr)
403+
}
404+
b := BaseAuthModule{
405+
properties: map[string]interface{}{
406+
keyTabFileProperty: rule.AuthRule.GssConfig.KrbKeyTabFile,
407+
},
408+
}
409+
kerb := NewKerberosModule(b)
410+
cred, err := kerb.Process(cl)
411+
if err != nil {
412+
return err
413+
}
414+
username := cred.UserName()
415+
if rule.AuthRule.GssConfig.IncludeRealm {
416+
username = fmt.Sprintf("%s@%s", cred.UserName(), cred.Realm())
417+
}
418+
if username != cl.Usr() {
419+
return fmt.Errorf("GSS username missmatch with pg user: '%v' != '%v'", username, cl.Usr())
420+
}
421+
if rule.AuthRule.GssConfig.KrbRealm != "" && rule.AuthRule.GssConfig.KrbRealm != cred.Realm() {
422+
return fmt.Errorf("GSS realm in token missmatch with realm in confing: '%v' != '%v'", rule.AuthRule.GssConfig.KrbRealm, cred.Realm())
423+
}
424+
return nil
397425
default:
398426
return fmt.Errorf("invalid auth method '%v'", rule.AuthRule.Method)
399427
}

pkg/auth/krb5.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package auth
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"github.com/jackc/pgx/v5/pgproto3"
7+
"github.com/jcmturner/gokrb5/v8/credentials"
8+
"github.com/jcmturner/gokrb5/v8/gssapi"
9+
"github.com/jcmturner/gokrb5/v8/keytab"
10+
"github.com/jcmturner/gokrb5/v8/service"
11+
"github.com/pg-sharding/spqr/pkg/client"
12+
"log"
13+
)
14+
15+
type BaseAuthModule struct {
16+
properties map[string]interface{}
17+
}
18+
type Kerberos struct {
19+
BaseAuthModule
20+
servicePrincipal string
21+
kt *keytab.Keytab
22+
}
23+
24+
const (
25+
keyTabFileProperty = "keytabfile"
26+
keyTabDataProperty = "keytabdata"
27+
servicePrincipalProperty = "serviceprincipal"
28+
)
29+
30+
func NewKerberosModule(base BaseAuthModule) *Kerberos {
31+
k := &Kerberos{
32+
BaseAuthModule: base,
33+
}
34+
var kt *keytab.Keytab
35+
var err error
36+
if ktFileProp, ok := k.BaseAuthModule.properties[keyTabFileProperty]; ok {
37+
ktFile, _ := ktFileProp.(string)
38+
kt, err = keytab.Load(ktFile)
39+
if err != nil {
40+
panic(err) // If the "krb5.keytab" file is not available the application will show an error message.
41+
}
42+
} else if ktDataProp, ok := k.BaseAuthModule.properties[keyTabDataProperty]; ok {
43+
ktData := ktDataProp.(string)
44+
b, _ := hex.DecodeString(ktData)
45+
kt = keytab.New()
46+
err = kt.Unmarshal(b)
47+
if err != nil {
48+
panic(err)
49+
}
50+
}
51+
k.kt = kt
52+
if spProp, ok := k.BaseAuthModule.properties[servicePrincipalProperty]; ok {
53+
k.servicePrincipal = spProp.(string)
54+
}
55+
56+
return k
57+
}
58+
59+
func (k *Kerberos) Process(cl client.Client) (cred *credentials.Credentials, err error) {
60+
kt := k.kt
61+
if err != nil {
62+
panic(err) // If the "krb5.keytab" file is not available the application will show an error message.
63+
}
64+
settings := service.NewSettings(kt)
65+
msg := &pgproto3.AuthenticationGSS{}
66+
if err := cl.Send(msg); err != nil {
67+
return nil, err
68+
}
69+
if err := cl.SetAuthType(pgproto3.AuthTypeGSS); err != nil {
70+
return nil, err
71+
}
72+
73+
st := KRB5Token{
74+
settings: settings,
75+
}
76+
clientMsgRaw, err := cl.Receive()
77+
if err != nil {
78+
return nil, err
79+
}
80+
switch clientMsgRaw := clientMsgRaw.(type) {
81+
case *pgproto3.GSSResponse:
82+
err := st.Unmarshal(clientMsgRaw.Data)
83+
if err != nil {
84+
return nil, err
85+
}
86+
default:
87+
return nil, fmt.Errorf("unexpected message type %T", clientMsgRaw)
88+
}
89+
// Validate the context token
90+
authed, status := st.Verify()
91+
if status.Code != gssapi.StatusComplete && status.Code != gssapi.StatusContinueNeeded {
92+
errText := fmt.Sprintf("Kerberos validation error: %v", status)
93+
log.Print(errText)
94+
return nil, fmt.Errorf(errText)
95+
}
96+
if status.Code == gssapi.StatusContinueNeeded {
97+
errText := "Kerberos GSS-API continue needed"
98+
log.Print(errText)
99+
return nil, fmt.Errorf(errText)
100+
}
101+
if authed {
102+
ctx := st.Context()
103+
id := ctx.Value(CtxCredential).(*credentials.Credentials)
104+
return id, nil
105+
} else {
106+
errText := "Kerberos authentication failed"
107+
log.Print(errText)
108+
return nil, fmt.Errorf(errText)
109+
}
110+
}

pkg/auth/krb5Token.go

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/jcmturner/gofork/encoding/asn1"
10+
"github.com/jcmturner/gokrb5/v8/asn1tools"
11+
"github.com/jcmturner/gokrb5/v8/gssapi"
12+
"github.com/jcmturner/gokrb5/v8/iana/msgtype"
13+
"github.com/jcmturner/gokrb5/v8/messages"
14+
"github.com/jcmturner/gokrb5/v8/service"
15+
)
16+
17+
type ContextKey string
18+
19+
const CtxCredential ContextKey = "spqr/gokrb5/CtxCredential"
20+
21+
// GSSAPI KRB5 MechToken IDs.
22+
const (
23+
TOK_ID_KRB_AP_REQ = "0100"
24+
TOK_ID_KRB_AP_REP = "0200"
25+
TOK_ID_KRB_ERROR = "0300"
26+
)
27+
28+
// KRB5Token context token implementation for GSSAPI.
29+
type KRB5Token struct {
30+
OID asn1.ObjectIdentifier
31+
tokID []byte
32+
APReq messages.APReq
33+
APRep messages.APRep
34+
KRBError messages.KRBError
35+
settings *service.Settings
36+
context context.Context
37+
}
38+
39+
// Marshal a KRB5Token into a slice of bytes.
40+
func (m *KRB5Token) Marshal() ([]byte, error) {
41+
// Create the header
42+
b, _ := asn1.Marshal(m.OID)
43+
b = append(b, m.tokID...)
44+
var tb []byte
45+
var err error
46+
switch hex.EncodeToString(m.tokID) {
47+
case TOK_ID_KRB_AP_REQ:
48+
tb, err = m.APReq.Marshal()
49+
if err != nil {
50+
return []byte{}, fmt.Errorf("error marshalling AP_REQ for MechToken: %v", err)
51+
}
52+
case TOK_ID_KRB_AP_REP:
53+
return []byte{}, errors.New("marshal of AP_REP GSSAPI MechToken not supported by gokrb5")
54+
case TOK_ID_KRB_ERROR:
55+
return []byte{}, errors.New("marshal of KRB_ERROR GSSAPI MechToken not supported by gokrb5")
56+
}
57+
if err != nil {
58+
return []byte{}, fmt.Errorf("error mashalling kerberos message within mech token: %v", err)
59+
}
60+
b = append(b, tb...)
61+
return asn1tools.AddASNAppTag(b, 0), nil
62+
}
63+
64+
// Unmarshal a KRB5Token.
65+
func (m *KRB5Token) Unmarshal(b []byte) error {
66+
var oid asn1.ObjectIdentifier
67+
r, err := asn1.UnmarshalWithParams(b, &oid, fmt.Sprintf("application,explicit,tag:%v", 0))
68+
if err != nil {
69+
return fmt.Errorf("error unmarshalling KRB5Token OID: %v", err)
70+
}
71+
if !oid.Equal(gssapi.OIDKRB5.OID()) {
72+
return fmt.Errorf("error unmarshalling KRB5Token, OID is %s not %s", oid.String(), gssapi.OIDKRB5.OID().String())
73+
}
74+
m.OID = oid
75+
if len(r) < 2 {
76+
return fmt.Errorf("krb5token too short")
77+
}
78+
m.tokID = r[0:2]
79+
switch hex.EncodeToString(m.tokID) {
80+
case TOK_ID_KRB_AP_REQ:
81+
var a messages.APReq
82+
err = a.Unmarshal(r[2:])
83+
if err != nil {
84+
return fmt.Errorf("error unmarshalling KRB5Token AP_REQ: %v", err)
85+
}
86+
m.APReq = a
87+
case TOK_ID_KRB_AP_REP:
88+
var a messages.APRep
89+
err = a.Unmarshal(r[2:])
90+
if err != nil {
91+
return fmt.Errorf("error unmarshalling KRB5Token AP_REP: %v", err)
92+
}
93+
m.APRep = a
94+
case TOK_ID_KRB_ERROR:
95+
var a messages.KRBError
96+
err = a.Unmarshal(r[2:])
97+
if err != nil {
98+
return fmt.Errorf("error unmarshalling KRB5Token KRBError: %v", err)
99+
}
100+
m.KRBError = a
101+
}
102+
return nil
103+
}
104+
105+
// Verify a KRB5Token.
106+
func (m *KRB5Token) Verify() (bool, gssapi.Status) {
107+
switch hex.EncodeToString(m.tokID) {
108+
case TOK_ID_KRB_AP_REQ:
109+
ok, creds, err := service.VerifyAPREQ(&m.APReq, m.settings)
110+
if err != nil {
111+
return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: err.Error()}
112+
}
113+
if !ok {
114+
return false, gssapi.Status{Code: gssapi.StatusDefectiveCredential, Message: "KRB5_AP_REQ token not valid"}
115+
}
116+
m.context = context.Background()
117+
m.context = context.WithValue(m.context, CtxCredential, creds)
118+
return true, gssapi.Status{Code: gssapi.StatusComplete}
119+
case TOK_ID_KRB_AP_REP:
120+
// Client side
121+
// TODO how to verify the AP_REP - not yet implemented
122+
return false, gssapi.Status{Code: gssapi.StatusFailure, Message: "verifying an AP_REP is not currently supported by gokrb5"}
123+
case TOK_ID_KRB_ERROR:
124+
if m.KRBError.MsgType != msgtype.KRB_ERROR {
125+
return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: "KRB5_Error token not valid"}
126+
}
127+
return true, gssapi.Status{Code: gssapi.StatusUnavailable}
128+
}
129+
return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: "unknown TOK_ID in KRB5 token"}
130+
}
131+
132+
// IsAPReq tests if the MechToken contains an AP_REQ.
133+
func (m *KRB5Token) IsAPReq() bool {
134+
return hex.EncodeToString(m.tokID) == TOK_ID_KRB_AP_REQ
135+
}
136+
137+
// IsAPRep tests if the MechToken contains an AP_REP.
138+
func (m *KRB5Token) IsAPRep() bool {
139+
return hex.EncodeToString(m.tokID) == TOK_ID_KRB_AP_REP
140+
}
141+
142+
// IsKRBError tests if the MechToken contains an KRB_ERROR.
143+
func (m *KRB5Token) IsKRBError() bool {
144+
return hex.EncodeToString(m.tokID) == TOK_ID_KRB_ERROR
145+
}
146+
147+
// Context returns the KRB5 token's context which will contain any verify user identity information.
148+
func (m *KRB5Token) Context() context.Context {
149+
return m.context
150+
}

pkg/config/auth.go

+2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ const (
1313
AuthMD5 = AuthMethod("md5")
1414
AuthSCRAM = AuthMethod("scram")
1515
AuthLDAP = AuthMethod("ldap")
16+
AuthGSS = AuthMethod("gss")
1617
)
1718

1819
type AuthCfg struct {
1920
Method AuthMethod `json:"auth_method" yaml:"auth_method" toml:"auth_method"`
2021
Password string `json:"password" yaml:"password" toml:"password"`
2122
LDAPConfig *LDAPCfg `json:"ldap_config" yaml:"ldap_config" toml:"ldap_config"`
23+
GssConfig *GssCfg `json:"gss_config" yaml:"gss_config" toml:"gss_config"`
2224
}

pkg/config/gss.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package config
2+
3+
type GssCfg struct {
4+
KrbKeyTabFile string `json:"krb_keytab_file" yaml:"krb_keytab_file" toml:"krb_keytab_file"`
5+
KrbRealm string `json:"krb_realm" yaml:"krb_realm" toml:"krb_realm"`
6+
IncludeRealm bool `json:"include_realm" yaml:"include_realm" toml:"include_realm"`
7+
}

test/feature/conf/kdc/Dockerfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM alpine:3.14
2+
3+
RUN apk add --no-cache \
4+
krb5-server \
5+
&& rm -rf /var/cache/apk/*
6+
7+
COPY krb5.conf /etc/krb5.conf
8+
9+
RUN kdb5_util create -s -P kpass \
10+
&& kadmin.local -q "addprinc -pw psql tester@MY.EX" \
11+
&& kadmin.local -q "addprinc -randkey postgres/localhost@MY.EX"
12+
13+
CMD ["sh", "/start.sh"]

0 commit comments

Comments
 (0)