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

Support function authentication with OpenFaaS IAM #15

Merged
merged 2 commits into from
Jun 17, 2024
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
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Version := $(shell git describe --tags --dirty)
GitCommit := $(shell git rev-parse HEAD)
LDFLAGS := "-s -w -X main.Version=$(Version) -X main.GitCommit=$(GitCommit)"


SERVER?=ghcr.io
OWNER?=openfaas
IMG_NAME?=classic-watchdog
TAG?=$(Version)

.PHONY: all
all: gofmt test dist hashgen

Expand All @@ -26,3 +32,15 @@ dist:
GOARCH=arm64 CGO_ENABLED=0 GOOS=linux go build -mod=vendor -a -ldflags $(LDFLAGS) -installsuffix cgo -o bin/fwatchdog-arm64
GOOS=windows CGO_ENABLED=0 go build -mod=vendor -a -ldflags $(LDFLAGS) -installsuffix cgo -o bin/fwatchdog.exe
GOOS=darwin CGO_ENABLED=0 go build -mod=vendor -a -ldflags $(LDFLAGS) -installsuffix cgo -o bin/fwatchdog-darwin

# Example:
# SERVER=docker.io OWNER=alexellis2 TAG=ready make publish
.PHONY: publish
publish:
@echo $(SERVER)/$(OWNER)/$(IMG_NAME):$(TAG) && \
docker buildx create --use --name=multiarch --node=multiarch && \
docker buildx build \
--platform linux/amd64,linux/arm/v7,linux/arm64 \
--push=true \
--tag $(SERVER)/$(OWNER)/$(IMG_NAME):$(TAG) \
.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ The watchdog can be configured through environment variables. You must always sp
| `write_debug` | Write all output, error messages, and additional information to the logs. Default is false |
| `combine_output` | True by default - combines stdout/stderr in function response, when set to false `stderr` is written to the container logs and stdout is used for function response |
| `max_inflight` | Limit the maximum number of requests in flight |
| `jwt_auth` | For OpenFaaS for Enterprises customers only. When set to `true`, the watchdog will require a JWT token to be passed as a Bearer token in the Authorization header. This token can only be obtained through the OpenFaaS gateway using a token exchange using the `http://gateway.openfaas:8080` address as the authority. |
| `jwt_auth_debug` | Print out debug messages from the JWT authentication process (OpenFaaS for Enterprises only). |
| `jwt_auth_local` | When set to `true`, the watchdog will attempt to validate the JWT token using a port-forwarded or local gateway running at `http://127.0.0.1:8080` instead of attempting to reach it via an in-cluster service name (OpenFaaS for Enterprises only). |

## Metrics

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ module github.com/openfaas/classic-watchdog
go 1.20

require (
github.com/openfaas/faas-middleware v1.2.3
github.com/openfaas/faas-middleware v1.2.4
github.com/prometheus/client_golang v1.17.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rakutentech/jwk-go v1.1.3 // indirect
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
golang.org/x/sys v0.11.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
156 changes: 30 additions & 126 deletions go.sum

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func makeHealthHandler() func(http.ResponseWriter, *http.Request) {
}
}

func makeRequestHandler(config *WatchdogConfig) http.HandlerFunc {
func makeRequestHandler(config *WatchdogConfig) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case
Expand All @@ -319,5 +319,5 @@ func makeRequestHandler(config *WatchdogConfig) http.HandlerFunc {

}
})
return limiter.NewConcurrencyLimiter(handler, config.maxInflight).ServeHTTP
return limiter.NewConcurrencyLimiter(handler, config.maxInflight)
}
56 changes: 55 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/openfaas/classic-watchdog/metrics"
"github.com/openfaas/classic-watchdog/types"
"github.com/openfaas/faas-middleware/auth"
"github.com/prometheus/client_golang/prometheus/testutil"
)

Expand Down Expand Up @@ -86,8 +87,18 @@ func main() {
healthcheckInterval)
log.Printf("Listening on port: %d\n", config.port)

requestHandler := makeRequestHandler(&config)
if config.jwtAuthentication {
handler, err := makeJWTAuthHandler(config, requestHandler)
if err != nil {
log.Fatalf("Error creating JWTAuthMiddleware: %s", err.Error())
}
requestHandler = handler

}

http.HandleFunc("/_/health", makeHealthHandler())
http.HandleFunc("/", metrics.InstrumentHandler(makeRequestHandler(&config), httpMetrics))
http.HandleFunc("/", metrics.InstrumentHandler(requestHandler, httpMetrics))

metricsServer := metrics.MetricsServer{}
metricsServer.Register(config.metricsPort)
Expand Down Expand Up @@ -179,3 +190,46 @@ func printVersion() {

log.Printf("Version: %v\tSHA: %v\n", BuildVersion(), sha)
}

