github.com/verrazzano/verrazzano@v1.7.1/platform-operator/helm_config/charts/verrazzano-authproxy/templates/verrazzano-authproxy-configmap.yaml (about) 1 # Copyright (c) 2021, 2023, Oracle and/or its affiliates. 2 # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 --- 5 apiVersion: v1 6 kind: ConfigMap 7 metadata: 8 name: verrazzano-authproxy-config 9 namespace: {{ .Release.Namespace }} 10 labels: 11 app: {{ .Values.name }} 12 data: 13 conf.lua: | 14 local clusterHostSuffix = '{{ .Values.config.envName }}'..'.'..'{{ .Values.config.dnsSuffix }}' 15 {{- with .Values.proxy }} 16 local ingressHost = ngx.req.get_headers()["x-forwarded-host"] 17 if not ingressHost then 18 ingressHost = ngx.req.get_headers()["host"] 19 end 20 if not ingressHost or #ingressHost < 1 or #ingressHost > 256 then 21 ingressHost = 'invalid-hostname' 22 end 23 24 local ingressUri = 'https://'..ingressHost 25 local callbackPath = "{{ .OidcCallbackPath }}" 26 local logoutPath = "{{ .OidcLogoutCallbackPath }}" 27 local singleLogoutPath = "{{ .OidcSingleLogoutCallbackPath }}" 28 local oidcProviderForConsole = "{{ .OidcProviderForConsole }}" 29 30 local auth = require("auth").config({ 31 hostSuffix = '.'..clusterHostSuffix, 32 callbackUri = ingressUri..callbackPath, 33 singleLogoutUri = ingressUri..singleLogoutPath, 34 hostUri = ingressUri 35 }) 36 37 -- determine backend and set backend parameters 38 local backend, can_redirect = auth.getBackendNameFromIngressHost(ingressHost) 39 local backendUrl = auth.getBackendServerUrlFromName(backend) 40 41 -- CORS handling 42 local h, _ = ngx.req.get_headers()["origin"] 43 if h ~= nil and h ~= "" then 44 auth.debug("Origin header: "..h) 45 if h == "*" then 46 -- not a legit origin, could be intended to trick us into oversharing 47 auth.bad_request("Invalid Origin header: '*'") 48 end 49 50 ngx.header["Vary"] = "Origin" 51 local requestMethod = ngx.req.get_method() 52 local originAllowed = auth.isOriginAllowed(h, backend, ingressUri) 53 54 -- From https://tools.ietf.org/id/draft-abarth-origin-03.html#server-behavior, if the request Origin is not in 55 -- whitelisted origins and the request method is not a safe non state changing (i.e. GET or HEAD), we should 56 -- abort the request. 57 if not originAllowed and requestMethod ~= "GET" and requestMethod ~= "HEAD" and requestMethod ~= "OPTIONS" then 58 auth.forbidden("Origin: " .. h .. " not allowed.") 59 end 60 61 if originAllowed then 62 -- From "Simple Cross-Origin Request, Actual Request, and Redirects" section of https://www.w3.org/TR/2020/SPSD-cors-20200602, 63 -- set the value of Access-Control-Allow-Origin and Access-Control-Allow-Credentials hedaers if there is a match. 64 ngx.header["Access-Control-Allow-Origin"] = h 65 ngx.header["Access-Control-Allow-Credentials"] = "true" 66 if requestMethod == "OPTIONS" then 67 ngx.header["Access-Control-Allow-Headers"] = "authorization, content-type" 68 ngx.header["Access-Control-Allow-Methods"] = "GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH" 69 end 70 end 71 72 if requestMethod == "OPTIONS" then 73 ngx.header["Content-Length"] = 0 74 ngx.status = 200 75 ngx.exit(ngx.HTTP_OK) 76 end 77 78 else 79 if ngx.req.get_method() == "OPTIONS" then 80 auth.bad_request("OPTIONS request with no Origin header") 81 end 82 end 83 84 auth.debug("Processing request for backend '"..ingressHost.."'") 85 if (backend == 'console' or backend == 'verrazzano') and (not auth.isBodyValidJson()) then 86 auth.bad_request("Invalid request") 87 end 88 89 if (backend == 'console' or backend == 'verrazzano') then 90 local oidcProviderFromMCSecret = auth.read_file("/api-config/oidc-provider") 91 if oidcProviderFromMCSecret and oidcProviderFromMCSecret ~= "" then 92 auth.debug("oidc-provider specified in multi-cluster secret, will use " .. oidcProviderFromMCSecret .. " as OIDC Provider.") 93 auth.initOidcProvider(oidcProviderFromMCSecret) 94 else 95 auth.initOidcProvider(oidcProviderForConsole) 96 end 97 else 98 auth.initOidcProvider("keycloak") 99 end 100 101 local authHeader = ngx.req.get_headers()["authorization"] 102 local token = nil 103 if authHeader then 104 if auth.hasCredentialType(authHeader, 'Bearer') then 105 token = auth.handleBearerToken(authHeader) 106 elseif auth.hasCredentialType(authHeader, 'Basic') then 107 token = auth.handleBasicAuth(authHeader) 108 end 109 if not token then 110 auth.debug("No recognized credentials in authorization header") 111 end 112 else 113 auth.debug("No authorization header found") 114 if auth.requestUriMatches(ngx.var.request_uri, callbackPath) then 115 -- we initiated authentication via pkce, and OP is delivering the code 116 -- will redirect to target url, where token will be found in cookie 117 auth.oidcHandleCallback() 118 end 119 120 if auth.requestUriMatches(ngx.var.request_uri, logoutPath) then 121 -- logout was triggered 122 auth.logout() 123 end 124 125 if auth.requestUriMatches(ngx.var.request_uri, singleLogoutPath) then 126 -- single logout was triggered 127 auth.singleLogout() 128 end 129 130 -- no token yet, and the request is not progressing an OIDC flow. 131 -- check if caller has an existing session with a valid token. 132 token = auth.getTokenFromSession() 133 134 -- still no token? redirect to OP to authenticate user (if request is not a Verrazzano API call) 135 if not token and can_redirect == true then 136 auth.oidcAuthenticate() 137 end 138 end 139 140 if not token then 141 auth.unauthorized("Not authenticated") 142 end 143 144 -- token will be an id token except when console calls api proxy, then it's an access token 145 if not auth.isAuthorized(token) then 146 auth.forbidden("Not authorized") 147 end 148 149 local usernameFromToken = auth.usernameFromIdToken(token) 150 auth.audit("User "..usernameFromToken.." authenticated and authorized") 151 local userRolesFromToken = auth.getRoles(token) 152 153 if backend == 'verrazzano' then 154 local args = ngx.req.get_uri_args() 155 if args.cluster then 156 -- returns remote cluster server URL 157 backendUrl = auth.handleExternalAPICall(token) 158 else 159 auth.handleLocalAPICall(token) 160 end 161 else 162 if auth.hasCredentialType(authHeader, 'Bearer') then 163 -- clear the auth header if it's a bearer token 164 ngx.req.clear_header("Authorization") 165 end 166 -- set the oidc_user 167 ngx.var.oidc_user = usernameFromToken 168 ngx.var.oidc_user_roles = userRolesFromToken 169 auth.debug("Authorized: oidc_user is "..ngx.var.oidc_user) 170 end 171 172 auth.debug("Setting backend_server_url to '"..backendUrl.."'") 173 ngx.var.backend_server_url = backendUrl 174 auth.lua: | 175 local me = {} 176 local random = require("resty.random") 177 local base64 = require("ngx.base64") 178 local cjson = require "cjson" 179 local jwt = require "resty.jwt" 180 local validators = require "resty.jwt-validators" 181 local b64 = ngx.encode_base64 182 local unb64url = require("ngx.base64").decode_base64url 183 184 local oidcRealm = "{{ .OidcRealm }}" 185 local oidcClient = "{{ .OIDCClientID }}" 186 local adminClusterOidcClient = "{{ .PKCEClientID }}" 187 local oidcDirectAccessClient = "{{ .PGClientID }}" 188 local requiredRole = "{{ .RequiredRealmRole }}" 189 190 local authStateTtlInSec = tonumber("{{ .AuthnStateTTL }}") 191 local oidcProviderHost = "{{ .OidcProviderHost }}" 192 local oidcProviderHostInCluster = "{{ .OidcProviderHostInCluster }}" 193 local oidcProviderHostDex = "{{ .OidcProviderHostDex }}" 194 local oidcProviderHostInClusterDex = "{{ .OidcProviderHostInClusterDex }}" 195 local oidcClientSecret = "{{ .OidcProviderClientSecret }}" 196 197 198 local oidcProviderUri = nil 199 local oidcProviderInClusterUri = nil 200 local oidcIssuerUri = nil 201 local oidcIssuerUriLocal = nil 202 203 local opensearchProtocol = "{{ $.Values.config.opensearch.protocol }}" 204 local opensearchService = "{{ $.Values.config.opensearch.service }}" 205 local osdService = "{{ $.Values.config.opensearch.osdService }}" 206 local opensearchNamespace = "{{ $.Values.config.opensearch.namespace }}" 207 208 function me.getRoles(idToken) 209 local id_token = jwt:load_jwt(idToken) 210 if id_token and id_token.payload and id_token.payload.realm_access and id_token.payload.realm_access.roles then 211 return table.concat(id_token.payload.realm_access.roles, ",") 212 end 213 214 if me.oidcProvider == "dex" then 215 return "offline_access,vz_api_access,vz_opensearch_admin,uma_authorization,default-roles-verrazzano-system" 216 end 217 return "" 218 end 219 220 function me.config(opts) 221 for key, val in pairs(opts) do 222 me[key] = val 223 end 224 me.initCookieEncryptor() 225 return me 226 end 227 228 function me.initCookieEncryptor() 229 local aes = require "resty.aes" 230 local key = me.read_file("/api-config/cookie-encryption-key") 231 if not key or #key ~= 64 then 232 me.internal_server_error("Error getting cookie key") 233 end 234 local salt = string.sub(key, 49, 56) 235 local encryptor, err = aes:new(key, salt, aes.cipher(256, "cbc"), aes.hash.sha256, 3) 236 if err or not encryptor then 237 me.internal_server_error("Unable to get encryptor, error is: '"..err.."'") 238 end 239 me.aes256 = encryptor 240 end 241 242 function me.initOidcProvider(oidcProvider) 243 me.oidcProvider = oidcProvider 244 local oidcProviderHostForRequest = "" 245 local oidcProviderHostInClusterForRequest = "" 246 if me.oidcProvider == "dex" then 247 oidcProviderHostForRequest = oidcProviderHostDex 248 oidcProviderHostInClusterForRequest = oidcProviderHostInClusterDex 249 else 250 oidcProviderHostForRequest = oidcProviderHost 251 oidcProviderHostInClusterForRequest = oidcProviderHostInCluster 252 end 253 254 me.oidcProviderUri = "https://" .. oidcProviderHostForRequest 255 if oidcProviderHostInClusterForRequest and oidcProviderHostInClusterForRequest ~= "" then 256 me.oidcProviderInClusterUri = "http://" .. oidcProviderHostInClusterForRequest 257 end 258 259 local oidcProviderURL = me.read_file("/api-config/" .. me.oidcProvider .. "-url") 260 if oidcProviderURL and oidcProviderURL ~= "" then 261 me.debug(me.oidcProvider .. "-url specified in multi-cluster secret, will not use in-cluster oidc provider host.") 262 263 me.oidcProviderUri = oidcProviderURL 264 me.oidcProviderInClusterUri = nil 265 end 266 267 if me.oidcProvider == "keycloak" then 268 me.oidcProviderUri = me.oidcProviderUri .. "/auth/realms/" .. oidcRealm 269 if me.oidcProviderInClusterUri then 270 me.oidcProviderInClusterUri = me.oidcProviderInClusterUri .. "/auth/realms/" .. oidcRealm 271 end 272 273 end 274 275 me.oidcIssuerUri = me.oidcProviderUri 276 me.oidcIssuerUriLocal = me.oidcProviderInClusterUri 277 end 278 279 function me.log(logLevel, msg, name, value) 280 local logObj = {message = msg} 281 if name then 282 logObj[name] = value 283 end 284 ngx.log(logLevel, cjson.encode(logObj)) 285 end 286 287 function me.logJson(logLevel, msg, err) 288 if err then 289 me.log(logLevel, msg, 'error', err) 290 else 291 me.log(logLevel, msg) 292 end 293 end 294 295 function me.info(msg, obj) 296 if obj then 297 me.log(ngx.INFO, msg, 'object', obj) 298 else 299 me.log(ngx.INFO, msg) 300 end 301 end 302 303 function me.error(msg, obj) 304 if obj then 305 me.log(ngx.ERR, msg, 'object', obj) 306 else 307 me.log(ngx.ERR, msg) 308 end 309 end 310 311 function me.debug(msg, obj) 312 if obj then 313 me.log(ngx.DEBUG, msg, 'object', obj) 314 else 315 me.log(ngx.DEBUG, msg) 316 end 317 end 318 319 function me.queryParams(req_uri) 320 local i = req_uri:find("?") 321 if not i then 322 i = 0 323 else 324 i = i + 1 325 end 326 return ngx.decode_args(req_uri:sub(i), 0) 327 end 328 329 function me.query(req_uri, name) 330 local i = req_uri:find("&"..name.."=") 331 if not i then 332 i = req_uri:find("?"..name.."=") 333 end 334 if not i then 335 return nil 336 else 337 local begin = i+2+name:len() 338 local endin = req_uri:find("&", begin) 339 if not endin then 340 return req_uri:sub(begin) 341 end 342 return req_uri:sub(begin, endin-1) 343 end 344 end 345 346 -- For now, auditing reports a log message at the debug level 347 function me.audit(msg, err) 348 me.logJson(ngx.DEBUG, msg, err) 349 end 350 351 function me.internal_server_error(msg, err) 352 me.audit(msg, err) 353 ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR 354 ngx.say("500 Internal Server Error") 355 ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 356 end 357 358 function me.unauthorized(msg, err) 359 me.deleteCookies("vz_authn", "vz_userinfo", "vz_state") 360 me.audit(msg, err) 361 ngx.status = ngx.HTTP_UNAUTHORIZED 362 ngx.say("401 Unauthorized") 363 ngx.exit(ngx.HTTP_UNAUTHORIZED) 364 end 365 366 function me.forbidden(msg, err) 367 me.audit(msg, err) 368 ngx.status = ngx.HTTP_FORBIDDEN 369 ngx.say("403 Forbidden") 370 ngx.exit(ngx.HTTP_FORBIDDEN) 371 end 372 373 function me.not_found(msg, err) 374 me.audit(msg, err) 375 ngx.status = ngx.HTTP_NOT_FOUND 376 ngx.say("404 Not Found") 377 ngx.exit(ngx.HTTP_NOT_FOUND) 378 end 379 380 function me.bad_request(msg, err) 381 me.audit(msg, err) 382 ngx.status = ngx.HTTP_BAD_REQUEST 383 ngx.say("400 Bad Request") 384 ngx.exit(ngx.HTTP_BAD_REQUEST) 385 end 386 387 function me.logout() 388 local ck = me.readCookie("vz_authn") 389 local redirectURL = me.hostUri 390 if ck then 391 if me.oidcProvider ~= "dex" then 392 local rft = ck.rt 393 local redirectArgs = 394 ngx.encode_args( 395 { 396 redirect_uri = me.hostUri 397 } 398 ) 399 ngx.req.set_header("Content-Type", "application/x-www-form-urlencoded") 400 ngx.req.set_method(ngx.HTTP_POST) 401 redirectURL = me.getOidcProviderUri() .. "/protocol/openid-connect/logout?" .. redirectArgs 402 403 local postArgs = 404 ngx.encode_args( 405 { 406 refresh_token = ck.rt, 407 redirect_uri = redirectURL, 408 client_id = oidcClient 409 } 410 ) 411 ngx.req.read_body() 412 413 ngx.req.set_body_data(postArgs) 414 end 415 me.deleteCookies("vz_authn", "vz_userinfo") 416 ngx.redirect(redirectURL) 417 end 418 end 419 420 function me.singleLogout() 421 -- Single logout not supported yet. 422 ngx.status = ngx.HTTP_METHOD_NOT_IMPLEMENTED 423 ngx.say("501 Single Logout not supported.") 424 ngx.exit(ngx.HTTP_METHOD_NOT_IMPLEMENTED) 425 end 426 427 function me.randomBase64(size) 428 local randBytes = random.bytes(size) 429 local encoded = base64.encode_base64url(randBytes) 430 return string.sub(encoded, 1, size) 431 end 432 433 function me.read_file(path) 434 local file = io.open(path, "rb") 435 if not file then return nil end 436 local content = file:read "*a" 437 file:close() 438 return content 439 end 440 441 function me.write_file(path, data) 442 local file = io.open(path, "a+") 443 if not file then return nil end 444 file:write(data) 445 file:close() 446 end 447 448 function me.hasCredentialType(authHeader, credentialType) 449 if authHeader then 450 local start, _ = authHeader:find(credentialType) 451 if start then 452 return true 453 end 454 end 455 return false 456 end 457 458 function me.requestUriMatches(requestUri, matchPath) 459 if requestUri then 460 if requestUri == matchPath then 461 return true 462 end 463 local start, _ = requestUri:find(matchPath..'?') 464 if start == 1 then 465 return true 466 end 467 end 468 return false 469 end 470 471 function me.startsWith(ingressHost, start) 472 return string.sub(ingressHost,1,string.len(start)) == start 473 end 474 475 local authproxy_prefix = 'verrazzano-authproxy-' 476 local in_cluster_namespace = '.verrazzano-system' 477 local in_cluster_dns_suffix = '.svc.cluster.local' 478 local vmi_system = '.vmi.system' 479 480 function me.validateIngressHost(backend, hostname, in_cluster) 481 -- assume backend, hostname, are always non-nil and non-empty 482 local check_host = nil 483 if in_cluster == true then 484 -- try full path 485 check_host = authproxy_prefix..backend..in_cluster_namespace..in_cluster_dns_suffix 486 if check_host == hostname then 487 return backend 488 end 489 -- try with just the namespace 490 check_host = authproxy_prefix..backend..in_cluster_namespace 491 if check_host == hostname then 492 return backend 493 end 494 -- try without namespace or dns suffix 495 check_host = authproxy_prefix..backend 496 if check_host == hostname then 497 return backend 498 end 499 else 500 if backend == 'verrazzano' then 501 check_host = backend..me.hostSuffix 502 elseif backend == 'jaeger' then 503 check_host = backend..me.hostSuffix 504 elseif backend == 'thanos-query' then 505 check_host = backend..me.hostSuffix 506 elseif backend == 'thanos-query-store' then 507 check_host = backend..me.hostSuffix 508 elseif backend == 'thanos-ruler' then 509 check_host = backend..me.hostSuffix 510 elseif backend == 'opensearch-logging' then 511 check_host = 'opensearch.logging'..me.hostSuffix 512 elseif backend == 'osd-logging' then 513 check_host = 'osd.logging'..me.hostSuffix 514 elseif backend == 'alertmanager' then 515 check_host = backend..me.hostSuffix 516 else 517 check_host = backend..vmi_system..me.hostSuffix 518 end 519 if check_host == hostname then 520 return backend 521 end 522 end 523 return 'invalid' 524 end 525 526 function me.getBackendNameFromIngressHost(ingressHost) 527 local backend_name = 'unknown' 528 local able_to_redirect = true 529 local hostname = nil 530 if ingressHost and #ingressHost > 0 then 531 me.debug("ingressHost is '"..ingressHost.."'") 532 -- Strip the port off, if present 533 local first, last = nil 534 first, last, hostname = ingressHost:find("^([^:]+)") 535 if hostname and #hostname > 0 then 536 first, last, backend_name = hostname:find("^([^.]+)") 537 if backend_name and #backend_name > 0 then 538 -- Append '-logging' if the ingressHost is for OS or OSD from logging namespace 539 if me.startsWith(ingressHost, 'opensearch.logging') or me.startsWith(ingressHost, 'osd.logging') then 540 backend_name = backend_name .. '-logging' 541 end 542 -- Strip the auth proxy prefix from the extracted backend name if present 543 if string.sub(backend_name, 1, #authproxy_prefix) == authproxy_prefix then 544 backend_name = string.sub(backend_name, #authproxy_prefix+1, -1) 545 me.debug("Validating host (local): backend_name is '"..backend_name.."', hostname is '"..hostname.."'") 546 backend_name = me.validateIngressHost(backend_name, hostname, true) 547 else 548 me.debug("Validating host (ingress): backend_name is '"..backend_name.."', hostname is '"..hostname.."'") 549 backend_name = me.validateIngressHost(backend_name, hostname, false) 550 end 551 end 552 end 553 end 554 local uri = ngx.var.request_uri 555 if backend_name == "verrazzano" then 556 local first, last = uri:find("/20210501/") 557 if not first or first ~= 1 then 558 backend_name = "console" 559 else 560 able_to_redirect = false 561 end 562 end 563 if backend_name == "osd" then 564 if not (uri == "/" or uri:find("/app/") == 1) then 565 able_to_redirect = false 566 end 567 end 568 if backend_name == "kibana" then 569 if not (uri == "/" or uri:find("/app/") == 1) then 570 able_to_redirect = false 571 end 572 end 573 if able_to_redirect == true then 574 me.debug("returning backend_name '"..backend_name.."', able_to_redirect is true") 575 else 576 me.debug("returning backend_name '"..backend_name.."', able_to_redirect is false") 577 end 578 return backend_name, able_to_redirect 579 end 580 581 function me.makeConsoleBackendUrl(port) 582 return 'http://verrazzano-console.verrazzano-system.svc.cluster.local'..':'..port 583 end 584 585 function me.makeVmiBackendUrl(backend, port) 586 return 'http://vmi-system-'..backend..'.verrazzano-system.svc.cluster.local'..':'..port 587 end 588 589 function me.makeMonitoringComponentBackendUrl(protocol, serviceName, port) 590 return protocol..'://'..serviceName..'.verrazzano-monitoring.svc.cluster.local'..':'..port 591 end 592 593 function me.makeLoggingComponentBackendUrl(serviceName, port) 594 return opensearchProtocol..'://'..serviceName..'.'..opensearchNamespace..'.svc.cluster.local'..':'..port 595 end 596 597 function me.getBackendServerUrlFromName(backend) 598 local serverUrl = nil 599 if backend == 'verrazzano' then 600 -- assume we're going to the local server; if not, we'll fix up the url when we handle the remote call 601 -- Route request to k8s API 602 serverUrl = me.getLocalKubernetesApiUrl() 603 elseif backend == 'console' then 604 -- Route request to console 605 serverUrl = me.makeConsoleBackendUrl('8000') 606 elseif backend == 'grafana' then 607 serverUrl = me.makeVmiBackendUrl(backend, '3000') 608 elseif backend == 'prometheus' then 609 serverUrl = me.makeMonitoringComponentBackendUrl('http', 'prometheus-operator-kube-p-prometheus', '9090') 610 elseif backend == 'thanos-query' then 611 serverUrl = me.makeMonitoringComponentBackendUrl('http', 'thanos-query-frontend', '9090') 612 elseif backend == 'thanos-query-store' then 613 serverUrl = me.makeMonitoringComponentBackendUrl('grpc', 'thanos-query-grpc', '10901') 614 elseif backend == 'thanos-ruler' then 615 serverUrl = me.makeMonitoringComponentBackendUrl('http', 'thanos-ruler', '9090') 616 elseif backend == 'kibana' then 617 serverUrl = me.makeLoggingComponentBackendUrl(osdService, '5601') 618 elseif backend == 'osd' then 619 serverUrl = me.makeLoggingComponentBackendUrl(osdService, '5601') 620 elseif backend == 'elasticsearch' then 621 serverUrl = me.makeLoggingComponentBackendUrl(opensearchService, '9200') 622 elseif backend == 'opensearch' then 623 serverUrl = me.makeLoggingComponentBackendUrl(opensearchService, '9200') 624 elseif backend == 'opensearch-logging' then 625 serverUrl = me.makeLoggingComponentBackendUrl('opensearch', '9200') 626 elseif backend == 'osd-logging' then 627 serverUrl = me.makeLoggingComponentBackendUrl('opensearch-dashboards', '5601') 628 elseif backend == 'kiali' then 629 serverUrl = me.makeVmiBackendUrl(backend, '20001') 630 elseif backend == 'jaeger' then 631 serverUrl = me.makeMonitoringComponentBackendUrl('http', 'jaeger-operator-jaeger-query', '16686') 632 elseif backend == 'alertmanager' then 633 serverUrl = me.makeMonitoringComponentBackendUrl('http', 'prometheus-operator-kube-p-alertmanager', '9093') 634 else 635 me.not_found("Invalid backend name '"..backend.."'") 636 end 637 return serverUrl 638 end 639 640 -- console originally sent access token by itself, as bearer token (originally obtained via pkce client) 641 -- with combined proxy, console no longer handles tokens, but tests may be sending ID tokens. 642 function me.handleBearerToken(authHeader) 643 local found, index = authHeader:find('Bearer') 644 if found then 645 local token = string.sub(authHeader, index+2) 646 if token then 647 me.debug("Found bearer token in authorization header") 648 me.oidcValidateBearerToken(token) 649 return token 650 else 651 me.unauthorized("Missing token in authorization header") 652 end 653 end 654 return nil 655 end 656 657 local basicCache = {} 658 659 -- should only be called if some vz process is trying to access vmi using basic auth 660 -- tokens are cached locally 661 function me.handleBasicAuth(authHeader) 662 -- me.debug("Checking for basic auth credentials") 663 local found, index = authHeader:find('Basic') 664 if not found then 665 me.debug("No basic auth credentials found") 666 return nil 667 end 668 local basicCred = string.sub(authHeader, index+2) 669 if not basicCred then 670 me.unauthorized("Invalid BasicAuth authorization header") 671 end 672 me.debug("Found basic auth credentials in authorization header") 673 local now = ngx.time() 674 local basicAuth = basicCache[basicCred] 675 if basicAuth and (now < basicAuth.expiry) then 676 me.debug("Returning cached token") 677 return basicAuth.id_token 678 end 679 local decode, err = ngx.decode_base64(basicCred) 680 if err then 681 me.unauthorized("Unable to decode BasicAuth authorization header") 682 end 683 local found = decode:find(':') 684 if not found then 685 me.unauthorized("Invalid BasicAuth authorization header") 686 end 687 local u = decode:sub(1, found-1) 688 local p = decode:sub(found+1) 689 local tokenRes = me.oidcGetTokenWithBasicAuth(u, p) 690 if not tokenRes then 691 me.unauthorized("Could not get token") 692 end 693 me.oidcValidateIDTokenPG(tokenRes.id_token) 694 local expires_in = tonumber(tokenRes.expires_in) 695 for key, val in pairs(basicCache) do 696 if val.expiry and now > val.expiry then 697 basicCache[key] = nil 698 end 699 end 700 basicCache[basicCred] = { 701 -- access_token = tokenRes.access_token, 702 id_token = tokenRes.id_token, 703 expiry = now + expires_in 704 } 705 return tokenRes.id_token 706 end 707 708 function me.getOidcProviderUri() 709 if me.oidcProviderUri and me.oidcProviderUri ~= "" then 710 return me.oidcProviderUri 711 else 712 return me.oidcProviderInClusterUri 713 end 714 end 715 716 function me.getLocalOidcProviderUri() 717 if me.oidcProviderInClusterUri and me.oidcProviderInClusterUri ~= "" then 718 return me.oidcProviderInClusterUri 719 else 720 return me.oidcProviderUri 721 end 722 end 723 724 function me.getOidcTokenUri() 725 if me.oidcProvider == "dex" then 726 return me.getLocalOidcProviderUri() .. "/token" 727 end 728 729 return me.getLocalOidcProviderUri() .. "/protocol/openid-connect/token" 730 end 731 732 function me.getOidcCertsUri() 733 if me.oidcProvider == "dex" then 734 return me.getLocalOidcProviderUri() .. "/keys" 735 end 736 737 return me.getLocalOidcProviderUri() .. "/protocol/openid-connect/certs" 738 end 739 740 function me.getOidcAuthUri() 741 if me.oidcProvider == "dex" then 742 return me.getOidcProviderUri() .. "/auth/local" 743 end 744 745 return me.getOidcProviderUri() .. "/protocol/openid-connect/auth" 746 end 747 748 function me.oidcAuthenticate() 749 me.debug("Authenticating user") 750 local sha256 = (require 'resty.sha256'):new() 751 -- code verifier must be between 43 and 128 characters 752 local codeVerifier = me.randomBase64(56) 753 sha256:update(codeVerifier) 754 local codeChallenge = base64.encode_base64url(sha256:final()) 755 local state = me.randomBase64(32) 756 local nonce = me.randomBase64(32) 757 local stateData = { 758 state = state, 759 request_uri = ngx.var.request_uri, 760 code_verifier = codeVerifier, 761 code_challenge = codeChallenge, 762 nonce = nonce 763 } 764 local rawRedirectArgs = { 765 client_id = oidcClient, 766 response_type = 'code', 767 scope = 'openid', 768 code_challenge_method = 'S256', 769 code_challenge = codeChallenge, 770 state = state, 771 nonce = nonce, 772 redirect_uri = me.callbackUri 773 } 774 775 if me.oidcProvider == "dex" then 776 rawRedirectArgs.scope = "openid email profile offline_access" 777 else 778 rawRedirectArgs.scope = "openid" 779 end 780 781 local redirectArgs = ngx.encode_args(rawRedirectArgs) 782 local redirectURL = me.getOidcAuthUri() .. "?" .. redirectArgs 783 -- there could be an existing (expired) vz_authn cookie. 784 -- delete it (and vz_userinfo) to avoid exceeding max header size 785 me.deleteCookies("vz_authn", "vz_userinfo") 786 me.setCookie("vz_state", stateData, authStateTtlInSec, true) 787 ngx.header["Cache-Control"] = "no-cache, no-store, max-age=0" 788 ngx.redirect(redirectURL) 789 end 790 791 function me.oidcHandleCallback() 792 me.debug("Handle authentication callback") 793 local queryParams = me.queryParams(ngx.var.request_uri) 794 local state = queryParams.state 795 local code = queryParams.code 796 local nonce = queryParams.nonce 797 local cookie = me.readCookie("vz_state") 798 if not cookie then 799 me.unauthorized("Missing state cookie") 800 end 801 me.deleteCookies("vz_state") 802 local stateCk = cookie.state 803 -- local nonceCk = cookie.nonce 804 local request_uri = cookie.request_uri 805 806 if (state == nil) or (stateCk == nil) then 807 me.unauthorized("Missing callback state") 808 else 809 if state ~= stateCk then 810 me.unauthorized("Invalid callback state") 811 end 812 if not cookie.code_verifier then 813 me.unauthorized("Invalid code_verifier") 814 end 815 local tokenRes = me.oidcGetTokenWithCode(code, cookie.code_verifier, me.callbackUri) 816 if tokenRes then 817 me.oidcValidateIDTokenPKCE(tokenRes.id_token) 818 me.tokenToCookie(tokenRes) 819 ngx.redirect(request_uri) 820 end 821 me.unauthorized("Failed to obtain token with code") 822 end 823 end 824 825 function me.oidcTokenRequest(formArgs) 826 me.debug("Requesting token from OP") 827 local tokenUri = me.getOidcTokenUri() 828 local http = require "resty.http" 829 local httpc = http.new() 830 local res, err = httpc:request_uri(tokenUri, { 831 method = "POST", 832 body = ngx.encode_args(formArgs), 833 headers = { 834 ["Content-Type"] = "application/x-www-form-urlencoded", 835 } 836 }) 837 if err then 838 me.error("Failed requesting token from " .. me.oidcProvider .. ": " .. err) 839 me.unauthorized("Error requesting token", err) 840 end 841 if not res then 842 me.info("Failed requesting token from " .. me.oidcProvider .. ": response is nil") 843 me.unauthorized("Error requesting token: response is nil") 844 end 845 if not (res.status == 200) then 846 me.info( 847 "Failed requesting token from " .. me.oidcProvider .. ": response status code is " .. 848 res.status .. " and response body is " .. res.body 849 ) 850 me.unauthorized( 851 "Error requesting token: response status code is " .. res.status .. " and response body is " .. res.body 852 ) 853 end 854 local tokenRes = cjson.decode(res.body) 855 if tokenRes.error or tokenRes.error_description then 856 me.info("Failed requesting token from " .. me.oidcProvider .. ": " .. tokenRes.error_description) 857 me.unauthorized("Error requesting token: " .. tokenRes.error_description) 858 end 859 return tokenRes 860 end 861 862 function me.oidcGetTokenWithBasicAuth(u, p) 863 local params = { 864 grant_type = 'password', 865 scope = 'openid', 866 client_id = oidcDirectAccessClient, 867 password = p, 868 username = u 869 } 870 if me.oidcProvider == "dex" then 871 params.scope = "openid profile" 872 end 873 return me.oidcTokenRequest(params) 874 end 875 876 function me.oidcGetTokenWithCode(code, verifier, callbackUri) 877 local params = { 878 grant_type = "authorization_code", 879 client_id = oidcClient, 880 code = code, 881 code_verifier = verifier, 882 redirect_uri = callbackUri 883 } 884 if me.oidcProvider == "dex" then 885 params.client_secret = oidcClientSecret 886 params.scope = "openid offline_access email" 887 end 888 return me.oidcTokenRequest(params) 889 end 890 891 function me.oidcRefreshToken(rft, callbackUri) 892 return me.oidcTokenRequest({ 893 grant_type = 'refresh_token', 894 client_id = oidcClient, 895 refresh_token = rft, 896 redirect_uri = callbackUri 897 }) 898 end 899 900 function me.oidcValidateBearerToken(token) 901 -- console sends access tokens obtained via PKCE client 902 -- test code sends ID tokens obtained from the PG client 903 -- need to accept either type in Authorization header (for now) 904 local clients = { oidcClient, oidcDirectAccessClient } 905 if oidcClient ~= adminClusterOidcClient then 906 clients = { oidcClient, adminClusterOidcClient, oidcDirectAccessClient } 907 end 908 local claim_spec = { 909 typ = validators.equals_any_of({"Bearer", "ID"}), 910 iss = validators.equals(me.oidcIssuerUri), 911 azp = validators.equals_any_of(clients) 912 } 913 914 if me.oidcProvider == "dex" then 915 claim_spec = { 916 iss = validators.equals(me.oidcIssuerUri), 917 aud = validators.equals_any_of(clients) 918 } 919 end 920 921 me.oidcValidateTokenWithClaims(token, claim_spec) 922 end 923 924 function me.oidcValidateIDTokenPKCE(token) 925 me.oidcValidateToken(token, "ID", me.oidcIssuerUri, oidcClient) 926 end 927 928 function me.oidcValidateIDTokenPG(token) 929 if not me.oidcIssuerUriLocal then 930 me.oidcValidateToken(token, "ID", me.oidcIssuerUri, oidcDirectAccessClient) 931 else 932 me.oidcValidateToken(token, "ID", me.oidcIssuerUriLocal, oidcDirectAccessClient) 933 end 934 end 935 936 function me.oidcValidateToken(token, expectedType, expectedIssuer, clientName) 937 if not token or token == "" then 938 me.unauthorized("Nil or empty token") 939 end 940 if not expectedType then 941 me.unauthorized("Nil or empty expectedType") 942 end 943 if not expectedIssuer then 944 me.unauthorized("Nil or empty expectedIssuer") 945 end 946 if not clientName then 947 me.unauthorized("Nil or empty clientName") 948 end 949 local claim_spec = { 950 typ = validators.equals( expectedType ), 951 iss = validators.equals( expectedIssuer ), 952 azp = validators.equals( clientName ) 953 } 954 if me.oidcProvider == "dex" then 955 claim_spec = { 956 iss = validators.equals(me.oidcIssuerUri), 957 aud = validators.equals(clientName) 958 } 959 end 960 me.oidcValidateTokenWithClaims(token, claim_spec) 961 end 962 963 function me.oidcValidateTokenWithClaims(token, claim_spec) 964 me.debug("Validating JWT token") 965 local default_claim_spec = { 966 iat = validators.is_not_before(), 967 exp = validators.is_not_expired(), 968 aud = validators.required() 969 } 970 -- passing verify a function to retrieve key didn't seem to work, so doing load then verify 971 local jwt_obj = jwt:load_jwt(token) 972 if (not jwt_obj) or (not jwt_obj.header) or (not jwt_obj.header.kid) then 973 me.unauthorized("Failed to load token or no kid") 974 end 975 local publicKey = me.publicKey(jwt_obj.header.kid) 976 if not publicKey then 977 me.unauthorized("No public key found") 978 end 979 -- me.debug("TOKEN: iss is "..jwt_obj.payload.iss) 980 -- me.debug("TOKEN: oidcIssuerUri is"..me.oidcIssuerUri) 981 local verified = jwt:verify_jwt_obj(publicKey, jwt_obj, default_claim_spec, claim_spec) 982 if not verified or (tostring(jwt_obj.valid) == "false" or tostring(jwt_obj.verified) == "false") then 983 me.unauthorized("Failed to validate token", jwt_obj.reason) 984 end 985 end 986 987 function me.isAuthorized(idToken) 988 me.debug("Checking for required role '"..requiredRole.."'") 989 local id_token = jwt:load_jwt(idToken) 990 local userRoles = me.getRoles(idToken) 991 me.debug("Roles available for users '"..userRoles.."'") 992 if userRoles:match(requiredRole) then 993 me.debug ("Required role '"..requiredRole.."' found") 994 return true 995 else 996 me.debug ("Required role '"..requiredRole.."' not found") 997 return false 998 end 999 end 1000 1001 function me.usernameFromIdToken(idToken) 1002 -- me.debug("usernameFromIdToken: fetching preferred_username") 1003 local id_token = jwt:load_jwt(idToken) 1004 if me.oidcProvider == "dex" then 1005 if id_token and id_token.payload and id_token.payload.name then 1006 return id_token.payload.name 1007 end 1008 me.unauthorized("nameIdToken: name not found") 1009 end 1010 1011 if id_token and id_token.payload and id_token.payload.preferred_username then 1012 return id_token.payload.preferred_username 1013 end 1014 me.unauthorized("usernameFromIdToken: preferred_username not found") 1015 end 1016 1017 -- returns id token, token is refreshed first, if necessary. 1018 -- nil token returned if no session or the refresh token has expired 1019 function me.getTokenFromSession() 1020 -- me.debug("Check for existing session") 1021 local ck = me.readCookie("vz_authn") 1022 if ck then 1023 me.debug("Existing session found") 1024 local rft = ck.rt 1025 local now = ngx.time() 1026 local expiry = tonumber(ck.expiry) 1027 local refresh_expiry = tonumber(ck.refresh_expiry) 1028 if now < expiry then 1029 -- me.debug("Returning ID token") 1030 return ck.it 1031 else 1032 if now < refresh_expiry then 1033 me.debug("Token is expired, refreshing") 1034 local tokenRes = me.oidcRefreshToken(rft, me.callbackUri) 1035 if tokenRes then 1036 me.oidcValidateIDTokenPKCE(tokenRes.id_token) 1037 me.tokenToCookie(tokenRes) 1038 -- me.debug("Token refreshed", tokenRes) 1039 return tokenRes.id_token 1040 else 1041 me.debug("No valid response from token refresh") 1042 end 1043 else 1044 me.debug("Refresh token expired, cannot refresh") 1045 end 1046 end 1047 else 1048 me.debug("No existing session found") 1049 end 1050 -- no valid token found, delete cookie 1051 me.deleteCookies("vz_authn", "vz_userinfo") 1052 return nil 1053 end 1054 1055 function me.tokenToCookie(tokenRes) 1056 -- Do we need access_token? too big > 4k 1057 local cookiePairs = { 1058 rt = tokenRes.refresh_token, 1059 -- at = tokenRes.access_token, 1060 it = tokenRes.id_token 1061 } 1062 -- Cookie to save username and email with http-only diabled 1063 local userCookiePairs = { 1064 username = "" 1065 } 1066 local id_token = jwt:load_jwt(tokenRes.id_token) 1067 local expires_in = tonumber(tokenRes.expires_in) 1068 local refresh_expires_in = expires_in 1069 if tokenRes.refresh_expires_in then 1070 refresh_expires_in = tonumber(tokenRes.refresh_expires_in) 1071 end 1072 1073 local now = ngx.time() 1074 local issued_at = now 1075 if id_token and id_token.payload then 1076 if id_token.payload.iat then 1077 issued_at = tonumber(id_token.payload.iat) 1078 else 1079 if id_token.payload.auth_time then 1080 issued_at = tonumber(id_token.payload.auth_time) 1081 end 1082 end 1083 if id_token.payload.preferred_username then 1084 userCookiePairs.username = id_token.payload.preferred_username 1085 end 1086 end 1087 local skew = now - issued_at 1088 -- Expire 30 secs before actual time 1089 local expiryBuffer = 30 1090 cookiePairs.expiry = now + expires_in - skew - expiryBuffer 1091 cookiePairs.refresh_expiry = now + refresh_expires_in - skew - expiryBuffer 1092 userCookiePairs.expiry = now + expires_in - skew - expiryBuffer 1093 userCookiePairs.refresh_expiry = now + refresh_expires_in - skew - expiryBuffer 1094 local expiresInSec = refresh_expires_in - expiryBuffer 1095 me.setCookie("vz_authn", cookiePairs, expiresInSec, true) 1096 me.setCookie("vz_userinfo", userCookiePairs, expiresInSec, false) 1097 end 1098 1099 function me.setCookie(ckName, cookiePairs, expiresInSec, httponly) 1100 local ck = require "resty.cookie" 1101 local cookie, err = ck:new() 1102 if not cookie then 1103 me.debug("Error setting cookie "..ckName..": "..err) 1104 return 1105 end 1106 local expires = ngx.cookie_time(ngx.time() + expiresInSec) 1107 local ckValue = "" 1108 if ckName == "vz_userinfo" then 1109 -- No need to encrypt in case of vz_userinfo 1110 for key, value in pairs(cookiePairs) do 1111 ckValue = ckValue..key.."="..value.."," 1112 end 1113 ckValue = ckValue:sub(1, -2) 1114 else 1115 ckValue = me.aes256:encrypt(cjson.encode(cookiePairs)) 1116 end 1117 ckValue = base64.encode_base64url(ckValue) 1118 cookie:set({key=ckName, value=ckValue, path="/", secure=true, httponly=httponly, expires=expires}) 1119 end 1120 1121 function me.deleteCookies(...) 1122 local cookies = {} 1123 local arg = {...} 1124 for i,v in ipairs(arg) do 1125 cookies[i] = tostring(v)..'=; Path=/; Secure; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 UTC;' 1126 end 1127 ngx.header["Set-Cookie"] = cookies 1128 end 1129 1130 function me.readCookie(ckName) 1131 if not ckName then 1132 return nil 1133 end 1134 local cookie, err = require("resty.cookie"):new() 1135 local ck = cookie:get(ckName) 1136 if not ck then 1137 me.debug("Cookie not found") 1138 return nil 1139 end 1140 local decoded = base64.decode_base64url(ck) 1141 if not decoded then 1142 me.debug("Cookie not decoded") 1143 return nil 1144 end 1145 local json = me.aes256:decrypt(decoded) 1146 if not json then 1147 me.debug("Cookie not decrypted") 1148 return nil 1149 end 1150 return cjson.decode(json) 1151 end 1152 1153 local certs = {} 1154 local moduli = {} 1155 local exponents = {} 1156 1157 function me.realmCerts(kid) 1158 local pk = certs[kid] 1159 local modulus = moduli[kid] 1160 local exponent = exponents[kid] 1161 if pk then 1162 return pk, nil, nil 1163 elseif modulus and exponent then 1164 return nil, modulus, exponent 1165 end 1166 local http = require "resty.http" 1167 local httpc = http.new() 1168 local certsUri = me.getOidcCertsUri() 1169 local res, err = httpc:request_uri(certsUri) 1170 if err then 1171 me.error("Could not retrieve certs: "..err) 1172 return nil 1173 end 1174 local data = cjson.decode(res.body) 1175 if not (data.keys) then 1176 me.error("Failed to find keys: key object is nil") 1177 return nil 1178 end 1179 for i, key in pairs(data.keys) do 1180 if key.kid and key.x5c then 1181 certs[key.kid] = key.x5c 1182 elseif key.kty == "RSA" and key.n and key.e then 1183 moduli[key.kid] = key.n 1184 exponents[key.kid] = key.e 1185 end 1186 end 1187 return certs[kid], moduli[kid], exponents[kid] 1188 end 1189 1190 function me.publicKey(kid) 1191 local x5c, n, e = me.realmCerts(kid) 1192 if x5c and #x5c ~= 0 then 1193 return "-----BEGIN CERTIFICATE-----\n" .. x5c[1] .. "\n-----END CERTIFICATE-----" 1194 end 1195 1196 if n and e then 1197 return me.generate_key(n, e) 1198 end 1199 1200 return nil 1201 end 1202 1203 -- api-proxy - methods for handling multi-cluster k8s API requests 1204 1205 local vzApiHost = os.getenv("VZ_API_HOST") 1206 local vzApiVersion = os.getenv("VZ_API_VERSION") 1207 1208 function me.getServiceAccountToken() 1209 me.debug("Read service account token") 1210 local serviceAccountToken = me.read_file("/run/secrets/kubernetes.io/serviceaccount/token") 1211 if not (serviceAccountToken) then 1212 me.unauthorized("No service account token present in pod.") 1213 end 1214 return serviceAccountToken 1215 end 1216 1217 function me.getLocalKubernetesApiUrl() 1218 local host = os.getenv("KUBERNETES_SERVICE_HOST") 1219 local port = os.getenv("KUBERNETES_SERVICE_PORT") 1220 local serverUrl = "https://" .. host .. ":" .. port 1221 return serverUrl 1222 end 1223 1224 function me.getK8SResource(token, resourcePath) 1225 local http = require "resty.http" 1226 local httpc = http.new() 1227 local res, err = httpc:request_uri("https://" .. vzApiHost .. "/" .. vzApiVersion .. resourcePath,{ 1228 headers = { 1229 ["Authorization"] = "Bearer "..token 1230 }, 1231 }) 1232 if err then 1233 me.unauthorized("Error accessing vz api", err) 1234 end 1235 if not(res) or not (res.body) then 1236 me.unauthorized("Unable to get k8s resource.") 1237 end 1238 local cjson = require "cjson" 1239 return cjson.decode(res.body) 1240 end 1241 1242 function me.getVMC(token, cluster) 1243 return me.getK8SResource(token, "/apis/clusters.verrazzano.io/v1alpha1/namespaces/verrazzano-mc/verrazzanomanagedclusters/" .. cluster) 1244 end 1245 1246 function me.getSecret(token, secret) 1247 return me.getK8SResource(token, "/api/v1/namespaces/verrazzano-mc/secrets/" .. secret) 1248 end 1249 1250 function me.handleLocalAPICall(token) 1251 local uri = ngx.var.uri 1252 local first, last = uri:find("/"..vzApiVersion) 1253 if first and first == 1 then 1254 uri = string.sub(uri, last+1) 1255 ngx.req.set_uri(uri) 1256 end 1257 1258 local serviceAccountToken = me.getServiceAccountToken() 1259 me.debug("Set service account bearer token as Authorization header") 1260 ngx.req.set_header("Authorization", "Bearer " .. serviceAccountToken) 1261 1262 if not token then 1263 me.unauthenticated("Invalid token") 1264 end 1265 1266 local jwt_obj = jwt:load_jwt(token) 1267 if not jwt_obj then 1268 me.unauthenticated("Invalid token") 1269 end 1270 1271 local groups = {} 1272 1273 if jwt_obj.payload and jwt_obj.payload.sub then 1274 -- Uid is ignored prior to kubernetes v1.22 1275 me.debug(("Adding sub as Impersonate-Uid: " .. jwt_obj.payload.sub)) 1276 ngx.req.set_header("Impersonate-Uid", jwt_obj.payload.sub) 1277 -- this group is needed for discovery, but not automatically set for impersonated users prior to v1.20. 1278 -- the group is ignored if duplicated >= v1.20, so no harm done if we always send it. 1279 -- adding it here so that it's present IFF we actually have a subject. 1280 local sys_auth = "system:authenticated" 1281 me.debug(("Including group: " .. sys_auth)) 1282 table.insert(groups, sys_auth) 1283 end 1284 if jwt_obj.payload and jwt_obj.payload.preferred_username then 1285 me.debug(("Adding preferred_username as Impersonate-User: " .. jwt_obj.payload.preferred_username)) 1286 ngx.req.set_header("Impersonate-User", jwt_obj.payload.preferred_username) 1287 end 1288 1289 if me.oidcProvider == "dex" and jwt_obj.payload and jwt_obj.payload.name then 1290 me.debug(("Adding name as Impersonate-User: " .. jwt_obj.payload.name)) 1291 ngx.req.set_header("Impersonate-User", jwt_obj.payload.name) 1292 me.debug("Adding verrazzano-admins as Impersonate-Group") 1293 table.insert(groups, "verrazzano-admins") 1294 end 1295 1296 if jwt_obj.payload and jwt_obj.payload.groups then 1297 for key, grp in pairs(jwt_obj.payload.groups) do 1298 table.insert(groups, grp) 1299 end 1300 end 1301 if #groups > 0 then 1302 me.debug(("Adding groups as Impersonate-Group: " .. table.concat(groups, ", "))) 1303 ngx.req.set_header("Impersonate-Group", groups) 1304 end 1305 end 1306 1307 function me.handleExternalAPICall(token) 1308 local args = ngx.req.get_uri_args() 1309 1310 me.debug("Read vmc resource for " .. args.cluster) 1311 local vmc = me.getVMC(token, args.cluster) 1312 if not(vmc) or not(vmc.status) or not(vmc.status.apiUrl) then 1313 me.unauthorized("Unable to fetch vmc api url for vmc " .. args.cluster) 1314 end 1315 local serverUrl = vmc.status.apiUrl 1316 1317 -- To access managed cluster api server on self signed certificates, the admin cluster api server needs ca certificates for the managed cluster. 1318 -- A secret is created in admin cluster during multi cluster setup that contains the ca certificate. 1319 -- Here we read the name of that secret from vmc spec and retrieve the secret from cluster and read the cacrt field. 1320 -- The value of cacrt field is decoded to get the ca certificate and is appended to file being pointed to by the proxy_ssl_trusted_certificate variable. 1321 1322 if not(vmc.spec) or not(vmc.spec.caSecret) then 1323 me.debug("ca secret name not present on vmc resource, assuming well known CA certificate exists for managed cluster " .. args.cluster) 1324 do return end 1325 end 1326 1327 local secret = me.getSecret(token, vmc.spec.caSecret) 1328 if not(secret) or not(secret.data) or not(secret.data["cacrt"]) or secret.data["cacrt"] == "" then 1329 me.debug("Unable to fetch ca secret for vmc, assuming well known CA certificate exists for managed cluster " .. args.cluster) 1330 do return end 1331 end 1332 1333 local decodedSecret = ngx.decode_base64(secret.data["cacrt"]) 1334 if not(decodedSecret) then 1335 me.unauthorized("Unable to decode ca secret for vmc to access api server of managed cluster " .. args.cluster) 1336 end 1337 1338 local startIndex, _ = string.find(decodedSecret, "-----BEGIN CERTIFICATE-----") 1339 local _, endIndex = string.find(decodedSecret, "-----END CERTIFICATE-----") 1340 if startIndex >= 1 and endIndex > startIndex then 1341 me.write_file("/etc/nginx/upstream.pem", string.sub(decodedSecret, startIndex, endIndex)) 1342 end 1343 1344 -- remove the cluster query param 1345 args["cluster"] = nil 1346 ngx.req.set_uri_args(args) 1347 1348 -- propagate the user's token as a bearer token, remote cluster won't have access to the session. 1349 ngx.req.set_header("Authorization", "Bearer "..token) 1350 1351 return serverUrl 1352 end 1353 1354 function me.isBodyValidJson() 1355 ngx.req.read_body() 1356 local data = ngx.req.get_body_data() 1357 if data ~= nil then 1358 local decoder = require("cjson.safe").decode 1359 local decoded_data, err = decoder(data) 1360 if err then 1361 me.debug("Invalid request payload: " .. data) 1362 return false 1363 end 1364 end 1365 return true 1366 end 1367 1368 function me.isOriginAllowed(origin, backend, ingressUri) 1369 -- As per https://datatracker.ietf.org/doc/rfc6454, "User Agent Requirements" section a "null" value for 1370 -- Origin header is set by user agents for privacy-sensitive contexts. However it does not defined what 1371 -- a privacy-sensitive context means. The "Privacy-Sensitive Contexts" section of https://wiki.mozilla.org/Security/Origin 1372 -- defines certain contexts as privacy sensitive but there also it does not explain behaviour of server for the 1373 -- "Access-Control-Allow-Origin" header. Therefore we do not allow such requests. 1374 if origin == "null" then 1375 return false 1376 end 1377 1378 if origin == ingressUri then 1379 return true 1380 end 1381 1382 local allowedOrigins = nil 1383 if backend == 'verrazzano' then 1384 allowedOrigins = os.getenv("VZ_API_ALLOWED_ORIGINS") 1385 elseif backend == 'console' then 1386 allowedOrigins = os.getenv("VZ_CONSOLE_ALLOWED_ORIGINS") 1387 elseif backend == 'grafana' then 1388 allowedOrigins = os.getenv("VZ_GRAFANA_ALLOWED_ORIGINS") 1389 elseif backend == 'prometheus' then 1390 allowedOrigins = os.getenv("VZ_PROMETHEUS_ALLOWED_ORIGINS") 1391 elseif backend == 'thanos-query' then 1392 allowedOrigins = os.getenv("VZ_THANOS_QUERY_ALLOWED_ORIGINS") 1393 elseif backend == 'thanos-query-store' then 1394 allowedOrigins = os.getenv("VZ_QUERY_STORE_ALLOWED_ORIGINS") 1395 elseif backend == 'thanos-ruler' then 1396 allowedOrigins = os.getenv("VZ_THANOS_RULER_ALLOWED_ORIGINS") 1397 elseif backend == 'kibana' then 1398 allowedOrigins = os.getenv("VZ_KIBANA_ALLOWED_ORIGINS") 1399 elseif backend == 'osd' then 1400 allowedOrigins = os.getenv("VZ_KIBANA_ALLOWED_ORIGINS") 1401 elseif backend == 'elasticsearch' then 1402 allowedOrigins = os.getenv("VZ_ES_ALLOWED_ORIGINS") 1403 elseif backend == 'opensearch' then 1404 allowedOrigins = os.getenv("VZ_ES_ALLOWED_ORIGINS") 1405 elseif backend == 'opensearch-logging' then 1406 allowedOrigins = os.getenv("VZ_OS_LOGGING_ALLOWED_ORIGINS") 1407 elseif backend == 'osd-logging' then 1408 allowedOrigins = os.getenv("VZ_OSD_LOGGING_ALLOWED_ORIGINS") 1409 elseif backend == 'kiali' then 1410 allowedOrigins = os.getenv("VZ_KIALI_ALLOWED_ORIGINS") 1411 elseif backend == 'jaeger' then 1412 allowedOrigins = os.getenv("VZ_JAEGER_ALLOWED_ORIGINS") 1413 elseif backend == 'alertmanager' then 1414 allowedOrigins = os.getenv("VZ_ALERTMANAGER_ALLOWED_ORIGINS") 1415 end 1416 1417 if not allowedOrigins or allowedOrigins == "" then 1418 return false 1419 end 1420 1421 for requestOrigin in string.gmatch(origin, '([^ ]+)') do 1422 local originFound = false 1423 for allowedOrigin in string.gmatch(allowedOrigins, '([^,]+)') do 1424 if requestOrigin == allowedOrigin then 1425 originFound = true 1426 end 1427 end 1428 if originFound == false then 1429 return false 1430 end 1431 end 1432 return true 1433 end 1434 1435 -- Implementation of openidc_pem_from_rsa_n_and_e from lua-resty-openidc to generate public key from 1436 -- modulus and operand values of token 1437 function me.generate_key(n, e) 1438 me.debug("getting PEM public key from n and e parameters of json public key") 1439 local der_key = { 1440 unb64url(n), unb64url(e) 1441 } 1442 local encoded_key = me.encode_sequence_of_integer(der_key) 1443 local pem = me.der2pem(me.encode_sequence({ 1444 me.encode_sequence({ 1445 "\6\9\42\134\72\134\247\13\1\1\1" -- OID :rsaEncryption 1446 .. "\5\0" -- ASN.1 NULL of length 0 1447 }), 1448 me.encode_bit_string(encoded_key) 1449 }), "PUBLIC KEY") 1450 me.debug("Generated pem key from n and e: ", pem) 1451 return pem 1452 end 1453 1454 function me.encode_length(length) 1455 if length < 0x80 then 1456 return string.char(length) 1457 elseif length < 0x100 then 1458 return string.char(0x81, length) 1459 elseif length < 0x10000 then 1460 return string.char(0x82, math.floor(length / 0x100), length % 0x100) 1461 end 1462 me.error("Can't encode lengths over 65535") 1463 end 1464 1465 function me.encode_sequence(array, of) 1466 local encoded_array = array 1467 if of then 1468 encoded_array = {} 1469 for i = 1, #array do 1470 encoded_array[i] = of(array[i]) 1471 end 1472 end 1473 encoded_array = table.concat(encoded_array) 1474 1475 return string.char(0x30) .. me.encode_length(#encoded_array) .. encoded_array 1476 end 1477 1478 function me.encode_sequence_of_integer(array) 1479 return me.encode_sequence(array, me.encode_binary_integer) 1480 end 1481 1482 function me.der2pem(data, typ) 1483 local wrap = ('.'):rep(64) 1484 local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n" 1485 typ = typ:upper() or "CERTIFICATE" 1486 data = b64(data) 1487 return string.format(envelope, typ, data:gsub(wrap, '%0\n', (#data - 1) / 64), typ) 1488 end 1489 1490 function me.encode_bit_string(array) 1491 local s = "\0" .. array -- first octet holds the number of unused bits 1492 return "\3" .. me.encode_length(#s) .. s 1493 end 1494 1495 function me.encode_binary_integer(bytes) 1496 if bytes:byte(1) > 127 then 1497 -- We currenly only use this for unsigned integers, 1498 -- however since the high bit is set here, it would look 1499 -- like a negative signed int, so prefix with zeroes 1500 bytes = "\0" .. bytes 1501 end 1502 return "\2" .. me.encode_length(#bytes) .. bytes 1503 end 1504 1505 return me 1506 nginx.conf: | 1507 #user nobody; 1508 worker_processes 1; 1509 1510 error_log /var/log/nginx/error.log info; 1511 pid logs/nginx.pid; 1512 1513 env KUBERNETES_SERVICE_HOST; 1514 env KUBERNETES_SERVICE_PORT; 1515 env VZ_API_HOST; 1516 env VZ_API_VERSION; 1517 env VZ_API_ALLOWED_ORIGINS; 1518 env VZ_CONSOLE_ALLOWED_ORIGINS; 1519 env VZ_GRAFANA_ALLOWED_ORIGINS; 1520 env VZ_PROMETHEUS_ALLOWED_ORIGINS; 1521 env VZ_THANOS_QUERY_ALLOWED_ORIGINS; 1522 env VZ_QUERY_STORE_ALLOWED_ORIGINS; 1523 env VZ_THANOS_RULER_ALLOWED_ORIGINS; 1524 env VZ_KIBANA_ALLOWED_ORIGINS; 1525 env VZ_ES_ALLOWED_ORIGINS; 1526 env VZ_KIALI_ALLOWED_ORIGINS; 1527 env VZ_JAEGER_ALLOWED_ORIGINS; 1528 env VZ_OS_LOGGING_ALLOWED_ORIGINS; 1529 env VZ_OSD_LOGGING_ALLOWED_ORIGINS; 1530 env VZ_ALERTMANAGER_ALLOWED_ORIGINS; 1531 1532 events { 1533 worker_connections 1024; 1534 } 1535 1536 http { 1537 include mime.types; 1538 default_type application/octet-stream; 1539 1540 #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 1541 # '$status $body_bytes_sent "$http_referer" ' 1542 # '"$http_user_agent" "$http_x_forwarded_for"'; 1543 log_format json_combined escape=json '{' 1544 '"@timestamp": "$time_iso8601", ' # local time in the ISO 8601 standard format 1545 '"req_id": "$request_id", ' # the unique request id 1546 '"upstream_status": "$upstream_status", ' 1547 '"upstream_addr": "$upstream_addr", ' # upstream backend server for proxied requests 1548 '"message": "$request_method $http_host$request_uri", ' 1549 '"http_request": {' 1550 '"request_method": "$request_method", ' # request method 1551 '"requestUrl": "$http_host$request_uri", ' 1552 '"status": "$status", ' # response status code 1553 '"requestSize": "$request_length", ' # request length (including headers and body) 1554 '"responseSize": "$upstream_response_length", ' # upstream response length 1555 '"userAgent": "$http_user_agent", ' # user agent 1556 '"remoteIp": "$remote_addr", ' # client IP 1557 '"referer": "$http_referer", ' # HTTP referer 1558 '"latency": "$upstream_response_time s", ' # time spent receiving upstream body 1559 '"protocol": "$server_protocol" ' # request protocol, like HTTP/1.1 or HTTP/2.0 1560 '}' 1561 '}'; 1562 sendfile on; 1563 #tcp_nopush on; 1564 1565 # Posts from Fluentd can require more than the default 1m max body size 1566 client_max_body_size {{ .MaxRequestSize }}; 1567 proxy_buffer_size {{ .ProxyBufferSize }}; 1568 client_body_buffer_size 256k; 1569 1570 #keepalive_timeout 0; 1571 keepalive_timeout 65; 1572 1573 #gzip on; 1574 1575 lua_package_path '/usr/local/share/lua/5.1/?.lua;;'; 1576 lua_package_cpath '/usr/local/lib/lua/5.1/?.so;;'; 1577 resolver _NAMESERVER_; 1578 1579 # cache for discovery metadata documents 1580 lua_shared_dict discovery 1m; 1581 # cache for JWKs 1582 lua_shared_dict jwks 1m; 1583 1584 #access_log logs/host.access.log main; 1585 access_log /dev/stderr json_combined; 1586 server_tokens off; 1587 1588 #charset koi8-r; 1589 expires 0; 1590 #add_header Cache-Control private; 1591 add_header Cache-Control no-store always; 1592 1593 proxy_http_version 1.1; 1594 1595 # Verrazzano api and console 1596 server { 1597 listen 8775; 1598 server_name verrazzano-proxy; 1599 1600 # api 1601 location / { 1602 lua_ssl_verify_depth 2; 1603 lua_ssl_trusted_certificate /etc/nginx/upstream.pem; 1604 # oauth-proxy ssl certs location: lua_ssl_trusted_certificate /etc/nginx/all-ca-certs.pem; 1605 1606 set $oidc_user ""; 1607 set $oidc_user_roles ""; 1608 set $backend_server_url ""; 1609 rewrite_by_lua_file /etc/nginx/conf.lua; 1610 proxy_set_header X-WEBAUTH-USER $oidc_user; 1611 proxy_set_header x-proxy-roles $oidc_user_roles; 1612 proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for; 1613 proxy_pass $backend_server_url; 1614 proxy_ssl_trusted_certificate /etc/nginx/upstream.pem; 1615 } 1616 1617 location /nginx_status { 1618 stub_status; 1619 allow 127.0.0.1; 1620 deny all; 1621 } 1622 } 1623 1624 # Verrazzano gRPC proxy 1625 server { 1626 listen 8776 http2; 1627 server_name verrazzano-grpc-proxy; 1628 1629 location / { 1630 lua_ssl_verify_depth 2; 1631 lua_ssl_trusted_certificate /etc/nginx/upstream.pem; 1632 1633 set $oidc_user ""; 1634 set $backend_server_url ""; 1635 rewrite_by_lua_file /etc/nginx/conf.lua; 1636 proxy_set_header X-WEBAUTH-USER $oidc_user; 1637 grpc_pass $backend_server_url; 1638 proxy_ssl_trusted_certificate /etc/nginx/upstream.pem; 1639 } 1640 } 1641 } 1642 startup.sh: | 1643 #!/bin/bash 1644 startupDir=$(dirname $0) 1645 cd $startupDir 1646 cp $startupDir/nginx.conf /etc/nginx/nginx.conf 1647 cp $startupDir/auth.lua /etc/nginx/auth.lua 1648 cp $startupDir/conf.lua /etc/nginx/conf.lua 1649 nameserver=$(grep -i nameserver /etc/resolv.conf | awk '{split($0,line," "); print line[2]}') 1650 sed -i -e "s|_NAMESERVER_|${nameserver}|g" /etc/nginx/nginx.conf 1651 1652 mkdir -p /etc/nginx/logs 1653 1654 export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH 1655 1656 cat /etc/ssl/certs/ca-bundle.crt > /etc/nginx/upstream.pem 1657 1658 /usr/local/nginx/sbin/nginx -c /etc/nginx/nginx.conf -p /etc/nginx -t 1659 /usr/local/nginx/sbin/nginx -c /etc/nginx/nginx.conf -p /etc/nginx 1660 1661 while [ $? -ne 0 ]; do 1662 sleep 20 1663 echo "retry nginx startup ..." 1664 /usr/local/nginx/sbin/nginx -c /etc/nginx/nginx.conf -p /etc/nginx 1665 done 1666 1667 sh -c "$startupDir/reload.sh &" 1668 1669 tail -f /var/log/nginx/error.log 1670 reload.sh: | 1671 #!/bin/bash 1672 1673 adminCABundleMD5="" 1674 defaultCABundleMD5="" 1675 upstreamCACertFile="/etc/nginx/upstream.pem" 1676 localClusterCACertFile="/api-config/default-ca-bundle" 1677 adminClusterCACertFile="/api-config/admin-ca-bundle" 1678 defaultCACertFile="/etc/ssl/certs/ca-bundle.crt" 1679 tmpUpstreamCACertFile="/tmp/upstream.pem" 1680 maxSizeTrustedCertsFileDefault=$(echo $((10*1024*1024))) 1681 if [[ ! -z "${MAX_SIZE_TRUSTED_CERTS_FILE}" ]]; then 1682 maxSizeTrustedCertsFileDefault=${MAX_SIZE_TRUSTED_CERTS_FILE} 1683 fi 1684 1685 function reload() { 1686 nginx -t -p /etc/nginx 1687 if [ $? -eq 0 ] 1688 then 1689 echo "Detected Nginx Configuration Change" 1690 echo "Executing: nginx -s reload -p /etc/nginx" 1691 nginx -s reload -p /etc/nginx 1692 fi 1693 } 1694 1695 function reset_md5() { 1696 adminCABundleMD5="" 1697 defaultCABundleMD5="" 1698 } 1699 1700 function local_cert_config() { 1701 if [[ -s $localClusterCACertFile ]]; then 1702 md5Hash=$(md5sum "$localClusterCACertFile") 1703 if [ "$defaultCABundleMD5" != "$md5Hash" ] ; then 1704 echo "Adding local CA cert to $upstreamCACertFile" 1705 cat $upstreamCACertFile > $tmpUpstreamCACertFile 1706 cat $localClusterCACertFile > $upstreamCACertFile 1707 cat $tmpUpstreamCACertFile >> $upstreamCACertFile 1708 rm -rf $tmpUpstreamCACertFile 1709 defaultCABundleMD5="$md5Hash" 1710 reload 1711 fi 1712 fi 1713 } 1714 1715 function admin_cluster_cert_config() { 1716 if [[ -s $adminClusterCACertFile ]]; then 1717 md5Hash=$(md5sum "$adminClusterCACertFile") 1718 if [ "$adminCABundleMD5" != "$md5Hash" ] ; then 1719 echo "Adding admin cluster CA cert to $upstreamCACertFile" 1720 cat $upstreamCACertFile > $tmpUpstreamCACertFile 1721 cat $adminClusterCACertFile > $upstreamCACertFile 1722 cat $tmpUpstreamCACertFile >> $upstreamCACertFile 1723 rm -rf $tmpUpstreamCACertFile 1724 adminCABundleMD5="$md5Hash" 1725 reload 1726 fi 1727 else 1728 if [ "$adminCABundleMD5" != "" ] ; then 1729 reset_md5 1730 local_cert_config 1731 fi 1732 fi 1733 } 1734 1735 function default_cert_config() { 1736 cat $defaultCACertFile > $upstreamCACertFile 1737 } 1738 1739 while true 1740 do 1741 trustedCertsFileSize=$(wc -c < $upstreamCACertFile) 1742 if [ $trustedCertsFileSize -ge $maxSizeTrustedCertsFileDefault ] ; then 1743 echo "$upstreamCACertFile file size greater than $maxSizeTrustedCertsFileDefault, resetting.." 1744 reset_md5 1745 default_cert_config 1746 fi 1747 1748 local_cert_config 1749 admin_cluster_cert_config 1750 sleep .1 1751 done 1752 {{ end }} 1753