Adds correct support for TLS Muthual autentication and depth verification
modified: controllers/nginx/configuration.md modified: controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl modified: core/pkg/ingress/annotations/authtls/main.go modified: core/pkg/ingress/controller/backend_ssl.go modified: core/pkg/ingress/controller/controller.go modified: core/pkg/ingress/controller/util_test.go modified: core/pkg/ingress/resolver/main.go modified: core/pkg/ingress/types.go modified: core/pkg/net/ssl/ssl.go modified: examples/PREREQUISITES.md new file: examples/auth/client-certs/nginx/README.md new file: examples/auth/client-certs/nginx/nginx-tls-auth.yaml
This commit is contained in:
parent
f5e005f84f
commit
a342c0bce3
12 changed files with 349 additions and 52 deletions
|
@ -44,6 +44,8 @@ The following annotations are supported:
|
||||||
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|
||||||
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|
||||||
|[ingress.kubernetes.io/auth-url](#external-authentication)|string|
|
|[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/enable-cors](#enable-cors)|true or false|
|
||||||
|[ingress.kubernetes.io/limit-connections](#rate-limiting)|number|
|
|[ingress.kubernetes.io/limit-connections](#rate-limiting)|number|
|
||||||
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
|
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
|
||||||
|
@ -126,6 +128,27 @@ ingress.kubernetes.io/auth-realm: "realm string"
|
||||||
|
|
||||||
Please check the [auth](examples/auth/README.md) example.
|
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
|
### 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.
|
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.
|
||||||
|
|
|
@ -225,10 +225,11 @@ http {
|
||||||
{{ $path := buildLocation $location }}
|
{{ $path := buildLocation $location }}
|
||||||
{{ $authPath := buildAuthLocation $location }}
|
{{ $authPath := buildAuthLocation $location }}
|
||||||
|
|
||||||
{{ if not (empty $location.CertificateAuth.CertFileName) }}
|
{{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }}
|
||||||
# PEM sha: {{ $location.CertificateAuth.PemSHA }}
|
# PEM sha: {{ $location.CertificateAuth.AuthSSLCert.PemSHA }}
|
||||||
ssl_client_certificate {{ $location.CertificateAuth.CAFileName }};
|
ssl_client_certificate {{ $location.CertificateAuth.AuthSSLCert.CAFileName }};
|
||||||
ssl_verify_client on;
|
ssl_verify_client on;
|
||||||
|
ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }};
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if not (empty $authPath) }}
|
{{ if not (empty $authPath) }}
|
||||||
|
@ -295,6 +296,11 @@ http {
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
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
|
# Pass Real IP
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
||||||
|
|
|
@ -28,11 +28,16 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// name of the secret
|
// 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 {
|
// AuthSSLConfig contains the AuthSSLCert used for muthual autentication
|
||||||
certResolver resolver.AuthCertificate
|
// and the configured ValidationDepth
|
||||||
|
type AuthSSLConfig struct {
|
||||||
|
AuthSSLCert resolver.AuthSSLCert
|
||||||
|
ValidationDepth int `json:"validationDepth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewParser creates a new TLS authentication annotation parser
|
// NewParser creates a new TLS authentication annotation parser
|
||||||
|
@ -40,29 +45,42 @@ func NewParser(resolver resolver.AuthCertificate) parser.IngressAnnotation {
|
||||||
return authTLS{resolver}
|
return authTLS{resolver}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseAnnotations parses the annotations contained in the ingress
|
type authTLS struct {
|
||||||
// rule used to use an external URL as source for authentication
|
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) {
|
func (a authTLS) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||||
str, err := parser.GetStringAnnotation(authTLSSecret, ing)
|
|
||||||
|
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &AuthSSLConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if str == "" {
|
if tlsauthsecret == "" {
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = k8s.ParseNameNS(str)
|
_, _, err = k8s.ParseNameNS(tlsauthsecret)
|
||||||
if err != nil {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
authCert, err := a.certResolver.GetAuthCertificate(str)
|
tlsdepth, err := parser.GetIntAnnotation(annotationAuthTLSDepth, ing)
|
||||||
|
if err != nil || tlsdepth == 0 {
|
||||||
|
tlsdepth = defaultAuthTLSDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
authCert, err := a.certResolver.GetAuthCertificate(tlsauthsecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ing_errors.LocationDenied{
|
return &AuthSSLConfig{}, ing_errors.LocationDenied{
|
||||||
Reason: errors.Wrap(err, "error obtaining certificate"),
|
Reason: errors.Wrap(err, "error obtaining certificate"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return authCert, nil
|
return &AuthSSLConfig{
|
||||||
|
AuthSSLCert: *authCert,
|
||||||
|
ValidationDepth: tlsdepth,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,8 @@ func (ic *GenericController) syncSecret(k interface{}) error {
|
||||||
return nil
|
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) {
|
func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLCert, error) {
|
||||||
secretInterface, exists, err := ic.secrLister.Store.GetByKey(secretName)
|
secretInterface, exists, err := ic.secrLister.Store.GetByKey(secretName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -108,19 +110,24 @@ func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLC
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := secretInterface.(*api.Secret)
|
secret := secretInterface.(*api.Secret)
|
||||||
cert, ok := secret.Data[api.TLSCertKey]
|
cert, okcert := secret.Data[api.TLSCertKey]
|
||||||
if !ok {
|
key, okkey := secret.Data[api.TLSPrivateKeyKey]
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ca := secret.Data["ca.crt"]
|
ca := secret.Data["ca.crt"]
|
||||||
|
|
||||||
nsSecName := strings.Replace(secretName, "/", "-", -1)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -680,16 +680,23 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
|
||||||
|
|
||||||
// GetAuthCertificate ...
|
// GetAuthCertificate ...
|
||||||
func (ic GenericController) GetAuthCertificate(secretName string) (*resolver.AuthSSLCert, error) {
|
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)
|
bc, exists := ic.sslCertTracker.Get(secretName)
|
||||||
if !exists {
|
if !exists {
|
||||||
return &resolver.AuthSSLCert{}, fmt.Errorf("secret %v does not exists", secretName)
|
return &resolver.AuthSSLCert{}, fmt.Errorf("secret %v does not exists", secretName)
|
||||||
}
|
}
|
||||||
cert := bc.(*ingress.SSLCert)
|
cert := bc.(*ingress.SSLCert)
|
||||||
return &resolver.AuthSSLCert{
|
return &resolver.AuthSSLCert{
|
||||||
Secret: secretName,
|
Secret: secretName,
|
||||||
CertFileName: cert.PemFileName,
|
CAFileName: cert.CAFileName,
|
||||||
CAFileName: cert.CAFileName,
|
PemSHA: cert.PemSHA,
|
||||||
PemSHA: cert.PemSHA,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,11 @@ import (
|
||||||
"k8s.io/ingress/core/pkg/ingress"
|
"k8s.io/ingress/core/pkg/ingress"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
|
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
|
"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/ipwhitelist"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
|
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
|
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
|
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
|
||||||
"k8s.io/ingress/core/pkg/ingress/resolver"
|
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
)
|
)
|
||||||
|
@ -136,7 +136,7 @@ func TestMergeLocationAnnotations(t *testing.T) {
|
||||||
"Redirect": rewrite.Redirect{},
|
"Redirect": rewrite.Redirect{},
|
||||||
"Whitelist": ipwhitelist.SourceRange{},
|
"Whitelist": ipwhitelist.SourceRange{},
|
||||||
"Proxy": proxy.Configuration{},
|
"Proxy": proxy.Configuration{},
|
||||||
"CertificateAuth": resolver.AuthSSLCert{},
|
"CertificateAuth": authtls.AuthSSLConfig{},
|
||||||
"UsePortInRedirects": true,
|
"UsePortInRedirects": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,6 @@ type Secret interface {
|
||||||
// AuthCertificate resolves a given secret name into an SSL certificate.
|
// AuthCertificate resolves a given secret name into an SSL certificate.
|
||||||
// The secret must contain 3 keys named:
|
// The secret must contain 3 keys named:
|
||||||
// ca.crt: contains the certificate chain used for authentication
|
// 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 {
|
type AuthCertificate interface {
|
||||||
GetAuthCertificate(string) (*AuthSSLCert, error)
|
GetAuthCertificate(string) (*AuthSSLCert, error)
|
||||||
}
|
}
|
||||||
|
@ -48,10 +46,6 @@ type AuthCertificate interface {
|
||||||
type AuthSSLCert struct {
|
type AuthSSLCert struct {
|
||||||
// Secret contains the name of the secret this was fetched from
|
// Secret contains the name of the secret this was fetched from
|
||||||
Secret string `json:"secret"`
|
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 contains the path to the secrets 'ca.crt'
|
||||||
CAFileName string `json:"caFilename"`
|
CAFileName string `json:"caFilename"`
|
||||||
// PemSHA contains the SHA1 hash of the 'tls.crt' value
|
// PemSHA contains the SHA1 hash of the 'tls.crt' value
|
||||||
|
|
|
@ -27,12 +27,12 @@ import (
|
||||||
cache_store "k8s.io/ingress/core/pkg/cache"
|
cache_store "k8s.io/ingress/core/pkg/cache"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
|
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
|
"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/ipwhitelist"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
|
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
|
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
|
||||||
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
|
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
|
||||||
"k8s.io/ingress/core/pkg/ingress/defaults"
|
"k8s.io/ingress/core/pkg/ingress/defaults"
|
||||||
"k8s.io/ingress/core/pkg/ingress/resolver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -274,7 +274,7 @@ type Location struct {
|
||||||
// CertificateAuth indicates the access to this location requires
|
// CertificateAuth indicates the access to this location requires
|
||||||
// external authentication
|
// external authentication
|
||||||
// +optional
|
// +optional
|
||||||
CertificateAuth resolver.AuthSSLCert `json:"certificateAuth,omitempty"`
|
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth,omitempty"`
|
||||||
// UsePortInRedirects indicates if redirects must specify the port
|
// UsePortInRedirects indicates if redirects must specify the port
|
||||||
// +optional
|
// +optional
|
||||||
UsePortInRedirects bool `json:"use-port-in-redirects"`
|
UsePortInRedirects bool `json:"use-port-in-redirects"`
|
||||||
|
|
|
@ -37,6 +37,8 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
|
||||||
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
|
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
|
||||||
|
|
||||||
tempPemFile, err := ioutil.TempFile(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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
|
return nil, fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
|
||||||
}
|
}
|
||||||
|
@ -64,12 +66,12 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pembBock, _ := pem.Decode(pemCerts)
|
pemBlock, _ := pem.Decode(pemCerts)
|
||||||
if pembBock == nil {
|
if pemBlock == nil {
|
||||||
return nil, fmt.Errorf("No valid PEM formatted block found")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -97,21 +99,21 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
|
||||||
return nil, errors.New(oe)
|
return nil, errors.New(oe)
|
||||||
}
|
}
|
||||||
|
|
||||||
caName := fmt.Sprintf("ca-%v.pem", name)
|
caFile, err := os.OpenFile(pemFileName, os.O_RDWR|os.O_APPEND, 0600)
|
||||||
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)
|
|
||||||
f, err := os.Create(caFileName)
|
|
||||||
if err != nil {
|
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 {
|
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{
|
return &ingress.SSLCert{
|
||||||
CAFileName: caFileName,
|
CAFileName: pemFileName,
|
||||||
PemFileName: pemFileName,
|
PemFileName: pemFileName,
|
||||||
PemSHA: pemSHA1(pemFileName),
|
PemSHA: pemSHA1(pemFileName),
|
||||||
CN: cn,
|
CN: cn,
|
||||||
|
@ -125,6 +127,36 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
|
||||||
}, nil
|
}, 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
|
// 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
|
// 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
|
// returns the path. If not it just returns an empty string
|
||||||
|
|
|
@ -36,6 +36,105 @@ $ kubectl create secret tls tls-secret --key tls.key --cert tls.crt
|
||||||
secret "tls-secret" created
|
secret "tls-secret" created
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CA Authentication
|
||||||
|
You can act as your very own CA, or use an existing one. As an exercise / learning, we're going to generate our
|
||||||
|
own CA, and also generate a client certificate.
|
||||||
|
|
||||||
|
These instructions are based in CoreOS OpenSSL [instructions](https://coreos.com/kubernetes/docs/latest/openssl.html)
|
||||||
|
|
||||||
|
### Generating a CA
|
||||||
|
|
||||||
|
First of all, you've to generate a CA. This is going to be the one who will sign your client certificates.
|
||||||
|
In real production world, you may face CAs with intermediate certificates, as the following:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ openssl s_client -connect www.google.com:443
|
||||||
|
[...]
|
||||||
|
---
|
||||||
|
Certificate chain
|
||||||
|
0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=www.google.com
|
||||||
|
i:/C=US/O=Google Inc/CN=Google Internet Authority G2
|
||||||
|
1 s:/C=US/O=Google Inc/CN=Google Internet Authority G2
|
||||||
|
i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
|
||||||
|
2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
|
||||||
|
i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate our CA Certificate, we've to run the following commands:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ openssl genrsa -out ca.key 2048
|
||||||
|
$ openssl req -x509 -new -nodes -key ca.key -days 10000 -out ca.crt -subj "/CN=example-ca"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate two files: A private key (ca.key) and a public key (ca.crt). This CA is valid for 10000 days.
|
||||||
|
The ca.crt can be used later in the step of creation of CA authentication secret.
|
||||||
|
|
||||||
|
### Generating the client certificate
|
||||||
|
The following steps generates a client certificate signed by the CA generated above. This client can be
|
||||||
|
used to authenticate in a tls-auth configured ingress.
|
||||||
|
|
||||||
|
First, we need to generate an 'openssl.cnf' file that will be used while signing the keys:
|
||||||
|
|
||||||
|
```
|
||||||
|
[req]
|
||||||
|
req_extensions = v3_req
|
||||||
|
distinguished_name = req_distinguished_name
|
||||||
|
[req_distinguished_name]
|
||||||
|
[ v3_req ]
|
||||||
|
basicConstraints = CA:FALSE
|
||||||
|
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, a user generates his very own private key (that he needs to keep secret)
|
||||||
|
and a CSR (Certificate Signing Request) that will be sent to the CA to sign and generate a certificate.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ openssl genrsa -out client1.key 2048
|
||||||
|
$ openssl req -new -key client1.key -out client1.csr -subj "/CN=client1" -config openssl.cnf
|
||||||
|
```
|
||||||
|
|
||||||
|
As the CA receives the generated 'client1.csr' file, it signs it and generates a client.crt certificate:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ openssl x509 -req -in client1.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client1.crt -days 365 -extensions v3_req -extfile openssl.cnf
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you'll have 3 files: the client.key (user's private key), client.crt (user's public key) and client.csr (disposable CSR).
|
||||||
|
|
||||||
|
|
||||||
|
### Creating the CA Authentication secret
|
||||||
|
If you're using the CA Authentication feature, you need to generate a secret containing
|
||||||
|
all the authorized CAs. You must download them from your CA site in PEM format (like the following):
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
[....]
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
You can have as many certificates as you want. If they're in the binary DER format,
|
||||||
|
you can convert them as the following:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ openssl x509 -in certificate.der -inform der -out certificate.crt -outform pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you've to concatenate them all in only one file, named 'ca.crt' as the following:
|
||||||
|
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ cat certificate1.crt certificate2.crt certificate3.crt >> ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
The final step is to create a secret with the content of this file. This secret is going to be used in
|
||||||
|
the TLS Auth directive:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ kubectl create secret generic caingress --namespace=default --from-file=ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
## Test HTTP Service
|
## Test HTTP Service
|
||||||
|
|
||||||
All examples that require a test HTTP Service use the standard http-svc pod,
|
All examples that require a test HTTP Service use the standard http-svc pod,
|
||||||
|
|
86
examples/auth/client-certs/nginx/README.md
Normal file
86
examples/auth/client-certs/nginx/README.md
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# TLS authentication
|
||||||
|
|
||||||
|
This example demonstrates how to enable the TLS Authentication through the nginx Ingress controller.
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
* CA: Certificate authority signing the client cert, in this example we will play the role of a CA.
|
||||||
|
You can generate a CA cert as show in this doc.
|
||||||
|
|
||||||
|
* CA Certificate(s) - Certificate Authority public key. Client certs must chain back to this cert,
|
||||||
|
meaning the Issuer field of some certificate in the chain leading up to the client cert must contain
|
||||||
|
the name of this CA. For purposes of this example, this is a self signed certificate.
|
||||||
|
|
||||||
|
* CA chains: A chain of certificates where the parent has a Subject field matching the Issuer field of
|
||||||
|
the child, except for the root, which has Issuer == Subject.
|
||||||
|
|
||||||
|
* Client Cert: Certificate used by the clients to authenticate themselves with the loadbalancer/backends.
|
||||||
|
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You need a valid CA File, composed of a group of valid enabled CAs. This MUST be in PEM Format.
|
||||||
|
The instructions are described [here](../../../PREREQUISITES.md#ca-authentication)
|
||||||
|
|
||||||
|
Also your ingress must be configured as a HTTPs/TLS Ingress.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Certificate Authentication is achieved through 2 annotations on the Ingress, as shown in the [example](nginx-tls-auth.yaml).
|
||||||
|
|
||||||
|
|Name|Description|Values|
|
||||||
|
| --- | --- | --- |
|
||||||
|
|ingress.kubernetes.io/auth-tls-secret|Sets the secret that contains the authorized CA Chain|string|
|
||||||
|
|ingress.kubernetes.io/auth-tls-verify-depth|The verification depth Certificate Authentication will make|number (default to 1)|
|
||||||
|
|
||||||
|
|
||||||
|
The following command instructs the controller to enable TLS authentication using the secret from the ``ingress.kubernetes.io/auth-tls-secret``
|
||||||
|
annotation on the Ingress. Clients must present this cert to the loadbalancer, or they will receive a HTTP 400 response
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ kubectl create -f nginx-tls-auth.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
You can confirm that the Ingress works.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ kubectl describe ing nginx-test
|
||||||
|
Name: nginx-test
|
||||||
|
Namespace: default
|
||||||
|
Address: 104.198.183.6
|
||||||
|
Default backend: default-http-backend:80 (10.180.0.4:8080,10.240.0.2:8080)
|
||||||
|
TLS:
|
||||||
|
tls-secret terminates ingress.test.com
|
||||||
|
Rules:
|
||||||
|
Host Path Backends
|
||||||
|
---- ---- --------
|
||||||
|
*
|
||||||
|
http-svc:80 (<none>)
|
||||||
|
Annotations:
|
||||||
|
auth-tls-secret: default/caingress
|
||||||
|
auth-tls-verify-depth: 3
|
||||||
|
|
||||||
|
Events:
|
||||||
|
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
|
||||||
|
--------- -------- ----- ---- ------------- -------- ------ -------
|
||||||
|
7s 7s 1 {nginx-ingress-controller } Normal CREATE default/nginx-test
|
||||||
|
7s 7s 1 {nginx-ingress-controller } Normal UPDATE default/nginx-test
|
||||||
|
7s 7s 1 {nginx-ingress-controller } Normal CREATE ip: 104.198.183.6
|
||||||
|
7s 7s 1 {nginx-ingress-controller } Warning MAPPING Ingress rule 'default/nginx-test' contains no path definition. Assuming /
|
||||||
|
|
||||||
|
|
||||||
|
$ curl -k https://ingress.test.com
|
||||||
|
HTTP/1.1 400 Bad Request
|
||||||
|
Server: nginx/1.11.9
|
||||||
|
|
||||||
|
$ curl -I -k --key ~/user.key --cert ~/user.cer https://ingress.test.com
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Server: nginx/1.11.9
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
You must use the full DNS name while testing, as NGINX relies on the Server Name (SNI) to select the correct Ingress to be used.
|
||||||
|
|
||||||
|
The curl version used here was ``curl 7.47.0``
|
25
examples/auth/client-certs/nginx/nginx-tls-auth.yaml
Normal file
25
examples/auth/client-certs/nginx/nginx-tls-auth.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
# Create this with kubectl create secret generic caingress --from-file=ca.crt --namespace=default
|
||||||
|
ingress.kubernetes.io/auth-tls-secret: "default/caingress"
|
||||||
|
ingress.kubernetes.io/auth-tls-verify-depth: "3"
|
||||||
|
kubernetes.io/ingress.class: "nginx"
|
||||||
|
name: nginx-test
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: ingress.test.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: http-svc:80
|
||||||
|
servicePort: 80
|
||||||
|
path: /
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- ingress.test.com
|
||||||
|
# Create this cert as described in 'multi-tls' example
|
||||||
|
secretName: cert
|
||||||
|
|
Loading…
Reference in a new issue