func makeJWTAuthHandler(c WatchdogConfig, next http.Handler) (http.Handler, error) {
namespace, err := getFnNamespace()
if err != nil {
return nil, fmt.Errorf("failed to get function namespace: %w", err)
}
name, err := getFnName()
if err != nil {
return nil, fmt.Errorf("failed to get function name: %w", err)
}

authOpts := auth.JWTAuthOptions{
Name: name,
Namespace: namespace,
LocalAuthority: c.jwtAuthLocal,
Debug: c.jwtAuthDebug,
}

return auth.NewJWTAuthMiddleware(authOpts, next)
}

func getFnName() (string, error) {
name, ok := os.LookupEnv("OPENFAAS_NAME")
if !ok || len(name) == 0 {
return "", fmt.Errorf("env variable 'OPENFAAS_NAME' not set")
}

return name, nil
}

// getFnNamespace gets the namespace name from the env variable OPENFAAS_NAMESPACE
// or reads it from the service account if the env variable is not present
func getFnNamespace() (string, error) {
if namespace, ok := os.LookupEnv("OPENFAAS_NAMESPACE"); ok {
return namespace, nil
}

nsVal, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err != nil {
return "", err
}
return string(nsVal), nil
}
15 changes: 15 additions & 0 deletions readconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func (ReadConfig) Read(hasEnv HasEnv) WatchdogConfig {
cfg.combineOutput = parseBoolValue(hasEnv.Getenv("combine_output"))
}

cfg.jwtAuthentication = parseBoolValue(hasEnv.Getenv("jwt_auth"))
cfg.jwtAuthDebug = parseBoolValue(hasEnv.Getenv("jwt_auth_debug"))
cfg.jwtAuthLocal = parseBoolValue(hasEnv.Getenv("jwt_auth_local"))

cfg.metricsPort = 8081
cfg.maxInflight = parseIntValue(hasEnv.Getenv("max_inflight"), 0)

Expand Down Expand Up @@ -147,6 +151,17 @@ type WatchdogConfig struct {
// metricsPort is the HTTP port to serve metrics on
metricsPort int

// jwtAuthentication enables JWT authentication for the watchdog
// using the OpenFaaS gateway as the issuer.
jwtAuthentication bool

// jwtAuthDebug enables debug logging for the JWT authentication middleware.
jwtAuthDebug bool

// jwtAuthLocal indicates wether the JWT authentication middleware should use a port-forwarded or
// local gateway running at `http://127.0.0.1:8000` instead of attempting to reach it via an in-cluster service
jwtAuthLocal bool

// maxInflight limits the number of simultaneous
// requests that the watchdog allows concurrently.
// Any request which exceeds this limit will
Expand Down
28 changes: 14 additions & 14 deletions requesthandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestHandler_TransferEncodingPassedToFunction(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -81,7 +81,7 @@ func TestHandler_HasCustomHeaderInFunction_WithCgi_Mode(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -123,7 +123,7 @@ func TestHandler_HasCustomHeaderInFunction_WithCgiMode_AndBody(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -164,7 +164,7 @@ func TestHandler_HasHostHeaderWhenSet(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -194,7 +194,7 @@ func TestHandler_HostHeader_Empty_WhenNotSet(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -229,7 +229,7 @@ func TestHandler_StderrWritesToStderr_CombinedOutput_False(t *testing.T) {
}

handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusInternalServerError

Expand Down Expand Up @@ -270,7 +270,7 @@ func TestHandler_StderrWritesToResponse_CombinedOutput_True(t *testing.T) {
}

handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusInternalServerError

Expand Down Expand Up @@ -317,7 +317,7 @@ func TestHandler_DoesntHaveCustomHeaderInFunction_WithoutCgi_Mode(t *testing.T)
cgiHeaders: false,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -351,7 +351,7 @@ func TestHandler_HasXDurationSecondsHeader(t *testing.T) {
faasProcess: "cat",
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -383,7 +383,7 @@ func TestHandler_RequestTimeoutFailsForExceededDuration(t *testing.T) {
}

handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusRequestTimeout
if status := rr.Code; status != required {
Expand All @@ -409,7 +409,7 @@ func TestHandler_StatusOKAllowed_ForWriteableVerbs(t *testing.T) {
faasProcess: "cat",
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand All @@ -435,7 +435,7 @@ func TestHandler_StatusMethodNotAllowed_ForUnknown(t *testing.T) {

config := WatchdogConfig{}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusMethodNotAllowed
if status := rr.Code; status != required {
Expand All @@ -458,7 +458,7 @@ func TestHandler_StatusOKForGETAndNoBody(t *testing.T) {
}

handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down Expand Up @@ -553,7 +553,7 @@ func TestHandler_HasFullPathAndQueryInFunction_WithCgi_Mode(t *testing.T) {
cgiHeaders: true,
}
handler := makeRequestHandler(&config)
handler(rr, req)
handler.ServeHTTP(rr, req)

required := http.StatusOK
if status := rr.Code; status != required {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading