apiVersion: consul.hashicorp.com/v1alpha1 kind: ServiceDefaults metadata: name: api-gateway namespace: consul spec: protocol: http envoyExtensions: - name: builtin/lua required: true arguments: proxyType: "api-gateway" listener: "outbound" mutualTLSMode: "" script: | -- Appsentinels Envoy Filter in Lua -- Needed for visibility, pls update this to your filter instance name local INSTANCE_NAME = "consul-api-gateway" -- End of visibility -- Config section start local ENFORCEMENT = false local AUTHZ_TIMEOUT = 100 local ACCESSLOG_TIMEOUT = 100 local FAILOPEN = true local CONTROLLER_CLUSTER_NAME = "ext_authz-http-service" local SKIP_METHODS = {"HEAD", "OPTIONS"} local LOGGING_CONTENT_TYPES = {"json", "form", "xml", "graphql"} local MAX_PAYLOAD_SIZE = 131072 --- 128k local AS_DEBUG = false -- Config section end local HOSTNAME = os.getenv("HOSTNAME") -- Helper functions start -- function _bypass_method(method) if method == nil then return true end for _, v in ipairs(SKIP_METHODS) do --- do a substring match to check if v is in method --- --- This is a safe way to check if v is in SKIP_METHODS --- if string.find(v, method) then return true end end return false end function _valid_content_type(content_type) if content_type == nil then return true end for _, v in ipairs(LOGGING_CONTENT_TYPES) do if string.find(content_type, v) then return true end end return false end function _content_too_large(payload_size) if payload_size == nil then return false end if tonumber(payload_size) >= MAX_PAYLOAD_SIZE then return true else return false end end -- Helper functions end -- function appsentinels_on_request(request_handle) request_handle:logErr("sagarrrrrrrrrrrrrr In request: ") -- Create a new table to store headers for httpCall local httpCallHeaders = {} -- Get the HTTP method and URI from the request headers local method = request_handle:headers():get(":method") if _bypass_method(method) then if AS_DEBUG then request_handle:logInfo("as: bypassing unsupported method: " .. method) end -- Pass this message over to response handler so it can bypass this log as well request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.skip", true) return end request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.method", method) -- Get all headers from request_handle for name, value in pairs(request_handle:headers()) do httpCallHeaders[name] = value end httpCallHeaders["X-As-Error"] = "" -- Pass some info must for resp via metadata local metadata = request_handle:streamInfo():dynamicMetadata() local rid = httpCallHeaders["x-request-id"] if rid ~= nil then request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.x-request-id", rid) else httpCallHeaders["X-As-Error"] = "Missing X-Request-Id;" end local path = request_handle:headers():get(":path") if path ~= nil then request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.path", path) else httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing path;" end local host = request_handle:headers():get(":authority") if host ~= nil then request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.authority", host) else httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing host;" end local _start_time = request_handle:timestamp(EnvoyTimestampResolution.MILLISECOND) httpCallHeaders["X-As-Req-Timestamp"] = _start_time request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.req-time", _start_time) -- No error case if httpCallHeaders["X-As-Error"] == "" then -- delete this key httpCallHeaders["X-As-Error"] = nil end httpCallHeaders["connection"] = nil httpCallHeaders["keep-alive"] = nil -- Cannot send transfer-encoding header, this is relevant only for upstream to downstream connection if httpCallHeaders["transfer-encoding"] ~= nil then httpCallHeaders["X-As-TE"] = httpCallHeaders["transfer-encoding"] httpCallHeaders["transfer-encoding"] = nil end local request_body = "" local _req_payload_size = request_handle:headers():get("content-length") if _content_too_large(_req_payload_size) or not _valid_content_type(httpCallHeaders["content-type"]) then if AS_DEBUG then request_handle:logInfo("as: request body too large or unsupported content type") end -- Setting this will ensure edge controller understands the payload being too large httpCallHeaders["x-as-req-truncated-body"] = "true" -- pass the actual content length if possible if _req_payload_size ~= nil then httpCallHeaders["x-as-content-length"] = _req_payload_size end else -- Due to https://github.com/envoyproxy/envoy/issues/8785 request_handle:bodyChunks() -- cause probles in enforcement, therefore, reading the complete body in enforcement -- mode (Please note that it will result into buffering of the body in memory) if ENFORCEMENT then local body_obj = request_handle:body() if body_obj and body_obj:length() > 0 then if body_obj:length() > MAX_PAYLOAD_SIZE then if AS_DEBUG then request_handle:logInfo("as: request body too large") end httpCallHeaders["x-as-req-truncated-body"] = "true" else request_body = body_obj:getBytes(0, body_obj:length()) end end else local _total_len = 0 local _body_chunks = {} for chunk in request_handle:bodyChunks() do local len = chunk:length() if len > 0 then local result = chunk:getBytes(0, len) table.insert(_body_chunks, result) _total_len = _total_len + len end if _total_len > MAX_PAYLOAD_SIZE then if AS_DEBUG then request_handle:logInfo("as: request body too large") end _body_chunks = {} httpCallHeaders["x-as-req-truncated-body"] = "true" break end end if _total_len > 0 and #_body_chunks > 0 then request_body = table.concat(_body_chunks) end end end -- dont do asynch for enforcement mode local asynchronous = not ENFORCEMENT -- https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter local authz_headers, authz_body = request_handle:httpCall( "controller.default.dc1.internal.85c3ce6d-cc9e-db03-8944-2733bef37e23.consul", httpCallHeaders, request_body, AUTHZ_TIMEOUT, asynchronous) if ENFORCEMENT then local authz_status = authz_headers[":status"] authz_status = tonumber(authz_status) if FAILOPEN then -- 4xx for failopen case only if authz_status and (authz_status > 399 and authz_status < 500) then request_handle:respond(authz_headers, authz_body) return end else if authz_status and (authz_status > 399 and authz_status < 600) then request_handle:respond(authz_headers, authz_body) return end end end -- end of function appsentinels_on_request end local function append_query_param(existing_path, param_name, param_value) if string.find(existing_path, "?") then -- If there's already a query, append with & return string.format("%s&%s=%s", existing_path, param_name, param_value) else -- If there's no query yet, start with ? return string.format("%s?%s=%s", existing_path, param_name, param_value) end end function appsentinels_on_response(response_handle) response_handle:logErr("sagarrrrrrrrrrrrrr In response: ") local _skip = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.skip"] if _skip ~= nil and _skip == true then if AS_DEBUG then response_handle:logDebug("as: Skipping resp logging, because req bypassed") end return end -- Create a new table to store headers for httpCall local httpCallHeaders = {} -- Get all headers from response_handle for name, value in pairs(response_handle:headers()) do -- skip headers starting with : , example :status if string.sub(name, 1, 1) == ":" then -- response_handle:logErr("as: resp header not logging " .. name .. ": " .. value) else httpCallHeaders[name] = value -- response_handle:logErr("as: resp header name " .. name .. ": " .. value) end end local rid = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.x-request-id"] if rid ~= nil then httpCallHeaders["x-request-id"] = rid else if AS_DEBUG then response_handle:logErr("as: Resp: Missing x-request-id") end end -- For httpCall to work, we need :method, :path and "authority are must", in error cases, we must populate something httpCallHeaders["X-As-Error"] = "" local method = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.method"] if method ~= nil then httpCallHeaders[":method"] = method else httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing method;" end local path = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.path"] if path ~= nil then -- path can contain query local full_path = append_query_param(path, "sensor", "envoy-lua") full_path = append_query_param(full_path, "cap", "nohb") httpCallHeaders[":path"] = full_path else httpCallHeaders[":path"] = "/" httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing path;" end local host = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.authority"] if host ~= nil then httpCallHeaders[":authority"] = host else httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing host;" end httpCallHeaders["X-As-Req-Timestamp"] = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.req-time"] httpCallHeaders["X-As-Resp-Timestamp"] = response_handle:timestamp(EnvoyTimestampResolution.MILLISECOND) local status = response_handle:headers():get(":status") if status ~= nil then httpCallHeaders["X-As-Status"] = status else httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing status;" -- What else to put here? httpCallHeaders["X-As-Status"] = "200" end httpCallHeaders["X-As-Sensor"] = "envoy-http-access" httpCallHeaders["X-As-Sensor-Instance"] = INSTANCE_NAME httpCallHeaders["X-As-Sensor-Hostname"] = HOSTNAME -- No error case if httpCallHeaders["X-As-Error"] == "" then -- delete this key httpCallHeaders["X-As-Error"] = nil end httpCallHeaders["connection"] = nil httpCallHeaders["keep-alive"] = nil -- Cannot send transfer-encoding header, this is relevant only for upstream to downstream connection if httpCallHeaders["transfer-encoding"] ~= nil then httpCallHeaders["X-As-TE"] = httpCallHeaders["transfer-encoding"] httpCallHeaders["transfer-encoding"] = nil end -- The issue https://github.com/envoyproxy/envoy/issues/8785 does impact reading response -- body chunks, a synchronous call is not needed in this case, so not buffering the body -- in memory local _body_chunks = {} local _resp_payload_size = response_handle:headers():get("content-length") if _content_too_large(_resp_payload_size) or not _valid_content_type(httpCallHeaders["content-type"]) then if AS_DEBUG then response_handle:logInfo("as: resp body too large or unsupported content type") end httpCallHeaders["x-as-resp-truncated-body"] = "true" -- pass the actual content length if possible if _resp_payload_size ~= nil then httpCallHeaders["x-as-content-length"] = _resp_payload_size end else local _total_len = 0 for chunk in response_handle:bodyChunks() do local len = chunk:length() if len > 0 then local result = chunk:getBytes(0, len) table.insert(_body_chunks, result) _total_len = _total_len + len end if _total_len > MAX_PAYLOAD_SIZE then if AS_DEBUG then response_handle:logInfo("as: resp body too large") end _body_chunks = {} httpCallHeaders["x-as-resp-truncated-body"] = "true" break end end end local authz_headers, authz_body = response_handle:httpCall( "controller.default.dc1.internal.85c3ce6d-cc9e-db03-8944-2733bef37e23.consul", httpCallHeaders, table.concat(_body_chunks), ACCESSLOG_TIMEOUT, true) end function envoy_on_request(request_handle) appsentinels_on_request(request_handle) end function envoy_on_response(response_handle) appsentinels_on_response(response_handle) end