mirror of
https://github.com/fluxcd/flagger.git
synced 2026-04-15 06:57:34 +00:00
Only allow namespaces matching -namespace-regexp
This allows to forbid access from canaries in non-whitelisted namespaces. In a multi-tenant context, this can be combined with network policies to maintain isolation between namespaces. Signed-off-by: Cédric Connes <cedric.connes@gmail.com>
This commit is contained in:
+11
-1
@@ -19,6 +19,7 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/flagger/pkg/loadtester"
|
||||
@@ -32,6 +33,7 @@ var (
|
||||
logLevel string
|
||||
port string
|
||||
timeout time.Duration
|
||||
namespaceRegexp string
|
||||
zapReplaceGlobals bool
|
||||
zapEncoding string
|
||||
)
|
||||
@@ -40,6 +42,7 @@ func init() {
|
||||
flag.StringVar(&logLevel, "log-level", "debug", "Log level can be: debug, info, warning, error.")
|
||||
flag.StringVar(&port, "port", "9090", "Port to listen on.")
|
||||
flag.DurationVar(&timeout, "timeout", time.Hour, "Load test exec timeout.")
|
||||
flag.StringVar(&namespaceRegexp, "namespace-regexp", "", "Restrict access to canaries in matching namespaces.")
|
||||
flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.")
|
||||
flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.")
|
||||
}
|
||||
@@ -66,5 +69,12 @@ func main() {
|
||||
logger.Infof("Starting load tester v%s API on port %s", VERSION, port)
|
||||
|
||||
gateStorage := loadtester.NewGateStorage("in-memory")
|
||||
loadtester.ListenAndServe(port, time.Minute, logger, taskRunner, gateStorage, stopCh)
|
||||
|
||||
var namespaceRegexpCompiled *regexp.Regexp
|
||||
if namespaceRegexp != "" {
|
||||
namespaceRegexpCompiled = regexp.MustCompile(namespaceRegexp)
|
||||
}
|
||||
authorizer := loadtester.NewAuthorizer(namespaceRegexpCompiled)
|
||||
|
||||
loadtester.ListenAndServe(port, time.Minute, logger, taskRunner, gateStorage, authorizer, stopCh)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2020 The Flux 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 loadtester
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
|
||||
)
|
||||
|
||||
type Authorizer struct {
|
||||
namespaceRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewAuthorizer(namespaceRegexp *regexp.Regexp) *Authorizer {
|
||||
return &Authorizer{
|
||||
namespaceRegexp: namespaceRegexp,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authorizer) Authorize(payload *flaggerv1.CanaryWebhookPayload) bool {
|
||||
return a.namespaceRegexp == nil || a.namespaceRegexp.MatchString(payload.Namespace)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
// ListenAndServe starts a web server and waits for SIGTERM
|
||||
func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogger, taskRunner *TaskRunner, gate *GateStorage, stopCh <-chan struct{}) {
|
||||
func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogger, taskRunner *TaskRunner, gate *GateStorage, authorizer *Authorizer, stopCh <-chan struct{}) {
|
||||
mux := http.DefaultServeMux
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/healthz", HandleHealthz)
|
||||
@@ -60,6 +60,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)
|
||||
approved := gate.isOpen(canaryName)
|
||||
if approved {
|
||||
@@ -90,6 +96,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)
|
||||
gate.open(canaryName)
|
||||
|
||||
@@ -115,6 +127,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)
|
||||
gate.close(canaryName)
|
||||
|
||||
@@ -140,6 +158,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("rollback.%s.%s", canary.Name, canary.Namespace)
|
||||
approved := gate.isOpen(canaryName)
|
||||
if approved {
|
||||
@@ -169,6 +193,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("rollback.%s.%s", canary.Name, canary.Namespace)
|
||||
gate.open(canaryName)
|
||||
|
||||
@@ -193,6 +223,12 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(canary) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
canaryName := fmt.Sprintf("rollback.%s.%s", canary.Name, canary.Namespace)
|
||||
gate.close(canaryName)
|
||||
|
||||
@@ -201,7 +237,7 @@ func ListenAndServe(port string, timeout time.Duration, logger *zap.SugaredLogge
|
||||
logger.Infof("%s rollback closed", canaryName)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/", HandleNewTask(logger, taskRunner))
|
||||
mux.HandleFunc("/", HandleNewTask(logger, taskRunner, authorizer))
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
@@ -233,7 +269,7 @@ func HandleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// HandleNewTask handles task creation requests
|
||||
func HandleNewTask(logger *zap.SugaredLogger, taskRunner TaskRunnerInterface) func(w http.ResponseWriter, r *http.Request) {
|
||||
func HandleNewTask(logger *zap.SugaredLogger, taskRunner TaskRunnerInterface, authorizer *Authorizer) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -251,6 +287,12 @@ func HandleNewTask(logger *zap.SugaredLogger, taskRunner TaskRunnerInterface) fu
|
||||
return
|
||||
}
|
||||
|
||||
if !authorizer.Authorize(payload) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload.Metadata) > 0 {
|
||||
metadata := payload.Metadata
|
||||
var typ, ok = metadata["type"]
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestServer_HandleNewBashTaskCmdExitZero(t *testing.T) {
|
||||
"cmd": "echo some-output-not-to-be-returned",
|
||||
},
|
||||
})
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner)(resp, req)
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner, NewAuthorizer(nil))(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
assert.Empty(t, resp.Body.String())
|
||||
@@ -62,7 +62,7 @@ func TestServer_HandleNewBashTaskCmdExitZeroReturnCmdOutput(t *testing.T) {
|
||||
"returnCmdOutput": "true",
|
||||
},
|
||||
})
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner)(resp, req)
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner, NewAuthorizer(nil))(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
assert.Equal(t, "some-output-to-be-returned\n", resp.Body.String())
|
||||
@@ -78,7 +78,7 @@ func TestServer_HandleNewBashTaskCmdExitNonZero(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner)(resp, req)
|
||||
HandleNewTask(mocks.logger, mocks.taskRunner, NewAuthorizer(nil))(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, resp.Code)
|
||||
assert.Equal(t, "command false failed: : exit status 1", resp.Body.String())
|
||||
|
||||
Reference in New Issue
Block a user