Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic auth for APIs and metrics #27

Merged
merged 1 commit into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading