Skip to content

Commit 4e8e957

Browse files
weltekialexellis
authored andcommitted
Support function authentication with OpenFaaS IAM
Signed-off-by: Han Verstraete (OpenFaaS Ltd) <[email protected]>
1 parent b20362a commit 4e8e957

File tree

154 files changed

+19257
-145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

154 files changed

+19257
-145
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ The watchdog can be configured through environment variables. You must always sp
104104
| `write_debug` | Write all output, error messages, and additional information to the logs. Default is false |
105105
| `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 |
106106
| `max_inflight` | Limit the maximum number of requests in flight |
107+
| `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. |
108+
| `jwt_auth_debug` | Print out debug messages from the JWT authentication process (OpenFaaS for Enterprises only). |
109+
| `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). |
107110

108111
## Metrics
109112

go.mod

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ module github.com/openfaas/classic-watchdog
33
go 1.20
44

55
require (
6-
github.com/openfaas/faas-middleware v1.2.3
6+
github.com/openfaas/faas-middleware v1.2.4
77
github.com/prometheus/client_golang v1.17.0
88
)
99

1010
require (
1111
github.com/beorn7/perks v1.0.1 // indirect
1212
github.com/cespare/xxhash/v2 v2.2.0 // indirect
1313
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
1415
github.com/golang/protobuf v1.5.3 // indirect
1516
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
1617
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
1718
github.com/prometheus/common v0.44.0 // indirect
1819
github.com/prometheus/procfs v0.11.1 // indirect
20+
github.com/rakutentech/jwk-go v1.1.3 // indirect
21+
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
1922
golang.org/x/sys v0.11.0 // indirect
2023
google.golang.org/protobuf v1.31.0 // indirect
2124
)

go.sum

+30-126
Large diffs are not rendered by default.

handler.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ func makeHealthHandler() func(http.ResponseWriter, *http.Request) {
303303
}
304304
}
305305

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

320320
}
321321
})
322-
return limiter.NewConcurrencyLimiter(handler, config.maxInflight).ServeHTTP
322+
return limiter.NewConcurrencyLimiter(handler, config.maxInflight)
323323
}

main.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/openfaas/classic-watchdog/metrics"
2323
"github.com/openfaas/classic-watchdog/types"
24+
"github.com/openfaas/faas-middleware/auth"
2425
"github.com/prometheus/client_golang/prometheus/testutil"
2526
)
2627

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

90+
requestHandler := makeRequestHandler(&config)
91+
if config.jwtAuthentication {
92+
handler, err := makeJWTAuthHandler(config, requestHandler)
93+
if err != nil {
94+
log.Fatalf("Error creating JWTAuthMiddleware: %s", err.Error())
95+
}
96+
requestHandler = handler
97+
98+
}
99+
89100
http.HandleFunc("/_/health", makeHealthHandler())
90-
http.HandleFunc("/", metrics.InstrumentHandler(makeRequestHandler(&config), httpMetrics))
101+
http.HandleFunc("/", metrics.InstrumentHandler(requestHandler, httpMetrics))
91102

92103
metricsServer := metrics.MetricsServer{}
93104
metricsServer.Register(config.metricsPort)
@@ -179,3 +190,46 @@ func printVersion() {
179190

180191
log.Printf("Version: %v\tSHA: %v\n", BuildVersion(), sha)
181192
}
193+
194+
func makeJWTAuthHandler(c WatchdogConfig, next http.Handler) (http.Handler, error) {
195+
namespace, err := getFnNamespace()
196+
if err != nil {
197+
return nil, fmt.Errorf("failed to get function namespace: %w", err)
198+
}
199+
name, err := getFnName()
200+
if err != nil {
201+
return nil, fmt.Errorf("failed to get function name: %w", err)
202+
}
203+
204+
authOpts := auth.JWTAuthOptions{
205+
Name: name,
206+
Namespace: namespace,
207+
LocalAuthority: c.jwtAuthLocal,
208+
Debug: c.jwtAuthDebug,
209+
}
210+
211+
return auth.NewJWTAuthMiddleware(authOpts, next)
212+
}
213+
214+
func getFnName() (string, error) {
215+
name, ok := os.LookupEnv("OPENFAAS_NAME")
216+
if !ok || len(name) == 0 {
217+
return "", fmt.Errorf("env variable 'OPENFAAS_NAME' not set")
218+
}
219+
220+
return name, nil
221+
}
222+
223+
// getFnNamespace gets the namespace name from the env variable OPENFAAS_NAMESPACE
224+
// or reads it from the service account if the env variable is not present
225+
func getFnNamespace() (string, error) {
226+
if namespace, ok := os.LookupEnv("OPENFAAS_NAMESPACE"); ok {
227+
return namespace, nil
228+
}
229+
230+
nsVal, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
231+
if err != nil {
232+
return "", err
233+
}
234+
return string(nsVal), nil
235+
}

readconfig.go

+15
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ func (ReadConfig) Read(hasEnv HasEnv) WatchdogConfig {
9595
cfg.combineOutput = parseBoolValue(hasEnv.Getenv("combine_output"))
9696
}
9797

98+
cfg.jwtAuthentication = parseBoolValue(hasEnv.Getenv("jwt_auth"))
99+
cfg.jwtAuthDebug = parseBoolValue(hasEnv.Getenv("jwt_auth_debug"))
100+
cfg.jwtAuthLocal = parseBoolValue(hasEnv.Getenv("jwt_auth_local"))
101+
98102
cfg.metricsPort = 8081
99103
cfg.maxInflight = parseIntValue(hasEnv.Getenv("max_inflight"), 0)
100104

@@ -147,6 +151,17 @@ type WatchdogConfig struct {
147151
// metricsPort is the HTTP port to serve metrics on
148152
metricsPort int
149153

154+
// jwtAuthentication enables JWT authentication for the watchdog
155+
// using the OpenFaaS gateway as the issuer.
156+
jwtAuthentication bool
157+
158+
// jwtAuthDebug enables debug logging for the JWT authentication middleware.
159+
jwtAuthDebug bool
160+
161+
// jwtAuthLocal indicates wether the JWT authentication middleware should use a port-forwarded or
162+
// local gateway running at `http://127.0.0.1:8000` instead of attempting to reach it via an in-cluster service
163+
jwtAuthLocal bool
164+
150165
// maxInflight limits the number of simultaneous
151166
// requests that the watchdog allows concurrently.
152167
// Any request which exceeds this limit will

requesthandler_test.go

+14-14
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestHandler_TransferEncodingPassedToFunction(t *testing.T) {
4545
cgiHeaders: true,
4646
}
4747
handler := makeRequestHandler(&config)
48-
handler(rr, req)
48+
handler.ServeHTTP(rr, req)
4949

5050
required := http.StatusOK
5151
if status := rr.Code; status != required {
@@ -81,7 +81,7 @@ func TestHandler_HasCustomHeaderInFunction_WithCgi_Mode(t *testing.T) {
8181
cgiHeaders: true,
8282
}
8383
handler := makeRequestHandler(&config)
84-
handler(rr, req)
84+
handler.ServeHTTP(rr, req)
8585

8686
required := http.StatusOK
8787
if status := rr.Code; status != required {
@@ -123,7 +123,7 @@ func TestHandler_HasCustomHeaderInFunction_WithCgiMode_AndBody(t *testing.T) {
123123
cgiHeaders: true,
124124
}
125125
handler := makeRequestHandler(&config)
126-
handler(rr, req)
126+
handler.ServeHTTP(rr, req)
127127

128128
required := http.StatusOK
129129
if status := rr.Code; status != required {
@@ -164,7 +164,7 @@ func TestHandler_HasHostHeaderWhenSet(t *testing.T) {
164164
cgiHeaders: true,
165165
}
166166
handler := makeRequestHandler(&config)
167-
handler(rr, req)
167+
handler.ServeHTTP(rr, req)
168168

169169
required := http.StatusOK
170170
if status := rr.Code; status != required {
@@ -194,7 +194,7 @@ func TestHandler_HostHeader_Empty_WhenNotSet(t *testing.T) {
194194
cgiHeaders: true,
195195
}
196196
handler := makeRequestHandler(&config)
197-
handler(rr, req)
197+
handler.ServeHTTP(rr, req)
198198

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

231231
handler := makeRequestHandler(&config)
232-
handler(rr, req)
232+
handler.ServeHTTP(rr, req)
233233

234234
required := http.StatusInternalServerError
235235

@@ -270,7 +270,7 @@ func TestHandler_StderrWritesToResponse_CombinedOutput_True(t *testing.T) {
270270
}
271271

272272
handler := makeRequestHandler(&config)
273-
handler(rr, req)
273+
handler.ServeHTTP(rr, req)
274274

275275
required := http.StatusInternalServerError
276276

@@ -317,7 +317,7 @@ func TestHandler_DoesntHaveCustomHeaderInFunction_WithoutCgi_Mode(t *testing.T)
317317
cgiHeaders: false,
318318
}
319319
handler := makeRequestHandler(&config)
320-
handler(rr, req)
320+
handler.ServeHTTP(rr, req)
321321

322322
required := http.StatusOK
323323
if status := rr.Code; status != required {
@@ -351,7 +351,7 @@ func TestHandler_HasXDurationSecondsHeader(t *testing.T) {
351351
faasProcess: "cat",
352352
}
353353
handler := makeRequestHandler(&config)
354-
handler(rr, req)
354+
handler.ServeHTTP(rr, req)
355355

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

385385
handler := makeRequestHandler(&config)
386-
handler(rr, req)
386+
handler.ServeHTTP(rr, req)
387387

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

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

436436
config := WatchdogConfig{}
437437
handler := makeRequestHandler(&config)
438-
handler(rr, req)
438+
handler.ServeHTTP(rr, req)
439439

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

460460
handler := makeRequestHandler(&config)
461-
handler(rr, req)
461+
handler.ServeHTTP(rr, req)
462462

463463
required := http.StatusOK
464464
if status := rr.Code; status != required {
@@ -553,7 +553,7 @@ func TestHandler_HasFullPathAndQueryInFunction_WithCgi_Mode(t *testing.T) {
553553
cgiHeaders: true,
554554
}
555555
handler := makeRequestHandler(&config)
556-
handler(rr, req)
556+
handler.ServeHTTP(rr, req)
557557

558558
required := http.StatusOK
559559
if status := rr.Code; status != required {

vendor/github.com/golang-jwt/jwt/v5/.gitignore

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/golang-jwt/jwt/v5/LICENSE

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)