Skip to content

Commit

Permalink
Merge pull request #235 from rikatz/ingress-ssl-auth
Browse files Browse the repository at this point in the history
Adds correct support for TLS Muthual autentication
  • Loading branch information
aledbf authored Feb 25, 2017
2 parents 77395e0 + a342c0b commit 0aabfba
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 52 deletions.
23 changes: 23 additions & 0 deletions controllers/nginx/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ The following annotations are supported:
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|[ingress.kubernetes.io/auth-url](#external-authentication)|string|
|[ingress.kubernetes.io/auth-tls-secret](#Certificate Authentication)|string|
|[ingress.kubernetes.io/auth-tls-verify-depth](#Certificate Authentication)|number|
|[ingress.kubernetes.io/enable-cors](#enable-cors)|true or false|
|[ingress.kubernetes.io/limit-connections](#rate-limiting)|number|
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
Expand Down Expand Up @@ -126,6 +128,27 @@ ingress.kubernetes.io/auth-realm: "realm string"

Please check the [auth](examples/auth/README.md) example.

### Certificate Authentication

It's possible to enable Certificate based authentication using additional annotations in Ingres Rule.

The annotations are:

```
ingress.kubernetes.io/auth-tls-secret: secretName
```

The name of the secret that contains the full Certificate Authority chain that is enabled to authenticate against this ingress. It's composed of namespace/secretName

```
ingress.kubernetes.io/auth-tls-verify-depth
```

The validation depth between the provided client certificate and the Certification Authority chain.

Please check the [tls-auth](examples/auth/client-certs/README.md) example.


### Enable CORS

To enable Cross-Origin Resource Sharing (CORS) in an Ingress rule add the annotation `ingress.kubernetes.io/enable-cors: "true"`. This will add a section in the server location enabling this functionality.
Expand Down
12 changes: 9 additions & 3 deletions controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,11 @@ http {
{{ $path := buildLocation $location }}
{{ $authPath := buildAuthLocation $location }}

{{ if not (empty $location.CertificateAuth.CertFileName) }}
# PEM sha: {{ $location.CertificateAuth.PemSHA }}
ssl_client_certificate {{ $location.CertificateAuth.CAFileName }};
{{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }}
# PEM sha: {{ $location.CertificateAuth.AuthSSLCert.PemSHA }}
ssl_client_certificate {{ $location.CertificateAuth.AuthSSLCert.CAFileName }};
ssl_verify_client on;
ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }};
{{ end }}

{{ if not (empty $authPath) }}
Expand Down Expand Up @@ -295,6 +296,11 @@ http {

proxy_set_header Host $host;

# Pass the extracted client certificate to the backend
{{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }}
proxy_set_header ssl-client-cert $ssl_client_cert;
{{ end }}

# Pass Real IP
proxy_set_header X-Real-IP $remote_addr;

Expand Down
46 changes: 32 additions & 14 deletions core/pkg/ingress/annotations/authtls/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,41 +28,59 @@ import (

const (
// name of the secret
authTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
annotationAuthTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
annotationAuthTLSDepth = "ingress.kubernetes.io/auth-tls-verify-depth"
defaultAuthTLSDepth = 1
)

type authTLS struct {
certResolver resolver.AuthCertificate
// AuthSSLConfig contains the AuthSSLCert used for muthual autentication
// and the configured ValidationDepth
type AuthSSLConfig struct {
AuthSSLCert resolver.AuthSSLCert
ValidationDepth int `json:"validationDepth"`
}

// NewParser creates a new TLS authentication annotation parser
func NewParser(resolver resolver.AuthCertificate) parser.IngressAnnotation {
return authTLS{resolver}
}

// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an external URL as source for authentication
type authTLS struct {
certResolver resolver.AuthCertificate
}

// Parse parses the annotations contained in the ingress
// rule used to use a Certificate as authentication method
func (a authTLS) Parse(ing *extensions.Ingress) (interface{}, error) {
str, err := parser.GetStringAnnotation(authTLSSecret, ing)

tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing)
if err != nil {
return nil, err
return &AuthSSLConfig{}, err
}

if str == "" {
return nil, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
if tlsauthsecret == "" {
return &AuthSSLConfig{}, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
}

_, _, err = k8s.ParseNameNS(str)
_, _, err = k8s.ParseNameNS(tlsauthsecret)
if err != nil {
return nil, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
return &AuthSSLConfig{}, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
}

tlsdepth, err := parser.GetIntAnnotation(annotationAuthTLSDepth, ing)
if err != nil || tlsdepth == 0 {
tlsdepth = defaultAuthTLSDepth
}

authCert, err := a.certResolver.GetAuthCertificate(str)
authCert, err := a.certResolver.GetAuthCertificate(tlsauthsecret)
if err != nil {
return nil, ing_errors.LocationDenied{
return &AuthSSLConfig{}, ing_errors.LocationDenied{
Reason: errors.Wrap(err, "error obtaining certificate"),
}
}

return authCert, nil
return &AuthSSLConfig{
AuthSSLCert: *authCert,
ValidationDepth: tlsdepth,
}, nil
}
25 changes: 16 additions & 9 deletions core/pkg/ingress/controller/backend_ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ func (ic *GenericController) syncSecret(k interface{}) error {
return nil
}

// getPemCertificate receives a secret, and creates a ingress.SSLCert as return.
// It parses the secret and verifies if it's a keypair, or a 'ca.crt' secret only.
func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLCert, error) {
secretInterface, exists, err := ic.secrLister.Store.GetByKey(secretName)
if err != nil {
Expand All @@ -108,19 +110,24 @@ func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLC
}

secret := secretInterface.(*api.Secret)
cert, ok := secret.Data[api.TLSCertKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no private key", secretName)
}
key, ok := secret.Data[api.TLSPrivateKeyKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no cert", secretName)
}
cert, okcert := secret.Data[api.TLSCertKey]
key, okkey := secret.Data[api.TLSPrivateKeyKey]

ca := secret.Data["ca.crt"]

nsSecName := strings.Replace(secretName, "/", "-", -1)
s, err := ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)

var s *ingress.SSLCert
if okcert && okkey {
glog.V(3).Infof("Found certificate and private key, configuring %v as a TLS Secret", secretName)
s, err = ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)
} else if ca != nil {
glog.V(3).Infof("Found only ca.crt, configuring %v as an Certificate Authentication secret", secretName)
s, err = ssl.AddCertAuth(nsSecName, ca)
} else {
return nil, fmt.Errorf("No keypair or CA cert could be found in %v", secretName)
}

if err != nil {
return nil, err
}
Expand Down
15 changes: 11 additions & 4 deletions core/pkg/ingress/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,16 +680,23 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress

// GetAuthCertificate ...
func (ic GenericController) GetAuthCertificate(secretName string) (*resolver.AuthSSLCert, error) {
key, err := ic.GetSecret(secretName)
if err != nil {
return &resolver.AuthSSLCert{}, fmt.Errorf("unexpected error: %v", err)
}
if key != nil {
ic.secretQueue.Enqueue(key)
}

bc, exists := ic.sslCertTracker.Get(secretName)
if !exists {
return &resolver.AuthSSLCert{}, fmt.Errorf("secret %v does not exists", secretName)
}
cert := bc.(*ingress.SSLCert)
return &resolver.AuthSSLCert{
Secret: secretName,
CertFileName: cert.PemFileName,
CAFileName: cert.CAFileName,
PemSHA: cert.PemSHA,
Secret: secretName,
CAFileName: cert.CAFileName,
PemSHA: cert.PemSHA,
}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions core/pkg/ingress/controller/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import (
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/resolver"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
)
Expand Down Expand Up @@ -136,7 +136,7 @@ func TestMergeLocationAnnotations(t *testing.T) {
"Redirect": rewrite.Redirect{},
"Whitelist": ipwhitelist.SourceRange{},
"Proxy": proxy.Configuration{},
"CertificateAuth": resolver.AuthSSLCert{},
"CertificateAuth": authtls.AuthSSLConfig{},
"UsePortInRedirects": true,
}

Expand Down
6 changes: 0 additions & 6 deletions core/pkg/ingress/resolver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ type Secret interface {
// AuthCertificate resolves a given secret name into an SSL certificate.
// The secret must contain 3 keys named:
// ca.crt: contains the certificate chain used for authentication
// tls.crt: (ignored) contains the tls certificate chain, or any other valid base64 data
// tls.key: (ignored) contains the tls secret key, or any other valid base64 data
type AuthCertificate interface {
GetAuthCertificate(string) (*AuthSSLCert, error)
}
Expand All @@ -48,10 +46,6 @@ type AuthCertificate interface {
type AuthSSLCert struct {
// Secret contains the name of the secret this was fetched from
Secret string `json:"secret"`
// CertFileName contains the filename the secret's 'tls.crt' was saved to
CertFileName string `json:"certFilename"`
// KeyFileName contains the path the secret's 'tls.key'
KeyFileName string `json:"keyFilename"`
// CAFileName contains the path to the secrets 'ca.crt'
CAFileName string `json:"caFilename"`
// PemSHA contains the SHA1 hash of the 'tls.crt' value
Expand Down
4 changes: 2 additions & 2 deletions core/pkg/ingress/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import (
cache_store "k8s.io/ingress/core/pkg/cache"
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/ingress/core/pkg/ingress/resolver"
)

var (
Expand Down Expand Up @@ -274,7 +274,7 @@ type Location struct {
// CertificateAuth indicates the access to this location requires
// external authentication
// +optional
CertificateAuth resolver.AuthSSLCert `json:"certificateAuth,omitempty"`
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth,omitempty"`
// UsePortInRedirects indicates if redirects must specify the port
// +optional
UsePortInRedirects bool `json:"use-port-in-redirects"`
Expand Down
56 changes: 44 additions & 12 deletions core/pkg/net/ssl/ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)

tempPemFile, err := ioutil.TempFile(ingress.DefaultSSLDirectory, pemName)

glog.V(3).Infof("Creating temp file %v for Keypair: %v", tempPemFile.Name(), pemName)
if err != nil {
return nil, fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
}
Expand Down Expand Up @@ -64,12 +66,12 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
return nil, err
}

pembBock, _ := pem.Decode(pemCerts)
if pembBock == nil {
pemBlock, _ := pem.Decode(pemCerts)
if pemBlock == nil {
return nil, fmt.Errorf("No valid PEM formatted block found")
}

pemCert, err := x509.ParseCertificate(pembBock.Bytes)
pemCert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -97,21 +99,21 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
return nil, errors.New(oe)
}

caName := fmt.Sprintf("ca-%v.pem", name)
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)
f, err := os.Create(caFileName)
caFile, err := os.OpenFile(pemFileName, os.O_RDWR|os.O_APPEND, 0600)
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
return nil, fmt.Errorf("Could not open file %v for writing additional CA chains: %v", pemFileName, err)
}
defer f.Close()
_, err = f.Write(ca)

defer caFile.Close()
_, err = caFile.Write([]byte("\n"))
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
return nil, fmt.Errorf("could not append CA to cert file %v: %v", pemFileName, err)
}
f.Write([]byte("\n"))
caFile.Write(ca)
caFile.Write([]byte("\n"))

return &ingress.SSLCert{
CAFileName: caFileName,
CAFileName: pemFileName,
PemFileName: pemFileName,
PemSHA: pemSHA1(pemFileName),
CN: cn,
Expand All @@ -125,6 +127,36 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
}, nil
}

// AddCertAuth creates a .pem file with the specified CAs to be used in Cert Authentication
// If it's already exists, it's clobbered.
func AddCertAuth(name string, ca []byte) (*ingress.SSLCert, error) {

caName := fmt.Sprintf("ca-%v.pem", name)
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)

pemCABlock, _ := pem.Decode(ca)
if pemCABlock == nil {
return nil, fmt.Errorf("No valid PEM formatted block found")
}

_, err := x509.ParseCertificate(pemCABlock.Bytes)
if err != nil {
return nil, err
}

err = ioutil.WriteFile(caFileName, ca, 0644)
if err != nil {
return nil, fmt.Errorf("could not write CA file %v: %v", caFileName, err)
}

glog.V(3).Infof("Created CA Certificate for authentication: %v", caFileName)
return &ingress.SSLCert{
CAFileName: caFileName,
PemFileName: caFileName,
PemSHA: pemSHA1(caFileName),
}, nil
}

// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory
// in order to find a file with the name dhparam.pem. If such file exists it will
// returns the path. If not it just returns an empty string
Expand Down
Loading

0 comments on commit 0aabfba

Please sign in to comment.