2016-05-10 03:14:47 +00:00
/ *
2016-09-08 11:02:39 +00:00
Copyright 2015 The Kubernetes Authors .
2016-05-10 03:14:47 +00:00
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package auth
import (
"fmt"
2021-08-06 14:18:17 +00:00
"os"
2016-05-11 05:22:17 +00:00
"regexp"
2019-09-13 14:59:32 +00:00
"strings"
2016-05-11 05:22:17 +00:00
2017-07-16 19:19:59 +00:00
api "k8s.io/api/core/v1"
2021-08-21 20:42:00 +00:00
networking "k8s.io/api/networking/v1"
2019-03-09 13:44:06 +00:00
"k8s.io/client-go/tools/cache"
2016-11-16 18:24:26 +00:00
2017-11-07 22:02:12 +00:00
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
2022-07-20 18:53:44 +00:00
"k8s.io/ingress-nginx/pkg/util/file"
2016-05-10 03:14:47 +00:00
)
2023-07-22 03:32:07 +00:00
const (
authSecretTypeAnnotation = "auth-secret-type" //#nosec G101
authRealmAnnotation = "auth-realm"
authTypeAnnotation = "auth-type"
// This should be exported as it is imported by other packages
AuthSecretAnnotation = "auth-secret" //#nosec G101
)
2016-05-10 03:14:47 +00:00
var (
2023-07-22 03:32:07 +00:00
authTypeRegex = regexp . MustCompile ( ` basic|digest ` )
authSecretTypeRegex = regexp . MustCompile ( ` auth-file|auth-map ` )
2016-12-29 20:02:06 +00:00
// AuthDirectory default directory used to store files
// to authenticate request
AuthDirectory = "/etc/ingress-controller/auth"
2016-05-31 16:22:04 +00:00
)
2016-05-11 05:22:17 +00:00
2023-07-22 03:32:07 +00:00
var AuthSecretConfig = parser . AnnotationConfig {
Validator : parser . ValidateRegex ( * parser . BasicCharsRegex , true ) ,
Scope : parser . AnnotationScopeLocation ,
Risk : parser . AnnotationRiskMedium , // Medium as it allows a subset of chars
Documentation : ` This annotation defines the name of the Secret that contains the usernames and passwords which are granted access to the paths defined in the Ingress rules. ` ,
}
var authSecretAnnotations = parser . Annotation {
Group : "authentication" ,
Annotations : parser . AnnotationFields {
AuthSecretAnnotation : AuthSecretConfig ,
authSecretTypeAnnotation : {
Validator : parser . ValidateRegex ( * authSecretTypeRegex , true ) ,
Scope : parser . AnnotationScopeLocation ,
Risk : parser . AnnotationRiskLow ,
Documentation : ` This annotation what is the format of auth - secret value . Can be "auth-file" that defines the content of an htpasswd file , or "auth-map" where each key
is a user and each value is the password . ` ,
} ,
authRealmAnnotation : {
Validator : parser . ValidateRegex ( * parser . CharsWithSpace , false ) ,
Scope : parser . AnnotationScopeLocation ,
Risk : parser . AnnotationRiskMedium , // Medium as it allows a subset of chars
Documentation : ` This annotation defines the realm (message) that should be shown to user when authentication is requested. ` ,
} ,
authTypeAnnotation : {
Validator : parser . ValidateRegex ( * authTypeRegex , true ) ,
Scope : parser . AnnotationScopeLocation ,
Risk : parser . AnnotationRiskLow ,
Documentation : ` This annotation defines the basic authentication type. Should be "basic" or "digest" ` ,
} ,
} ,
}
2020-01-27 03:02:08 +00:00
const (
fileAuth = "auth-file"
mapAuth = "auth-map"
)
2017-11-07 16:36:51 +00:00
// Config returns authentication configuration for an Ingress rule
type Config struct {
2019-09-13 14:59:32 +00:00
Type string ` json:"type" `
Realm string ` json:"realm" `
File string ` json:"file" `
Secured bool ` json:"secured" `
FileSHA string ` json:"fileSha" `
Secret string ` json:"secret" `
SecretType string ` json:"secretType" `
2016-05-10 03:14:47 +00:00
}
2017-11-07 16:36:51 +00:00
// Equal tests for equality between two Config types
func ( bd1 * Config ) Equal ( bd2 * Config ) bool {
2017-06-14 21:33:12 +00:00
if bd1 == bd2 {
return true
}
if bd1 == nil || bd2 == nil {
return false
}
if bd1 . Type != bd2 . Type {
return false
}
if bd1 . Realm != bd2 . Realm {
return false
}
if bd1 . File != bd2 . File {
return false
}
if bd1 . Secured != bd2 . Secured {
return false
}
2017-07-31 18:22:10 +00:00
if bd1 . FileSHA != bd2 . FileSHA {
return false
}
2018-01-22 22:07:31 +00:00
if bd1 . Secret != bd2 . Secret {
return false
}
2017-06-14 21:33:12 +00:00
return true
}
2016-12-29 20:02:06 +00:00
type auth struct {
2023-07-22 03:32:07 +00:00
r resolver . Resolver
authDirectory string
annotationConfig parser . Annotation
2016-12-29 20:02:06 +00:00
}
// NewParser creates a new authentication annotation parser
2017-11-08 20:58:57 +00:00
func NewParser ( authDirectory string , r resolver . Resolver ) parser . IngressAnnotation {
2023-07-22 03:32:07 +00:00
return auth {
r : r ,
authDirectory : authDirectory ,
annotationConfig : authSecretAnnotations ,
}
2016-12-29 20:02:06 +00:00
}
// Parse parses the annotations contained in the ingress
2016-05-11 05:22:17 +00:00
// rule used to add authentication in the paths defined in the rule
2016-05-10 03:14:47 +00:00
// and generated an htpasswd compatible file to be used as source
// during the authentication process
2019-06-09 22:49:59 +00:00
func ( a auth ) Parse ( ing * networking . Ingress ) ( interface { } , error ) {
2023-07-22 03:32:07 +00:00
at , err := parser . GetStringAnnotation ( authTypeAnnotation , ing , a . annotationConfig . Annotations )
2016-05-10 03:14:47 +00:00
if err != nil {
2016-12-29 20:02:06 +00:00
return nil , err
2016-11-10 22:56:29 +00:00
}
if ! authTypeRegex . MatchString ( at ) {
2016-12-29 20:02:06 +00:00
return nil , ing_errors . NewLocationDenied ( "invalid authentication type" )
2016-05-10 03:14:47 +00:00
}
2019-09-13 14:59:32 +00:00
var secretType string
2023-07-22 03:32:07 +00:00
secretType , err = parser . GetStringAnnotation ( authSecretTypeAnnotation , ing , a . annotationConfig . Annotations )
2019-09-13 14:59:32 +00:00
if err != nil {
2023-07-22 03:32:07 +00:00
if ing_errors . IsValidationError ( err ) {
return nil , err
}
2020-01-27 03:02:08 +00:00
secretType = fileAuth
2019-09-13 14:59:32 +00:00
}
2023-07-22 03:32:07 +00:00
s , err := parser . GetStringAnnotation ( AuthSecretAnnotation , ing , a . annotationConfig . Annotations )
2016-05-10 03:14:47 +00:00
if err != nil {
2016-12-29 20:02:06 +00:00
return nil , ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "error reading secret name from annotation: %w" , err ) ,
2016-12-29 20:02:06 +00:00
}
2016-05-10 03:14:47 +00:00
}
2019-03-09 13:44:06 +00:00
sns , sname , err := cache . SplitMetaNamespaceKey ( s )
if err != nil {
return nil , ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "error reading secret name from annotation: %w" , err ) ,
2019-03-09 13:44:06 +00:00
}
}
if sns == "" {
sns = ing . Namespace
}
2023-07-22 03:32:07 +00:00
secCfg := a . r . GetSecurityConfiguration ( )
// We don't accept different namespaces for secrets.
if ! secCfg . AllowCrossNamespaceResources && sns != ing . Namespace {
return nil , ing_errors . LocationDenied {
Reason : fmt . Errorf ( "cross namespace usage of secrets is not allowed" ) ,
}
}
2019-03-09 13:44:06 +00:00
name := fmt . Sprintf ( "%v/%v" , sns , sname )
2017-11-08 20:58:57 +00:00
secret , err := a . r . GetSecret ( name )
2016-05-10 03:14:47 +00:00
if err != nil {
2016-12-29 20:02:06 +00:00
return nil , ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "unexpected error reading secret %s: %w" , name , err ) ,
2016-12-29 20:02:06 +00:00
}
2016-05-10 03:14:47 +00:00
}
2023-07-22 03:32:07 +00:00
realm , err := parser . GetStringAnnotation ( authRealmAnnotation , ing , a . annotationConfig . Annotations )
if ing_errors . IsValidationError ( err ) {
return nil , err
}
2016-05-10 03:14:47 +00:00
2020-01-27 03:02:08 +00:00
passFilename := fmt . Sprintf ( "%v/%v-%v-%v.passwd" , a . authDirectory , ing . GetNamespace ( ) , ing . UID , secret . UID )
2019-09-13 14:59:32 +00:00
2020-01-27 03:02:08 +00:00
switch secretType {
case fileAuth :
err = dumpSecretAuthFile ( passFilename , secret )
2019-09-13 14:59:32 +00:00
if err != nil {
return nil , err
}
2020-01-27 03:02:08 +00:00
case mapAuth :
err = dumpSecretAuthMap ( passFilename , secret )
2019-09-13 14:59:32 +00:00
if err != nil {
return nil , err
}
2020-01-27 03:02:08 +00:00
default :
2019-09-13 14:59:32 +00:00
return nil , ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "invalid auth-secret-type in annotation, must be 'auth-file' or 'auth-map': %w" , err ) ,
2019-09-13 14:59:32 +00:00
}
2016-05-10 03:14:47 +00:00
}
2017-11-07 16:36:51 +00:00
return & Config {
2019-09-13 14:59:32 +00:00
Type : at ,
Realm : realm ,
2020-01-27 03:02:08 +00:00
File : passFilename ,
2019-09-13 14:59:32 +00:00
Secured : true ,
2020-01-27 03:02:08 +00:00
FileSHA : file . SHA1 ( passFilename ) ,
2019-09-13 14:59:32 +00:00
Secret : name ,
SecretType : secretType ,
2016-05-11 05:22:17 +00:00
} , nil
2016-05-10 03:14:47 +00:00
}
// dumpSecret dumps the content of a secret into a file
// in the expected format for the specified authorization
2019-09-13 14:59:32 +00:00
func dumpSecretAuthFile ( filename string , secret * api . Secret ) error {
2016-05-11 05:22:17 +00:00
val , ok := secret . Data [ "auth" ]
if ! ok {
2016-12-29 20:02:06 +00:00
return ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "the secret %s does not contain a key with value auth" , secret . Name ) ,
2016-12-29 20:02:06 +00:00
}
2016-05-11 05:22:17 +00:00
}
2016-05-10 03:14:47 +00:00
2021-08-06 14:18:17 +00:00
err := os . WriteFile ( filename , val , file . ReadWriteByUser )
2016-12-29 20:02:06 +00:00
if err != nil {
return ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "unexpected error creating password file: %w" , err ) ,
2016-12-29 20:02:06 +00:00
}
}
return nil
2016-05-10 03:14:47 +00:00
}
2019-09-13 14:59:32 +00:00
func dumpSecretAuthMap ( filename string , secret * api . Secret ) error {
builder := & strings . Builder { }
for user , pass := range secret . Data {
builder . WriteString ( user )
builder . WriteString ( ":" )
2023-06-01 14:29:47 +00:00
builder . Write ( pass )
2019-09-13 14:59:32 +00:00
builder . WriteString ( "\n" )
}
2021-08-06 14:18:17 +00:00
err := os . WriteFile ( filename , [ ] byte ( builder . String ( ) ) , file . ReadWriteByUser )
2019-09-13 14:59:32 +00:00
if err != nil {
return ing_errors . LocationDenied {
2022-01-10 00:29:12 +00:00
Reason : fmt . Errorf ( "unexpected error creating password file: %w" , err ) ,
2019-09-13 14:59:32 +00:00
}
}
return nil
}
2023-07-22 03:32:07 +00:00
func ( a auth ) GetDocumentation ( ) parser . AnnotationFields {
return a . annotationConfig . Annotations
}
func ( a auth ) Validate ( anns map [ string ] string ) error {
maxrisk := parser . StringRiskToRisk ( a . r . GetSecurityConfiguration ( ) . AnnotationsRiskLevel )
return parser . CheckAnnotationRisk ( anns , maxrisk , authSecretAnnotations . Annotations )
}