diff --git a/pkg/cmd/server/api/helpers.go b/pkg/cmd/server/api/helpers.go index 7396241dc78b..afd3d480e5d3 100644 --- a/pkg/cmd/server/api/helpers.go +++ b/pkg/cmd/server/api/helpers.go @@ -256,6 +256,8 @@ func GetMasterFileReferences(config *MasterConfig) []*string { refs = append(refs, &config.ControllerConfig.ServiceServingCert.Signer.KeyFile) } + refs = append(refs, &config.AuditConfig.AuditFilePath) + return refs } diff --git a/pkg/cmd/server/api/types.go b/pkg/cmd/server/api/types.go index 70c5a9304265..6a8e4fa63b9a 100644 --- a/pkg/cmd/server/api/types.go +++ b/pkg/cmd/server/api/types.go @@ -325,6 +325,14 @@ type AuditConfig struct { // If this flag is set, audit log will be printed in the logs. // The logs contains, method, user and a requested URL. Enabled bool + // All requests coming to the apiserver will be logged to this file. + AuditFilePath string + // Maximum number of days to retain old log files based on the timestamp encoded in their filename. + MaximumFileRetentionDays int + // Maximum number of old log files to retain. + MaximumRetainedFiles int + // Maximum size in megabytes of the log file before it gets rotated. Defaults to 100MB. + MaximumFileSizeMegabytes int } // JenkinsPipelineConfig holds configuration for the Jenkins pipeline strategy diff --git a/pkg/cmd/server/api/v1/swagger_doc.go b/pkg/cmd/server/api/v1/swagger_doc.go index 7df7fcf49b72..bf575ed7a999 100644 --- a/pkg/cmd/server/api/v1/swagger_doc.go +++ b/pkg/cmd/server/api/v1/swagger_doc.go @@ -75,8 +75,12 @@ func (AssetExtensionsConfig) SwaggerDoc() map[string]string { } var map_AuditConfig = map[string]string{ - "": "AuditConfig holds configuration for the audit capabilities", - "enabled": "If this flag is set, basic audit log will be printed in the logs. The logs contains, method, user and a requested URL.", + "": "AuditConfig holds configuration for the audit capabilities", + "enabled": "If this flag is set, audit log will be printed in the logs. The logs contains, method, user and a requested URL.", + "auditFilePath": "All requests coming to the apiserver will be logged to this file.", + "maximumFileRetentionDays": "Maximum number of days to retain old log files based on the timestamp encoded in their filename.", + "maximumRetainedFiles": "Maximum number of old log files to retain.", + "maximumFileSizeMegabytes": "Maximum size in megabytes of the log file before it gets rotated. Defaults to 100MB.", } func (AuditConfig) SwaggerDoc() map[string]string { diff --git a/pkg/cmd/server/api/v1/types.go b/pkg/cmd/server/api/v1/types.go index c83ad2839243..5781c53ca3de 100644 --- a/pkg/cmd/server/api/v1/types.go +++ b/pkg/cmd/server/api/v1/types.go @@ -248,9 +248,17 @@ type MasterConfig struct { // AuditConfig holds configuration for the audit capabilities type AuditConfig struct { - // If this flag is set, basic audit log will be printed in the logs. + // If this flag is set, audit log will be printed in the logs. // The logs contains, method, user and a requested URL. Enabled bool `json:"enabled"` + // All requests coming to the apiserver will be logged to this file. + AuditFilePath string `json:"auditFilePath"` + // Maximum number of days to retain old log files based on the timestamp encoded in their filename. + MaximumFileRetentionDays int `json:"maximumFileRetentionDays"` + // Maximum number of old log files to retain. + MaximumRetainedFiles int `json:"maximumRetainedFiles"` + // Maximum size in megabytes of the log file before it gets rotated. Defaults to 100MB. + MaximumFileSizeMegabytes int `json:"maximumFileSizeMegabytes"` } // JenkinsPipelineConfig holds configuration for the Jenkins pipeline strategy diff --git a/pkg/cmd/server/api/v1/types_test.go b/pkg/cmd/server/api/v1/types_test.go index 04fc82fb802c..4399589da763 100644 --- a/pkg/cmd/server/api/v1/types_test.go +++ b/pkg/cmd/server/api/v1/types_test.go @@ -105,7 +105,11 @@ assetConfig: namedCertificates: null requestTimeoutSeconds: 0 auditConfig: + auditFilePath: "" enabled: false + maximumFileRetentionDays: 0 + maximumFileSizeMegabytes: 0 + maximumRetainedFiles: 0 controllerConfig: serviceServingCert: signer: null diff --git a/pkg/cmd/server/api/validation/master.go b/pkg/cmd/server/api/validation/master.go index e9bfe7b926ac..29669dc5d735 100644 --- a/pkg/cmd/server/api/validation/master.go +++ b/pkg/cmd/server/api/validation/master.go @@ -191,6 +191,28 @@ func ValidateMasterConfig(config *api.MasterConfig, fldPath *field.Path) Validat validationResults.Append(ValidateControllerConfig(config.ControllerConfig, fldPath.Child("controllerConfig"))) + validationResults.Append(ValidateAuditConfig(config.AuditConfig, fldPath.Child("auditConfig"))) + + return validationResults +} + +func ValidateAuditConfig(config api.AuditConfig, fldPath *field.Path) ValidationResults { + validationResults := ValidationResults{} + + if len(config.AuditFilePath) == 0 { + // for backwards compatibility reasons we can't error this out + validationResults.AddWarnings(field.Required(fldPath.Child("auditFilePath"), "audit can now be logged to a separate file")) + } + if config.MaximumFileRetentionDays < 0 { + validationResults.AddErrors(field.Invalid(fldPath.Child("maximumFileRetentionDays"), config.MaximumFileRetentionDays, "must be greater than or equal to 0")) + } + if config.MaximumRetainedFiles < 0 { + validationResults.AddErrors(field.Invalid(fldPath.Child("maximumRetainedFiles"), config.MaximumRetainedFiles, "must be greater than or equal to 0")) + } + if config.MaximumFileSizeMegabytes < 0 { + validationResults.AddErrors(field.Invalid(fldPath.Child("maximumFileSizeMegabytes"), config.MaximumFileSizeMegabytes, "must be greater than or equal to 0")) + } + return validationResults } diff --git a/pkg/cmd/server/api/validation/master_test.go b/pkg/cmd/server/api/validation/master_test.go index 85b4f0927d60..e008b5f4b48e 100644 --- a/pkg/cmd/server/api/validation/master_test.go +++ b/pkg/cmd/server/api/validation/master_test.go @@ -383,7 +383,7 @@ func TestValidateAdmissionPluginConfigConflicts(t *testing.T) { // these fields have warnings in the empty case defaultWarningFields := sets.NewString( "serviceAccountConfig.managedNames", "serviceAccountConfig.publicKeyFiles", "serviceAccountConfig.privateKeyFile", "serviceAccountConfig.masterCA", - "projectConfig.securityAllocator", "kubernetesMasterConfig.proxyClientInfo") + "projectConfig.securityAllocator", "kubernetesMasterConfig.proxyClientInfo", "auditConfig.auditFilePath") for _, tc := range testCases { results := ValidateMasterConfig(&tc.options, nil) diff --git a/pkg/cmd/server/origin/audit.go b/pkg/cmd/server/origin/audit.go deleted file mode 100644 index 05c07324e4b0..000000000000 --- a/pkg/cmd/server/origin/audit.go +++ /dev/null @@ -1,113 +0,0 @@ -package origin - -import ( - "bufio" - "fmt" - "net" - "net/http" - - "github.com/golang/glog" - "github.com/pborman/uuid" - - kapi "k8s.io/kubernetes/pkg/api" - utilnet "k8s.io/kubernetes/pkg/util/net" - - authenticationapi "github.com/openshift/origin/pkg/auth/api" -) - -// auditResponseWriter implements http.ResponseWriter interface. -type auditResponseWriter struct { - http.ResponseWriter - id string -} - -func (a *auditResponseWriter) WriteHeader(code int) { - glog.Infof("AUDIT: id=%q response=\"%d\"", a.id, code) - a.ResponseWriter.WriteHeader(code) -} - -var _ http.ResponseWriter = &auditResponseWriter{} - -// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and -// http.Hijacker which are needed to make certain http operation (eg. watch, rsh, etc) -// working. -type fancyResponseWriterDelegator struct { - *auditResponseWriter -} - -func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool { - return f.ResponseWriter.(http.CloseNotifier).CloseNotify() -} - -func (f *fancyResponseWriterDelegator) Flush() { - f.ResponseWriter.(http.Flusher).Flush() -} - -func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return f.ResponseWriter.(http.Hijacker).Hijack() -} - -var _ http.CloseNotifier = &fancyResponseWriterDelegator{} -var _ http.Flusher = &fancyResponseWriterDelegator{} -var _ http.Hijacker = &fancyResponseWriterDelegator{} - -// auditHandler is responsible for logging audit information for all the -// request coming to server. Each audit log contains two entries: -// 1. the request line containing: -// - unique id allowing to match the response line (see 2) -// - source ip of the request -// - HTTP method being invoked -// - original user invoking the operation -// - impersonated user for the operation -// - namespace of the request or -// - uri is the full URI as requested -// 2. the response line containing the unique id from 1 and response code -func (c *MasterConfig) auditHandler(handler http.Handler) http.Handler { - if !c.Options.AuditConfig.Enabled { - return handler - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, _ := c.RequestContextMapper.Get(req) - user, _ := kapi.UserFrom(ctx) - asuser := req.Header.Get(authenticationapi.ImpersonateUserHeader) - if len(asuser) == 0 { - asuser = "" - } - requestedGroups := req.Header[authenticationapi.ImpersonateGroupHeader] - asgroups := "" - if len(requestedGroups) == 0 { - asgroups = "" - first := true - for _, group := range requestedGroups { - if !first { - asgroups = asgroups + "," - } - asgroups = asgroups + fmt.Sprintf("%q", group) - first = false - } - } - namespace := kapi.NamespaceValue(ctx) - if len(namespace) == 0 { - namespace = "" - } - id := uuid.NewRandom().String() - - glog.Infof("AUDIT: id=%q ip=%q method=%q user=%q as=%q asgroups=%q namespace=%q uri=%q", - id, utilnet.GetClientIP(req), req.Method, user.GetName(), asuser, asgroups, namespace, req.URL) - handler.ServeHTTP(constructResponseWriter(w, id), req) - }) -} - -func constructResponseWriter(responseWriter http.ResponseWriter, id string) http.ResponseWriter { - delegate := &auditResponseWriter{ResponseWriter: responseWriter, id: id} - // check if the ResponseWriter we're wrapping is the fancy one we need - // or if the basic is sufficient - _, cn := responseWriter.(http.CloseNotifier) - _, fl := responseWriter.(http.Flusher) - _, hj := responseWriter.(http.Hijacker) - if cn && fl && hj { - return &fancyResponseWriterDelegator{delegate} - } - return delegate -} diff --git a/pkg/cmd/server/origin/audit_test.go b/pkg/cmd/server/origin/audit_test.go deleted file mode 100644 index de544c66c019..000000000000 --- a/pkg/cmd/server/origin/audit_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package origin - -import ( - "bufio" - "net" - "net/http" - "reflect" - "testing" -) - -type simpleResponseWriter struct { - http.ResponseWriter -} - -func (*simpleResponseWriter) WriteHeader(code int) {} - -type fancyResponseWriter struct { - simpleResponseWriter -} - -func (*fancyResponseWriter) CloseNotify() <-chan bool { return nil } - -func (*fancyResponseWriter) Flush() {} - -func (*fancyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } - -func TestConstructResponseWriter(t *testing.T) { - actual := constructResponseWriter(&simpleResponseWriter{}, "") - switch v := actual.(type) { - case *auditResponseWriter: - break - default: - t.Errorf("Expected auditResponseWriter, got %v", reflect.TypeOf(v)) - } - - actual = constructResponseWriter(&fancyResponseWriter{}, "") - switch v := actual.(type) { - case *fancyResponseWriterDelegator: - break - default: - t.Errorf("Expected fancyResponseWriterDelegator, got %v", reflect.TypeOf(v)) - } -} diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 5892e182cbaf..6d0c3dae78e3 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "net/http" "os" "regexp" @@ -16,6 +17,7 @@ import ( "github.com/go-openapi/spec" "github.com/golang/glog" "github.com/prometheus/client_golang/prometheus" + "gopkg.in/natefinch/lumberjack.v2" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" @@ -25,6 +27,7 @@ import ( "k8s.io/kubernetes/pkg/apimachinery/registered" v1beta1extensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/apiserver/audit" "k8s.io/kubernetes/pkg/client/restclient" kclient "k8s.io/kubernetes/pkg/client/unversioned" clientadapter "k8s.io/kubernetes/pkg/client/unversioned/adapters/internalclientset" @@ -180,7 +183,22 @@ func (c *MasterConfig) Run(protected []APIInstaller, unprotected []APIInstaller) handler = c.authorizationFilter(handler) handler = c.impersonationFilter(handler) // audit handler must comes before the impersonationFilter to read the original user - handler = c.auditHandler(handler) + if c.Options.AuditConfig.Enabled { + attributeGetter := apiserver.NewRequestAttributeGetter(c.getRequestContextMapper(), c.getRequestInfoResolver()) + var writer io.Writer + if len(c.Options.AuditConfig.AuditFilePath) > 0 { + writer = &lumberjack.Logger{ + Filename: c.Options.AuditConfig.AuditFilePath, + MaxAge: c.Options.AuditConfig.MaximumFileRetentionDays, + MaxBackups: c.Options.AuditConfig.MaximumRetainedFiles, + MaxSize: c.Options.AuditConfig.MaximumFileSizeMegabytes, + } + } else { + // backwards compatible writer to regular log + writer = cmdutil.NewGLogWriterV(0) + } + handler = audit.WithAudit(handler, attributeGetter, writer) + } handler = authenticationHandlerFilter(handler, c.Authenticator, c.getRequestContextMapper()) handler = namespacingFilter(handler, c.getRequestContextMapper()) handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached @@ -876,6 +894,23 @@ func (c *MasterConfig) getRequestContextMapper() kapi.RequestContextMapper { return c.RequestContextMapper } +// getRequestInfoResolver returns a request resolver. +func (c *MasterConfig) getRequestInfoResolver() *apiserver.RequestInfoResolver { + if c.RequestInfoResolver == nil { + c.RequestInfoResolver = &apiserver.RequestInfoResolver{ + APIPrefixes: sets.NewString(strings.Trim(LegacyOpenShiftAPIPrefix, "/"), + strings.Trim(OpenShiftAPIPrefix, "/"), + strings.Trim(KubernetesAPIPrefix, "/"), + strings.Trim(KubernetesAPIGroupPrefix, "/")), // all possible API prefixes + GrouplessAPIPrefixes: sets.NewString(strings.Trim(LegacyOpenShiftAPIPrefix, "/"), + strings.Trim(OpenShiftAPIPrefix, "/"), + strings.Trim(KubernetesAPIPrefix, "/"), + ), // APIPrefixes that won't have groups (legacy) + } + } + return c.RequestInfoResolver +} + // RouteAllocator returns a route allocation controller. func (c *MasterConfig) RouteAllocator() *routeallocationcontroller.RouteAllocationController { osclient, kclient := c.RouteAllocatorClients() diff --git a/pkg/cmd/server/origin/master_config.go b/pkg/cmd/server/origin/master_config.go index 78b250f2ce15..5ff5caf3e30a 100644 --- a/pkg/cmd/server/origin/master_config.go +++ b/pkg/cmd/server/origin/master_config.go @@ -111,6 +111,8 @@ type MasterConfig struct { // RequestContextMapper maps requests to contexts RequestContextMapper kapi.RequestContextMapper + // RequestInfoResolver is responsible for reading request attributes + RequestInfoResolver *apiserver.RequestInfoResolver AdmissionControl admission.Interface diff --git a/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit.go b/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit.go index b2859014bc8f..54de1ea7ae6c 100644 --- a/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit.go +++ b/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit.go @@ -22,10 +22,12 @@ import ( "io" "net" "net/http" + "strings" "time" "github.com/pborman/uuid" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" "k8s.io/kubernetes/pkg/apiserver" utilnet "k8s.io/kubernetes/pkg/util/net" ) @@ -82,18 +84,27 @@ var _ http.Hijacker = &fancyResponseWriterDelegator{} func WithAudit(handler http.Handler, attributeGetter apiserver.RequestAttributeGetter, out io.Writer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { attribs := attributeGetter.GetAttribs(req) - asuser := req.Header.Get("Impersonate-User") + asuser := req.Header.Get(authenticationapi.ImpersonateUserHeader) if len(asuser) == 0 { asuser = "" } + asgroups := "" + requestedGroups := req.Header[authenticationapi.ImpersonateGroupHeader] + if len(requestedGroups) > 0 { + quotedGroups := make([]string, len(requestedGroups)) + for i, group := range requestedGroups { + quotedGroups[i] = fmt.Sprintf("%q", group) + } + asgroups = strings.Join(quotedGroups, ", ") + } namespace := attribs.GetNamespace() if len(namespace) == 0 { namespace = "" } id := uuid.NewRandom().String() - fmt.Fprintf(out, "%s AUDIT: id=%q ip=%q method=%q user=%q as=%q namespace=%q uri=%q\n", - time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(req), req.Method, attribs.GetUser().GetName(), asuser, namespace, req.URL) + fmt.Fprintf(out, "%s AUDIT: id=%q ip=%q method=%q user=%q as=%q asgroups=%q namespace=%q uri=%q\n", + time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(req), req.Method, attribs.GetUser().GetName(), asuser, asgroups, namespace, req.URL) respWriter := decorateResponseWriter(w, out, id) handler.ServeHTTP(respWriter, req) }) diff --git a/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit_test.go b/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit_test.go index 3131f979d412..7fea8963b957 100644 --- a/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit_test.go +++ b/vendor/k8s.io/kubernetes/pkg/apiserver/audit/audit_test.go @@ -95,7 +95,7 @@ func TestAudit(t *testing.T) { if len(line) != 2 { t.Fatalf("Unexpected amount of lines in audit log: %d", len(line)) } - match, err := regexp.MatchString(`[\d\:\-\.\+]+ AUDIT: id="[\w-]+" ip="127.0.0.1" method="GET" user="admin" as="" namespace="default" uri="/api/v1/namespaces/default/pods"`, line[0]) + match, err := regexp.MatchString(`[\d\:\-\.\+]+ AUDIT: id="[\w-]+" ip="127.0.0.1" method="GET" user="admin" as="" asgroups="" namespace="default" uri="/api/v1/namespaces/default/pods"`, line[0]) if err != nil { t.Errorf("Unexpected error matching first line: %v", err) }