Skip to content

Commit

Permalink
basic auth for APIs and metrics
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>
  • Loading branch information
fjogeleit committed Sep 8, 2023
1 parent b9ad9d9 commit 8675c6e
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.6.0

* Support BasicAuth for REST APIs and metrics

## 1.5.1

* Add zap.Logger
Expand Down
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) {
if err := v.BindEnv("leaderElection.podName", "POD_NAME"); err != nil {
zap.L().Warn("failed to bind env POD_NAME")
}
if err := v.BindEnv("leaderElection.namespace", "POD_NAMESPACE"); err != nil {

if err := v.BindEnv("namespace", "POD_NAMESPACE"); err != nil {
zap.L().Warn("failed to bind env POD_NAMESPACE")
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func newRunCMD() *cobra.Command {
return err
}

server := resolver.APIServer(policyClient.HasSynced)
server := resolver.APIServer(cmd.Context(), policyClient.HasSynced)

if c.REST.Enabled || c.BlockReports.Enabled {
resolver.RegisterStoreListener()
Expand Down
41 changes: 41 additions & 0 deletions pkg/api/basic_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api

import (
"crypto/sha256"
"crypto/subtle"
"net/http"
)

type BasicAuth struct {
Username string
Password string
}

func HTTPBasic(auth *BasicAuth, next http.HandlerFunc) http.HandlerFunc {
if auth == nil {
return next
}

expectedUsernameHash := sha256.Sum256([]byte(auth.Username))
expectedPasswordHash := sha256.Sum256([]byte(auth.Password))

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok {

usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))

usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)

if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
}
}

w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
31 changes: 25 additions & 6 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,40 @@ type httpServer struct {
reports reporting.PolicyReportGenerator
http http.Server
synced func() bool
auth *BasicAuth
}

func (s *httpServer) registerHandler() {
s.mux.HandleFunc("/healthz", HealthzHandler(s.synced))
s.mux.HandleFunc("/ready", ReadyHandler())
}

func (s *httpServer) middleware(handler http.HandlerFunc) http.HandlerFunc {
handler = Gzip(handler)

if s.auth != nil {
handler = HTTPBasic(s.auth, handler)
}

return handler
}

func (s *httpServer) RegisterMetrics() {
s.mux.Handle("/metrics", promhttp.Handler())
handler := promhttp.Handler()

if s.auth != nil {
s.mux.HandleFunc("/metrics", HTTPBasic(s.auth, handler.ServeHTTP))
return
}

s.mux.Handle("/metrics", handler)
}

func (s *httpServer) RegisterREST() {
s.mux.HandleFunc("/policies", Gzip(PolicyHandler(s.store)))
s.mux.HandleFunc("/verify-image-rules", Gzip(VerifyImageRulesHandler(s.store)))
s.mux.HandleFunc("/namespace-details-reporting", Gzip(NamespaceReportingHandler(s.reports, path.Join("templates", "reporting"))))
s.mux.HandleFunc("/policy-details-reporting", Gzip(PolicyReportingHandler(s.reports, path.Join("templates", "reporting"))))
s.mux.HandleFunc("/policies", s.middleware(PolicyHandler(s.store)))
s.mux.HandleFunc("/verify-image-rules", s.middleware(VerifyImageRulesHandler(s.store)))
s.mux.HandleFunc("/namespace-details-reporting", s.middleware(NamespaceReportingHandler(s.reports, path.Join("templates", "reporting"))))
s.mux.HandleFunc("/policy-details-reporting", s.middleware(PolicyReportingHandler(s.reports, path.Join("templates", "reporting"))))
}

func (s *httpServer) Start() error {
Expand All @@ -59,14 +77,15 @@ func (s *httpServer) Shutdown(ctx context.Context) error {
}

// NewServer constructor for a new API Server
func NewServer(pStore *kyverno.PolicyStore, reports reporting.PolicyReportGenerator, port int, synced func() bool, logger *zap.Logger) Server {
func NewServer(pStore *kyverno.PolicyStore, reports reporting.PolicyReportGenerator, port int, synced func() bool, auth *BasicAuth, logger *zap.Logger) Server {
mux := http.NewServeMux()

s := &httpServer{
store: pStore,
reports: reports,
mux: mux,
synced: synced,
auth: auth,
http: http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: NewLoggerMiddleware(logger, mux),
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const port int = 9999
var logger = zap.NewNop()

func Test_NewServer(t *testing.T) {
server := api.NewServer(kyverno.NewPolicyStore(), &policyReportGeneratorStub{}, port, func() bool { return true }, logger)
server := api.NewServer(kyverno.NewPolicyStore(), &policyReportGeneratorStub{}, port, func() bool { return true }, nil, logger)

server.RegisterMetrics()
server.RegisterREST()
Expand Down
14 changes: 11 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package config

// BasicAuth configuration
type BasicAuth struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
SecretRef string `mapstructure:"secretRef"`
}

// API configuration
type API struct {
Port int `mapstructure:"port"`
Logging bool `mapstructure:"logging"`
Port int `mapstructure:"port"`
Logging bool `mapstructure:"logging"`
BasicAuth BasicAuth `mapstructure:"basicAuth"`
}

type Logging struct {
Expand Down Expand Up @@ -32,7 +40,6 @@ type Results struct {
type LeaderElection struct {
LockName string `mapstructure:"lockName"`
PodName string `mapstructure:"podName"`
Namespace string `mapstructure:"namespace"`
LeaseDuration int `mapstructure:"leaseDuration"`
RenewDeadline int `mapstructure:"renewDeadline"`
RetryPeriod int `mapstructure:"retryPeriod"`
Expand All @@ -57,4 +64,5 @@ type Config struct {
BlockReports BlockReports `mapstructure:"blockReports"`
LeaderElection LeaderElection `mapstructure:"leaderElection"`
Logging Logging `mapstructure:"logging"`
Namespace string `mapstructure:"namespace"`
}
51 changes: 49 additions & 2 deletions pkg/config/resolver.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"context"
"time"

"go.uber.org/zap"
Expand All @@ -22,6 +23,7 @@ import (
prk8s "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/policyreport/kubernetes"
"github.com/kyverno/policy-reporter-kyverno-plugin/pkg/reporting"
rk8s "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/reporting/kubernetes"
"github.com/kyverno/policy-reporter-kyverno-plugin/pkg/secrets"
"github.com/kyverno/policy-reporter-kyverno-plugin/pkg/violation"
vk8s "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/violation/kubernetes"
)
Expand All @@ -42,18 +44,45 @@ type Resolver struct {
logger *zap.Logger
}

// SecretClient resolver method
func (r *Resolver) SecretClient() (secrets.Client, error) {
clientset, err := r.Clientset()
if err != nil {
zap.L().Error("failed to create secret client, secretRefs can not be resolved", zap.Error(err))
return nil, err
}

return secrets.NewClient(clientset.CoreV1().Secrets(r.config.Namespace)), nil
}

// APIServer resolver method
func (r *Resolver) APIServer(synced func() bool) api.Server {
func (r *Resolver) APIServer(ctx context.Context, synced func() bool) api.Server {
var logger *zap.Logger
if r.config.API.Logging {
logger, _ = r.Logger()
}

authConfig := &r.config.API.BasicAuth
if authConfig.SecretRef != "" {
r.loadSecretRef(ctx, authConfig)
}

var auth *api.BasicAuth
if authConfig.Username != "" && authConfig.Password != "" {
auth = &api.BasicAuth{
Username: authConfig.Username,
Password: authConfig.Password,
}

zap.L().Info("API BasicAuth enabled")
}

return api.NewServer(
r.PolicyStore(),
r.Reporting(),
r.config.API.Port,
synced,
auth,
logger,
)
}
Expand Down Expand Up @@ -185,7 +214,7 @@ func (r *Resolver) LeaderElectionClient() (*leaderelection.Client, error) {
r.leaderClient = leaderelection.New(
clientset.CoordinationV1(),
r.config.LeaderElection.LockName,
r.config.LeaderElection.Namespace,
r.config.Namespace,
r.config.LeaderElection.PodName,
time.Duration(r.config.LeaderElection.LeaseDuration)*time.Second,
time.Duration(r.config.LeaderElection.RenewDeadline)*time.Second,
Expand Down Expand Up @@ -305,6 +334,24 @@ func (r *Resolver) RegisterMetricsListener() {
r.EventPublisher().RegisterListener(listener.NewPolicyMetricsListener())
}

func (r *Resolver) loadSecretRef(ctx context.Context, auth *BasicAuth) {
client, err := r.SecretClient()
if err != nil {
return
}
values, err := client.Get(ctx, auth.SecretRef)
if err != nil {
zap.L().Error("failed to load basic auth secret", zap.Error(err))
}

if values.Username != "" {
auth.Username = values.Username
}
if values.Password != "" {
auth.Password = values.Password
}
}

// NewResolver constructor function
func NewResolver(config *Config, k8sConfig *rest.Config) Resolver {
return Resolver{
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/resolver_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config_test

import (
"context"
"testing"

"k8s.io/client-go/rest"
Expand Down Expand Up @@ -134,7 +135,7 @@ func Test_ResolvePolicyMapper(t *testing.T) {
func Test_ResolveAPIServer(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})

server := resolver.APIServer(func() bool { return true })
server := resolver.APIServer(context.Background(), func() bool { return true })
if server == nil {
t.Error("Error: Should return API Server")
}
Expand Down
72 changes: 72 additions & 0 deletions pkg/secrets/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package secrets

import (
"context"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/util/retry"
)

type Values struct {
Username string `json:"username" mapstructure:"username"`
Password string `json:"password" mapstructure:"password"`
}

type Client interface {
Get(context.Context, string) (Values, error)
}

type k8sClient struct {
client v1.SecretInterface
}

func (c *k8sClient) Get(ctx context.Context, name string) (Values, error) {
var secret *corev1.Secret

err := retry.OnError(retry.DefaultRetry, func(err error) bool {
if _, ok := err.(errors.APIStatus); !ok {
return true
}

if ok := errors.IsTimeout(err); ok {
return true
}

if ok := errors.IsServerTimeout(err); ok {
return true
}

if ok := errors.IsServiceUnavailable(err); ok {
return true
}

return false
}, func() error {
var err error
secret, err = c.client.Get(ctx, name, metav1.GetOptions{})

return err
})

values := Values{}
if err != nil {
return values, err
}

if username, ok := secret.Data["username"]; ok {
values.Username = string(username)
}

if password, ok := secret.Data["password"]; ok {
values.Password = string(password)
}

return values, nil
}

func NewClient(secretClient v1.SecretInterface) Client {
return &k8sClient{secretClient}
}
Loading

0 comments on commit 8675c6e

Please sign in to comment.