Basic audit log

pull/6/head
Maciej Szulik 2016-06-08 23:56:12 +02:00
parent ff3bd05efb
commit 24f1e1eaf6
6 changed files with 217 additions and 6 deletions

View File

@ -64,6 +64,7 @@ pkg/apis/autoscaling/install
pkg/apis/batch/install pkg/apis/batch/install
pkg/apis/certificates/install pkg/apis/certificates/install
pkg/apis/componentconfig/install pkg/apis/componentconfig/install
pkg/apiserver/audit
pkg/api/service pkg/api/service
pkg/apis/extensions/install pkg/apis/extensions/install
pkg/apis/extensions/v1beta1 pkg/apis/extensions/v1beta1

View File

@ -16,6 +16,10 @@ api-servers
api-token api-token
api-version api-version
apiserver-count apiserver-count
audit-log-maxage
audit-log-maxbackup
audit-log-maxsize
audit-log-path
auth-path auth-path
auth-provider auth-provider
auth-provider-arg auth-provider-arg

View File

@ -0,0 +1,114 @@
/*
Copyright 2016 The Kubernetes Authors.
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 audit
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/pborman/uuid"
"k8s.io/kubernetes/pkg/api"
utilnet "k8s.io/kubernetes/pkg/util/net"
)
var _ http.ResponseWriter = &auditResponseWriter{}
type auditResponseWriter struct {
http.ResponseWriter
out io.Writer
id string
}
func (a *auditResponseWriter) WriteHeader(code int) {
fmt.Fprintf(a.out, "%s AUDIT: id=%q response=\"%d\"\n", time.Now().Format(time.RFC3339Nano), a.id, code)
a.ResponseWriter.WriteHeader(code)
}
// 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{}
// WithAudit decorates a http.Handler with audit logging information for all the
// requests coming to the 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 <none>
// - uri is the full URI as requested
// 2. the response line containing:
// - the unique id from 1
// - response code
func WithAudit(handler http.Handler, requestContextMapper api.RequestContextMapper, out io.Writer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx, _ := requestContextMapper.Get(req)
user, _ := api.UserFrom(ctx)
asuser := req.Header.Get("Impersonate-User")
if len(asuser) == 0 {
asuser = "<self>"
}
namespace := api.NamespaceValue(ctx)
if len(namespace) == 0 {
namespace = "<none>"
}
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, user.GetName(), asuser, namespace, req.URL)
respWriter := decorateResponseWriter(w, out, id)
handler.ServeHTTP(respWriter, req)
})
}
func decorateResponseWriter(responseWriter http.ResponseWriter, out io.Writer, id string) http.ResponseWriter {
delegate := &auditResponseWriter{ResponseWriter: responseWriter, out: out, 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
}

View File

@ -0,0 +1,58 @@
/*
Copyright 2016 The Kubernetes Authors.
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 audit
import (
"bufio"
"io/ioutil"
"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 := decorateResponseWriter(&simpleResponseWriter{}, ioutil.Discard, "")
switch v := actual.(type) {
case *auditResponseWriter:
default:
t.Errorf("Expected auditResponseWriter, got %v", reflect.TypeOf(v))
}
actual = decorateResponseWriter(&fancyResponseWriter{}, ioutil.Discard, "")
switch v := actual.(type) {
case *fancyResponseWriterDelegator:
default:
t.Errorf("Expected fancyResponseWriterDelegator, got %v", reflect.TypeOf(v))
}
}

View File

@ -30,6 +30,12 @@ import (
"strings" "strings"
"time" "time"
systemd "github.com/coreos/go-systemd/daemon"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
"gopkg.in/natefinch/lumberjack.v2"
"k8s.io/kubernetes/pkg/admission" "k8s.io/kubernetes/pkg/admission"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/rest" "k8s.io/kubernetes/pkg/api/rest"
@ -37,6 +43,7 @@ import (
"k8s.io/kubernetes/pkg/apimachinery" "k8s.io/kubernetes/pkg/apimachinery"
"k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apiserver" "k8s.io/kubernetes/pkg/apiserver"
"k8s.io/kubernetes/pkg/apiserver/audit"
"k8s.io/kubernetes/pkg/auth/authenticator" "k8s.io/kubernetes/pkg/auth/authenticator"
"k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/handlers" "k8s.io/kubernetes/pkg/auth/handlers"
@ -54,11 +61,6 @@ import (
utilnet "k8s.io/kubernetes/pkg/util/net" utilnet "k8s.io/kubernetes/pkg/util/net"
utilruntime "k8s.io/kubernetes/pkg/util/runtime" utilruntime "k8s.io/kubernetes/pkg/util/runtime"
"k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/sets"
systemd "github.com/coreos/go-systemd/daemon"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
) )
const globalTimeout = time.Minute const globalTimeout = time.Minute
@ -96,7 +98,11 @@ type APIGroupInfo struct {
// Config is a structure used to configure a GenericAPIServer. // Config is a structure used to configure a GenericAPIServer.
type Config struct { type Config struct {
// The storage factory for other objects // The storage factory for other objects
StorageFactory StorageFactory StorageFactory StorageFactory
AuditLogPath string
AuditLogMaxAge int
AuditLogMaxBackups int
AuditLogMaxSize int
// allow downstream consumers to disable the core controller loops // allow downstream consumers to disable the core controller loops
EnableLogsSupport bool EnableLogsSupport bool
EnableUISupport bool EnableUISupport bool
@ -475,6 +481,17 @@ func (s *GenericAPIServer) init(c *Config) {
attributeGetter := apiserver.NewRequestAttributeGetter(s.RequestContextMapper, s.NewRequestInfoResolver()) attributeGetter := apiserver.NewRequestAttributeGetter(s.RequestContextMapper, s.NewRequestInfoResolver())
handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, s.authorizer) handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, s.authorizer)
if len(c.AuditLogPath) != 0 {
// audit handler must comes before the impersonationFilter to read the original user
writer := &lumberjack.Logger{
Filename: c.AuditLogPath,
MaxAge: c.AuditLogMaxAge,
MaxBackups: c.AuditLogMaxBackups,
MaxSize: c.AuditLogMaxSize,
}
handler = audit.WithAudit(handler, s.RequestContextMapper, writer)
defer writer.Close()
}
handler = apiserver.WithImpersonation(handler, s.RequestContextMapper, s.authorizer) handler = apiserver.WithImpersonation(handler, s.RequestContextMapper, s.authorizer)
// Install Authenticator // Install Authenticator
@ -542,6 +559,10 @@ func NewConfig(options *options.ServerRunOptions) *Config {
APIGroupPrefix: options.APIGroupPrefix, APIGroupPrefix: options.APIGroupPrefix,
APIPrefix: options.APIPrefix, APIPrefix: options.APIPrefix,
CorsAllowedOriginList: options.CorsAllowedOriginList, CorsAllowedOriginList: options.CorsAllowedOriginList,
AuditLogPath: options.AuditLogPath,
AuditLogMaxAge: options.AuditLogMaxAge,
AuditLogMaxBackups: options.AuditLogMaxBackups,
AuditLogMaxSize: options.AuditLogMaxSize,
EnableIndex: true, EnableIndex: true,
EnableLogsSupport: options.EnableLogsSupport, EnableLogsSupport: options.EnableLogsSupport,
EnableProfiling: options.EnableProfiling, EnableProfiling: options.EnableProfiling,

View File

@ -67,6 +67,10 @@ type ServerRunOptions struct {
DeleteCollectionWorkers int DeleteCollectionWorkers int
// Used to specify the storage version that should be used for the legacy v1 api group. // Used to specify the storage version that should be used for the legacy v1 api group.
DeprecatedStorageVersion string DeprecatedStorageVersion string
AuditLogPath string
AuditLogMaxAge int
AuditLogMaxBackups int
AuditLogMaxSize int
EnableLogsSupport bool EnableLogsSupport bool
EnableProfiling bool EnableProfiling bool
EnableSwaggerUI bool EnableSwaggerUI bool
@ -294,6 +298,15 @@ func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
fs.IntVar(&s.DeleteCollectionWorkers, "delete-collection-workers", s.DeleteCollectionWorkers, fs.IntVar(&s.DeleteCollectionWorkers, "delete-collection-workers", s.DeleteCollectionWorkers,
"Number of workers spawned for DeleteCollection call. These are used to speed up namespace cleanup.") "Number of workers spawned for DeleteCollection call. These are used to speed up namespace cleanup.")
fs.StringVar(&s.AuditLogPath, "audit-log-path", s.AuditLogPath,
"If set, all requests coming to the apiserver will be logged to this file.")
fs.IntVar(&s.AuditLogMaxAge, "audit-log-maxage", s.AuditLogMaxBackups,
"The maximum number of days to retain old audit log files based on the timestamp encoded in their filename.")
fs.IntVar(&s.AuditLogMaxBackups, "audit-log-maxbackup", s.AuditLogMaxBackups,
"The maximum number of old audit log files to retain.")
fs.IntVar(&s.AuditLogMaxSize, "audit-log-maxsize", s.AuditLogMaxSize,
"The maximum size in megabytes of the audit log file before it gets rotated. Defaults to 100MB.")
fs.BoolVar(&s.EnableProfiling, "profiling", s.EnableProfiling, fs.BoolVar(&s.EnableProfiling, "profiling", s.EnableProfiling,
"Enable profiling via web interface host:port/debug/pprof/") "Enable profiling via web interface host:port/debug/pprof/")