Skip to content

Commit

Permalink
Release v0.6.13 (#64)
Browse files Browse the repository at this point in the history
* Add GraphQL support

* Add documentation content
  • Loading branch information
afr1ka authored and Nikolay Tkachenko committed Sep 8, 2023
1 parent 66fe7fc commit 421fb1a
Show file tree
Hide file tree
Showing 62 changed files with 4,694 additions and 545 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
needs:
- draft-release
env:
X_GO_VERSION: "1.20.7-r0"
X_GO_VERSION: "1.20.8-r0"
strategy:
matrix:
include:
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ genmocks:
mockgen -source ./internal/platform/proxy/chainpool.go -destination ./internal/platform/proxy/httppool_mock.go -package proxy
mockgen -source ./internal/platform/database/database.go -destination ./internal/platform/database/database_mock.go -package database
mockgen -source ./cmd/api-firewall/internal/updater/updater.go -destination ./cmd/api-firewall/internal/updater/updater_mock.go -package updater
mockgen -source ./internal/platform/proxy/ws.go -destination ./internal/platform/proxy/ws_mock.go -package proxy
mockgen -source ./internal/platform/proxy/wsClient.go -destination ./internal/platform/proxy/wsClient_mock.go -package proxy

update:
go get -u ./...
Expand Down
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,57 @@
# Open Source API Firewall by Wallarm [![Black Hat Arsenal USA 2022](https://github.com/wallarm/api-firewall/blob/main/images/BHA2022.svg?raw=true)](https://www.blackhat.com/us-22/arsenal/schedule/index.html#open-source-api-firewall-new-features--functionalities-28038)

API Firewall is a high-performance proxy with API request and response validation based on [OpenAPI/Swagger](https://www.wallarm.com/what/what-is-openapi) schema. It is designed to protect REST API endpoints in cloud-native environments. API Firewall provides API hardening with the use of a positive security model allowing calls that match a predefined API specification for requests and responses, while rejecting everything else.
API Firewall is a high-performance proxy with API request and response validation based on [OpenAPI](https://wallarm.github.io/api-firewall/installation-guides/docker-container/) and [GraphQL](https://wallarm.github.io/api-firewall/installation-guides/graphql/docker-container/) schemas. It is designed to protect REST and GraphQL API endpoints in cloud-native environments. API Firewall provides API hardening with the use of a positive security model allowing calls that match a predefined API specification for requests and responses, while rejecting everything else.

The **key features** of API Firewall are:

* Secure REST API endpoints by blocking malicious requests
* Secure REST and GraphQL API endpoints by blocking malicious requests
* Stop API data breaches by blocking malformed API responses
* Discover Shadow API endpoints
* Validate JWT access tokens for OAuth 2.0 protocol-based authentication
* (NEW) Denylist compromised API tokens, keys, and Cookies
* Denylist compromised API tokens, keys, and Cookies

The product is **open source**, available at DockerHub and already got 1 billion (!!!) pulls. To support this project, you can star the [repository](https://hub.docker.com/r/wallarm/api-firewall).

## Operating modes

Wallarm API Firewall offers several operating modes:

* [`PROXY`](https://wallarm.github.io/api-firewall/installation-guides/docker-container/): validates HTTP requests and responses against OpenAPI 3.0 and proxies matching requests to the backend.
* [`API`](https://wallarm.github.io/api-firewall/installation-guides/api-mode/): validates individual requests against OpenAPI 3.0 without further proxying.
* [`graphql`](https://wallarm.github.io/api-firewall/installation-guides/graphql/docker-container/): validates HTTP and WebSocket requests against GraphQL schema and proxies matching requests to the backend.

## Use cases

### Running in blocking mode
* Block malicious requests that do not match the OpenAPI 3.0 specification

* Block malicious requests that do not match the specification
* Block malformed API responses to stop data breaches and sensitive information exposure

### Running in monitoring mode

* Discover Shadow APIs and undocumented API endpoints
* Log malformed requests and responses that do not match the OpenAPI 3.0 specification
* Log malformed requests and responses that do not match the specification

## API schema validation and positive security model

When starting API Firewall, you should provide the [OpenAPI 3.0 specification](https://swagger.io/specification/) of the application to be protected with API Firewall. The started API Firewall will operate as a reverse proxy and validate whether requests and responses match the schema defined in the specification.
When starting API Firewall, you should provide the REST or GraphQL API specification of the application to be protected with API Firewall. The started API Firewall will operate as a reverse proxy and validate whether requests and responses match the schema defined in the specification.

The traffic that does not match the schema will be logged using the [`STDOUT` and `STDERR` Docker services](https://docs.docker.com/config/containers/logging/) or blocked (depending on the configured API Firewall operation mode). When operating in the logging mode, API Firewall also logs so-called shadow API endpoints, those that are not covered in API specification but respond to requests (except for endpoints returning the code `404`).
The traffic that does not match the schema will be logged using the [`STDOUT` and `STDERR` Docker services](https://docs.docker.com/config/containers/logging/) or blocked (depending on the configured API Firewall operation mode). When operating in the logging mode on REST API, API Firewall also logs so-called shadow API endpoints, those that are not covered in API specification but respond to requests (except for endpoints returning the code `404`).

![API Firewall scheme](https://github.com/wallarm/api-firewall/blob/main/images/Firewall%20opensource%20-%20vertical.gif?raw=true)

[OpenAPI 3.0 specification](https://swagger.io/specification/) is supported and should be provided as a YAML or JSON file (`.yaml`, `.yml`, `.json` file extensions).

By allowing you to set the traffic requirements with the OpenAPI 3.0 specification, API Firewall relies on a positive security model.
By allowing you to set the traffic requirements with the API specification, API Firewall relies on a positive security model.

## Technical data

[API Firewall works](https://www.wallarm.com/what/the-concept-of-a-firewall) as a reverse proxy with a built-in OpenAPI 3.0 request and response validator. It's written in Golang and using fasthttp proxy. The project is optimized for extreme performance and near-zero added latency.
[API Firewall works](https://www.wallarm.com/what/the-concept-of-a-firewall) as a reverse proxy with a built-in OpenAPI 3.0 or GraphQL request and response validator. It is written in Golang and using fasthttp proxy. The project is optimized for extreme performance and near-zero added latency.

## Starting API Firewall

To download, install, and start API Firewall on Docker, see the [instructions](https://docs.wallarm.com/api-firewall/installation-guides/docker-container/).
To download, install, and start API Firewall on Docker, refer to:

* [REST API guide](https://wallarm.github.io/api-firewall/installation-guides/docker-container/)
* [GraphQL API guide](https://wallarm.github.io/api-firewall/installation-guides/graphql/docker-container/)

## Demos

Expand Down Expand Up @@ -78,4 +89,3 @@ Time per request: 0.127 [ms] (mean, across all concurrent requests)
```

These performance results are not the only ones we have got during API Firewall testing. Other results along with the methods used to improve API Firewall performance are described in this [Wallarm's blog article](https://lab.wallarm.com/wallarm-api-firewall-outperforms-nginx-in-a-production-environment/).

2 changes: 1 addition & 1 deletion cmd/api-firewall/internal/handlers/api/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ type APIMode struct {
CustomRoute *router.CustomRoute
OpenAPIRouter *router.Router
Log *logrus.Logger
Cfg *config.APIFWConfigurationAPIMode
Cfg *config.APIMode
ParserPool *fastjson.ParserPool
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/api-firewall/internal/handlers/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/wallarm/api-firewall/internal/platform/web"
)

func Handlers(lock *sync.RWMutex, cfg *config.APIFWConfigurationAPIMode, shutdown chan os.Signal, logger *logrus.Logger, storedSpecs database.DBOpenAPILoader) fasthttp.RequestHandler {
func Handlers(lock *sync.RWMutex, cfg *config.APIMode, shutdown chan os.Signal, logger *logrus.Logger, storedSpecs database.DBOpenAPILoader) fasthttp.RequestHandler {

// define FastJSON parsers pool
var parserPool fastjson.ParserPool
Expand Down
137 changes: 137 additions & 0 deletions cmd/api-firewall/internal/handlers/graphql/httpHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package graphql

import (
"bytes"
"errors"
"fmt"
"net/url"
"strings"
"sync"

"github.com/fasthttp/websocket"
"github.com/savsgio/gotils/strconv"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
"github.com/wallarm/api-firewall/internal/config"
"github.com/wallarm/api-firewall/internal/platform/proxy"
"github.com/wallarm/api-firewall/internal/platform/validator"
"github.com/wallarm/api-firewall/internal/platform/web"
"github.com/wundergraph/graphql-go-tools/pkg/graphql"
)

type Handler struct {
cfg *config.GraphQLMode
serverURL *url.URL
proxyPool proxy.Pool
logger *logrus.Logger
schema *graphql.Schema
parserPool *fastjson.ParserPool
wsClient proxy.WebSocketClient
upgrader *websocket.FastHTTPUpgrader
mu sync.Mutex
}

var ErrNetworkConnection = errors.New("network connection error")

// GraphQLHandle performs complexity checks to the GraphQL query and proxy request to the backend if all checks are passed
func (h *Handler) GraphQLHandle(ctx *fasthttp.RequestCtx) error {

// handle WS
if websocket.FastHTTPIsWebSocketUpgrade(ctx) {
return h.HandleWebSocketProxy(ctx)
}

// respond with 403 status code in case of content-type of POST request is not application/json
if strconv.B2S(ctx.Request.Header.Method()) == fasthttp.MethodPost &&
!bytes.EqualFold(ctx.Request.Header.ContentType(), []byte("application/json")) {
h.logger.WithFields(logrus.Fields{
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Debug("POST request without application/json content-type is received")

return web.RespondError(ctx, fasthttp.StatusForbidden, "")
}

// respond with 403 status code in case of lack of "query" query parameter in GET request
if strconv.B2S(ctx.Request.Header.Method()) == fasthttp.MethodGet &&
len(ctx.Request.URI().QueryArgs().Peek("query")) == 0 {
h.logger.WithFields(logrus.Fields{
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Debug("GET request without \"query\" query parameter is received")

return web.RespondError(ctx, fasthttp.StatusForbidden, "")
}

// Proxy request if the validation is disabled
if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationDisable) {
if err := proxy.Perform(ctx, h.proxyPool); err != nil {
h.logger.WithFields(logrus.Fields{
"error": err,
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Error("request proxying")

ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
return web.RespondGraphQLErrors(&ctx.Response, ErrNetworkConnection)
}
return nil
}

gqlRequest, err := validator.ParseGraphQLRequest(ctx)
if err != nil {
h.logger.WithFields(logrus.Fields{
"error": err,
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Error("GraphQL request unmarshal")

if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) {
return web.RespondGraphQLErrors(&ctx.Response, err)
}
}

// validate request
if gqlRequest != nil {
validationResult, err := validator.ValidateGraphQLRequest(&h.cfg.Graphql, h.schema, gqlRequest)
// internal errors
if err != nil {
h.logger.WithFields(logrus.Fields{
"error": err,
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Error("GraphQL query validation")

if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) {
return web.RespondGraphQLErrors(&ctx.Response, err)
}
}

// validation failed
if !validationResult.Valid && validationResult.Errors != nil {
h.logger.WithFields(logrus.Fields{
"error": validationResult.Errors,
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Error("GraphQL query validation")

if strings.EqualFold(h.cfg.Graphql.RequestValidation, web.ValidationBlock) {
return web.RespondGraphQLErrors(&ctx.Response, validationResult.Errors)
}
}
}

if err := proxy.Perform(ctx, h.proxyPool); err != nil {
h.logger.WithFields(logrus.Fields{
"error": err,
"protocol": "HTTP",
"request_id": fmt.Sprintf("#%016X", ctx.ID()),
}).Error("request proxying")

ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
return web.RespondGraphQLErrors(&ctx.Response, ErrNetworkConnection)
}

return nil
}
101 changes: 101 additions & 0 deletions cmd/api-firewall/internal/handlers/graphql/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package graphql

import (
"github.com/savsgio/gotils/strconv"
"github.com/savsgio/gotils/strings"
"net/url"
"os"
"sync"

"github.com/fasthttp/websocket"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
"github.com/wallarm/api-firewall/internal/config"
"github.com/wallarm/api-firewall/internal/mid"
"github.com/wallarm/api-firewall/internal/platform/denylist"
"github.com/wallarm/api-firewall/internal/platform/proxy"
"github.com/wallarm/api-firewall/internal/platform/web"
"github.com/wundergraph/graphql-go-tools/pkg/graphql"
"github.com/wundergraph/graphql-go-tools/pkg/playground"
)

func Handlers(cfg *config.GraphQLMode, schema *graphql.Schema, serverURL *url.URL, shutdown chan os.Signal, logger *logrus.Logger, proxy proxy.Pool, wsClient proxy.WebSocketClient, deniedTokens *denylist.DeniedTokens) fasthttp.RequestHandler {

// Construct the web.App which holds all routes as well as common Middleware.
appOptions := web.AppAdditionalOptions{
Mode: cfg.Mode,
PassOptions: false,
}

proxyOptions := mid.ProxyOptions{
Mode: web.GraphQLMode,
RequestValidation: cfg.Graphql.RequestValidation,
DeleteAcceptEncoding: cfg.Server.DeleteAcceptEncoding,
ServerURL: serverURL,
}

denylistOptions := mid.DenylistOptions{
Mode: web.GraphQLMode,
Config: &cfg.Denylist,
CustomBlockStatusCode: fasthttp.StatusUnauthorized,
DeniedTokens: deniedTokens,
Logger: logger,
}
app := web.NewApp(&appOptions, shutdown, logger, mid.Logger(logger), mid.Errors(logger), mid.Panics(logger), mid.Proxy(&proxyOptions), mid.Denylist(&denylistOptions))

// define FastJSON parsers pool
var parserPool fastjson.ParserPool

var upgrader = websocket.FastHTTPUpgrader{
Subprotocols: []string{"graphql-ws"},
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
if !cfg.Graphql.WSCheckOrigin {
return true
}
return strings.Include(cfg.Graphql.WSOrigin, strconv.B2S(ctx.Request.Header.Peek("Origin")))
},
}

s := Handler{
cfg: cfg,
serverURL: serverURL,
proxyPool: proxy,
logger: logger,
schema: schema,
parserPool: &parserPool,
wsClient: wsClient,
upgrader: &upgrader,
mu: sync.Mutex{},
}

graphqlPath := serverURL.Path
if graphqlPath == "" {
graphqlPath = "/"
}

app.Handle(fasthttp.MethodGet, graphqlPath, s.GraphQLHandle)
app.Handle(fasthttp.MethodPost, graphqlPath, s.GraphQLHandle)

// enable playground
if cfg.Graphql.Playground {
p := playground.New(playground.Config{
PathPrefix: "",
PlaygroundPath: cfg.Graphql.PlaygroundPath,
GraphqlEndpointPath: graphqlPath,
GraphQLSubscriptionEndpointPath: graphqlPath,
})

handlers, err := p.Handlers()
if err != nil {
logger.Fatalf("playground handlers error: %v", err)
return nil
}

for i := range handlers {
app.Handle(fasthttp.MethodGet, handlers[i].Path, web.NewFastHTTPHandler(handlers[i].Handler, true))
}
}

return app.Router.Handler
}
Loading

0 comments on commit 421fb1a

Please sign in to comment.