Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support auth via certificates and headers #343

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
19 changes: 19 additions & 0 deletions auth_server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"io/ioutil"
"math/rand"
"net"
"net/http"
Expand Down Expand Up @@ -104,6 +106,23 @@ func ServeOnce(c *server.Config, cf string) (*server.AuthServer, *http.Server) {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, s.ID)
}
}
if c.Server.ClientAuth != "" {
value, found := server.ClientAuthValues[c.Server.ClientAuth]
if !found {
value = tls.NoClientCert
}
tlsConfig.ClientAuth = value
glog.Infof("TLS ClientAuth: %s", tlsConfig.ClientAuth)
}
if c.Server.ClientCA != "" {
pool := x509.NewCertPool()
caFile, err := ioutil.ReadFile(c.Server.ClientCA)
if err != nil {
glog.Exitf("Failed to load client CA file: %v", err)
}
pool.AppendCertsFromPEM(caFile)
tlsConfig.ClientCAs = pool
}
if c.Server.CertFile != "" || c.Server.KeyFile != "" {
// Check for partial configuration.
if c.Server.CertFile == "" || c.Server.KeyFile == "" {
Expand Down
87 changes: 69 additions & 18 deletions auth_server/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,33 @@ import (
"github.com/cesanta/docker_auth/auth_server/authz"
)

const (
sourceHeader = "header"
sourceCN = "cn"
sourceStatic = "static"
)

type Config struct {
Server ServerConfig `yaml:"server"`
Token TokenConfig `yaml:"token"`
Users map[string]*authn.Requirements `yaml:"users,omitempty"`
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
GitHubAuth *authn.GitHubAuthConfig `yaml:"github_auth,omitempty"`
OIDCAuth *authn.OIDCAuthConfig `yaml:"oidc_auth,omitempty"`
GitlabAuth *authn.GitlabAuthConfig `yaml:"gitlab_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
XormAuthn *authn.XormAuthnConfig `yaml:"xorm_auth,omitempty"`
ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
PluginAuthn *authn.PluginAuthnConfig `yaml:"plugin_authn,omitempty"`
ACL authz.ACL `yaml:"acl,omitempty"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
ACLXorm *authz.XormAuthzConfig `yaml:"acl_xorm,omitempty"`
ExtAuthz *authz.ExtAuthzConfig `yaml:"ext_authz,omitempty"`
PluginAuthz *authz.PluginAuthzConfig `yaml:"plugin_authz,omitempty"`
CasbinAuthz *authz.CasbinAuthzConfig `yaml:"casbin_authz,omitempty"`
Server ServerConfig `yaml:"server"`
Token TokenConfig `yaml:"token"`
Users map[string]*authn.Requirements `yaml:"users,omitempty"`
GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
GitHubAuth *authn.GitHubAuthConfig `yaml:"github_auth,omitempty"`
OIDCAuth *authn.OIDCAuthConfig `yaml:"oidc_auth,omitempty"`
GitlabAuth *authn.GitlabAuthConfig `yaml:"gitlab_auth,omitempty"`
LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
XormAuthn *authn.XormAuthnConfig `yaml:"xorm_auth,omitempty"`
ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
PluginAuthn *authn.PluginAuthnConfig `yaml:"plugin_authn,omitempty"`
ACL authz.ACL `yaml:"acl,omitempty"`
ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
ACLXorm *authz.XormAuthzConfig `yaml:"acl_xorm,omitempty"`
ExtAuthz *authz.ExtAuthzConfig `yaml:"ext_authz,omitempty"`
PluginAuthz *authz.PluginAuthzConfig `yaml:"plugin_authz,omitempty"`
CasbinAuthz *authz.CasbinAuthzConfig `yaml:"casbin_authz,omitempty"`
ClientCertLabels string `yaml:"client_cert_labels,omitempty"`
AlternateCredentials *AlternateCredentialsConfig `yaml:"alternate_credentials"`
}

type ServerConfig struct {
Expand All @@ -62,6 +70,8 @@ type ServerConfig struct {
RealIPPos int `yaml:"real_ip_pos,omitempty"`
CertFile string `yaml:"certificate,omitempty"`
KeyFile string `yaml:"key,omitempty"`
ClientAuth string `yaml:"client_auth_type,omitempty"`
ClientCA string `yaml:"client_ca,omitempty"`
HSTS bool `yaml:"hsts,omitempty"`
TLSMinVersion string `yaml:"tls_min_version,omitempty"`
TLSCurvePreferences []string `yaml:"tls_curve_preferences,omitempty"`
Expand Down Expand Up @@ -90,6 +100,17 @@ type TokenConfig struct {
sigAlg string
}

type AlternateCredentialsConfig struct {
Label string `yaml:"label,omitempty"`
Username *CredentialSourceConfig `yaml:"username,omitempty"`
Password *CredentialSourceConfig `yaml:"password,omitempty"`
}

type CredentialSourceConfig struct {
Source string `yaml:"source,omitempty"`
Value string `yaml:"value,omitempty"`
}

// TLSCipherSuitesValues maps CipherSuite names as strings to the actual values
// in the crypto/tls package
// Taken from https://golang.org/pkg/crypto/tls/#pkg-constants
Expand Down Expand Up @@ -149,6 +170,24 @@ var TLSCurveIDValues = map[string]tls.CurveID{
"X25519": tls.X25519,
}

var ClientAuthValues = map[string]tls.ClientAuthType{
"NoClientCert": tls.NoClientCert,
"RequestClientCert": tls.RequestClientCert,
"RequireAndVerifyClientCert": tls.RequireAndVerifyClientCert,
"RequireAnyClientCert": tls.RequireAnyClientCert,
"VerifyClientCertIfGiven": tls.VerifyClientCertIfGiven,
}

func (c *CredentialSourceConfig) validate() error {
if c.Source != sourceHeader && c.Source != sourceCN && c.Source != sourceStatic {
return fmt.Errorf("invalid source: %s", c.Source)
}
if (c.Source == sourceHeader || c.Source == sourceStatic) && c.Value == "" {
return errors.New("value should not be empty")
}
return nil
}

func validate(c *Config) error {
if c.Server.ListenAddress == "" {
return errors.New("server.addr is required")
Expand Down Expand Up @@ -334,6 +373,18 @@ func validate(c *Config) error {
return fmt.Errorf("bad plugin_authz config: %s", err)
}
}

if c.AlternateCredentials != nil {
if c.AlternateCredentials.Username == nil || c.AlternateCredentials.Password == nil {
return errors.New("both username and password must be specified")
}
if err := c.AlternateCredentials.Username.validate(); err != nil {
return err
}
if err := c.AlternateCredentials.Password.validate(); err != nil {
return err
}
}
return nil
}

Expand Down
55 changes: 54 additions & 1 deletion auth_server/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,24 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
ar.Password = api.PasswordString(password)
}
}

if as.config.AlternateCredentials != nil {
var altCredsSuccess bool
username := alternateCredentials(req, as.config.AlternateCredentials.Username)
password := alternateCredentials(req, as.config.AlternateCredentials.Password)
if username != "" && password != "" && username == ar.User {
ar.User = username
ar.Password = api.PasswordString(password)
altCredsSuccess = true
}
if altCredsSuccess && as.config.AlternateCredentials.Label != "" {
if ar.Labels == nil {
ar.Labels = make(api.Labels, 1)
}
ar.Labels[as.config.AlternateCredentials.Label] = []string{fmt.Sprint(altCredsSuccess)}
}
}

ar.Account = req.FormValue("account")
if ar.Account == "" {
ar.Account = ar.User
Expand All @@ -273,6 +291,16 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
if err := req.ParseForm(); err != nil {
return nil, fmt.Errorf("invalid form value")
}

if as.config.ClientCertLabels != "" && req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
if ar.Labels == nil {
ar.Labels = make(api.Labels, 3)
}
clientCert := req.TLS.PeerCertificates[0]
ar.Labels[as.config.ClientCertLabels+"_O"] = clientCert.Subject.Organization
ar.Labels[as.config.ClientCertLabels+"_OU"] = clientCert.Subject.OrganizationalUnit
ar.Labels[as.config.ClientCertLabels+"_DNS_NAMES"] = clientCert.DNSNames
}
// https://github.com/docker/distribution/blob/1b9ab303a477ded9bdd3fc97e9119fa8f9e58fca/docs/spec/auth/scope.md#resource-scope-grammar
if req.FormValue("scope") != "" {
for _, scopeValue := range req.Form["scope"] {
Expand Down Expand Up @@ -311,6 +339,19 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
return ar, nil
}

func alternateCredentials(r *http.Request, config *CredentialSourceConfig) string {
switch config.Source {
case sourceHeader:
return r.Header.Get(config.Value)
case sourceStatic:
return config.Value
case sourceCN:
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
return r.TLS.PeerCertificates[0].Subject.CommonName
}
}
return ""
}
func (as *AuthServer) Authenticate(ar *authRequest) (bool, api.Labels, error) {
for i, a := range as.authenticators {
result, labels, err := a.Authenticate(ar.Account, ar.Password)
Expand Down Expand Up @@ -493,7 +534,7 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
http.Error(rw, "Auth failed.", http.StatusUnauthorized)
return
}
ar.Labels = labels
ar.Labels = mergeLabels(ar.Labels, labels)
}
if len(ar.Scopes) > 0 {
ares, err = as.Authorize(ar)
Expand Down Expand Up @@ -521,6 +562,18 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) {
rw.Write(result)
}

func mergeLabels(dest api.Labels, src api.Labels) api.Labels {
if dest == nil {
return src
} else if src == nil {
return dest
}
for k, v := range src {
dest[k] = v
}
return dest
}

func (as *AuthServer) Stop() {
for _, an := range as.authenticators {
an.Stop()
Expand Down
43 changes: 43 additions & 0 deletions examples/client_certificates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
server:
addr: :5001
certificate: /path/to/server.pem
key: /path/to/server.key
client_ca: /path/to/ca.crt
client_auth_type: RequireAndVerifyClientCert

alternate_credentials:
# Add a label indicating if alternate credentials were used.
label: ALTERNATE_CREDENTIALS
username:
source: cn
password:
source: static
# a "sentinel value for use with static auth
value: secret

users:
user1:
# the hash of "secret"
password: $2y$05$hS3s0Fbh5EMclimhNeCyEeH9VIynngvgDmGO6MbooXxle7S0D5boK

# Add TLS_* labels
client_cert_labels: TLS

acl:
- match: {account: "user1"}
actions: ["*"]
comment: User 1 has full access
# We can also require that alternate credentials be used through a label match:
- match: {account: "user1", labels: {ALTERNATE_CREDENTIALS: "true"}}
actions: ["*"]
comment: User 1 has full access
- match: {labels: {TLS_OU: developers}}
actions: ["pull"]
comment: Users whose certificate has the OU "developers" can pull

# To login with a client certificate:
#
# 1. Configure Docker to use the certificate: https://docs.docker.com/engine/security/certificates/
# 2. Run docker login. The username must be the same as the certificate CN (or header).
# The password can be anything.
32 changes: 32 additions & 0 deletions examples/reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ server: # Server settings.
# Use specific certificate and key.
certificate: "/path/to/server.pem"
key: "/path/to/server.key"
# Optional CA file for client certificate verification.
client_ca: /path/to/ca.crt
# Optional. Defaults to NoClientCert.
# Values can be found at https://pkg.go.dev/crypto/tls#ClientAuthType
client_auth_type: NoClientCert
#
# The following optional settings will fine tune TLS configuration to improve security.
# Leaving them unset should be just fine for most installations.
Expand Down Expand Up @@ -459,3 +464,30 @@ ext_authz:
plugin_authz:
plugin_path: ""

# If not empty, and a client certificate is present, the following
# lables prefixed with the given value, will be added:
# - <prefix>_O = <certificate O(s)>
# - <prefix>_OU = <certificate OU(s)>
# - <prefix>_DNS_NAMES = <certificate DNS SAN(s)>
client_cert_labels: TLS

# Populate the username/password from the HTTP request
alternate_credentials:
# If not empty, a label with the given name and single value "true" or "false" is added
# indicating whether or not alternate credentials was used.
label: ALTERNATE_CREDENTIALS
username:
# Required
# Choices:
# cn - Client certificate CN.
# header - The value of the header specified in `value`.
# static - The static value in `value`.
source: header
# Required if `source`` is static or header.
value: X-Forwarded-User
# See above for configuration values.
password:
# Required
source:
# Required
value:
Loading