Skip to content

Commit

Permalink
Merge pull request #1 from build-security/build-and-doc
Browse files Browse the repository at this point in the history
Add build files, policy code and documentation
  • Loading branch information
yashtewari authored Mar 8, 2021
2 parents 80bf5ee + ba14802 commit 980f6db
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 1 deletion.
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Build go executable
FROM amd64/golang:1.15.3-alpine3.12 as build-env
RUN apk add --no-cache git make curl

WORKDIR /app
COPY . .
ARG MAXMIND_LICENSE_KEY
RUN make fetch-assets
RUN GOFLAGS=-mod=vendor make build

# Run executable
FROM alpine:3.12 as run-env
WORKDIR /app

COPY --from=build-env /app/api_gw_pdp .

ENV PDP_LOG_LEVEL=error

CMD ["sh", "-c", "./api_gw_pdp run --server --log-level ${PDP_LOG_LEVEL} --skip-version-check"]
95 changes: 95 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2018 The OPA Authors. All rights reserved.
# Use of this source code is governed by an Apache2
# license that can be found in the LICENSE file.

VERSION := 0.1.0

PACKAGES := $(shell go list ./.../ | grep -v 'vendor')

GO := go
DISABLE_CGO := CGO_ENABLED=0

BIN := api_gw_pdp

REPOSITORY := buildsecurity
IMAGE := $(REPOSITORY)/api-gw-pdp

ASSETS = assets
ASSETS_LIBRARY = $(ASSETS)/statik
MAXMIND_DB_TAR := $(ASSETS)/db.tar.gz
MAXMIND_DB_DIR := $(ASSETS)/GeoLite2-City_*
MAXMIND_DB_FILE_SRC := $(MAXMIND_DB_DIR)/GeoLite2-City.mmdb
MAXMIND_DB_FILE_DST := $(ASSETS)/geolite2-city.mmdb

.PHONY: all build clean check check-fmt check-vet check-lint \
generate vendor image push test version

######################################################
#
# Development targets
#
######################################################

all: build test check

version:
@echo $(VERSION)

check-env:
ifndef MAXMIND_LICENSE_KEY
$(error environment variable MAXMIND_LICENSE_KEY is required)
endif

generate:
$(GO) generate ./...

update:
$(GO) get -u

tidy: update
$(GO) mod tidy

vendor: tidy
$(GO) mod vendor

fetch-assets: check-env
mkdir -p $(ASSETS)
curl --silent --location --request GET \
'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz' \
--output $(MAXMIND_DB_TAR)
tar xvf $(MAXMIND_DB_TAR) -C $(ASSETS)
mv $(MAXMIND_DB_FILE_SRC) $(MAXMIND_DB_FILE_DST)
rm -rf $(MAXMIND_DB_TAR) $(MAXMIND_DB_DIR)
GO111MODULE=off go get github.com/rakyll/statik
statik -src=$(ASSETS) -dest=$(ASSETS)

clean-assets:
rm -rf $(ASSETS)/*

build:
$(GO) build -o $(BIN) ./main.go

image: check-env
docker build --build-arg MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY} -t $(IMAGE):$(VERSION) .

push:
docker push $(IMAGE):$(VERSION)

test: generate clean-assets fetch-assets
$(DISABLE_CGO) $(GO) test -v -bench=. $(PACKAGES)

clean: clean-assets
rm -f .Dockerfile_*
rm -f opa_*_*
rm -f *.so

check: check-fmt check-vet check-lint

check-fmt:
./build/check-fmt.sh

check-vet:
./build/check-vet.sh

check-lint:
./build/check-lint.sh
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,66 @@
# aws-api-gateway-authz
# aws-api-gateway-authz

This package showcases an example of how Open Policy Agent (OPA) can be used as a Policy Decision Point (PDP) to provide featureful access control.

This package contains:

- An OPA enhancement that adds [two custom builtins functions](./builtins) to create our PDP
- [Policy code](./policy) that defines our access control

## Builtins

We've implemented two new builtin functions.
#### - `build.geo_from_ip(ip_address)`

Returns a detailed geolocation object for the given `ip_address` using the [Maxmind GeoLite2 Database](https://dev.maxmind.com/geoip/geoip2/geolite2/).

We can define access control based on geolocation using this builtin.

In the [example](./policy), we use it to check where a request to our AWS API Gateway endpoint is coming from, and allow/deny access based on this information.

#### - `build.rate_limit(key, limit)`

For a predefined `RATE_LIMITER_DURATION`, returns `false` for the the first `limit` times it is called within the duration. Returns `true` if it has been called more than `limit` times within the given duration.

This builtin provides a flexible way to implement rate-limiting on any operation. It needs to be connected to a [Redis](https://redis.io/) server: you can set it up yourself, or use solutions like [AWS ElastiCache](https://aws.amazon.com/elasticache/) (managed Redis).

Because it uses shared memory, this function is safe for use across multiple PDPs. If they are connected to the same Redis server, we can expect the results to be consistent for the given `key` and `limit` across all PDPs.

In the [example](./policy), we use it to rate-limit requests made to our AWS API Gateway endpoint.
## Start up the PDP

After [setting up Redis](https://redis.io/topics/quickstart), you can use our Docker image to run the PDP:

```
docker pull buildsecurity/api-gw-pdp
docker exec \
-e RATE_LIMITER_REDIS_ENDPOINT=<your Redis endpoint> \
-e RATE_LIMITER_REDIS_PASSWORD=<your Redis password, if you've set one> \
-e RATE_LIMITER_DURATION=<the duration basis for rate-limiting> \
-p 8181:8181 \
--name pdp
buildsecurity/api-gw-pdp
```

## Try the builtins using the CLI

After starting the PDP as described above, on a separate terminal, run

```
docker exec -it pdp ./api_gw_pdp run
```

You are now in OPA interactive mode. Try, for example,

```
build.geo_from_ip("8.8.8.8")
```

## Build from scratch

The build downloads Maxmind geolocation assets and packages them into the PDP. To build from scratch, you need to [create a MaxMind account](https://www.maxmind.com/en/geolite2/signup) and [generate a license key](https://www.maxmind.com/en/accounts/current/license-key).

Then run
```
MAXMIND_LICENSE_KEY=<your license> make fetch-assets && make build
```
3 changes: 3 additions & 0 deletions hooks/build
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

docker build --build-arg MAXMIND_LICENSE_KEY=${MAXMIND_LICENSE_KEY} -t ${DOCKER_REPO}:${DOCKER_TAG} .
97 changes: 97 additions & 0 deletions policy/pdp.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package aws.apigw.pdp

# Set defaults for rules.
default allow_user = "You are not authorized to access this service"
default allow_time = "You cannot access this service in this time period"
default allow_geo = "You cannot access this service from your location"

default stateless_checks_failed = false

default allow_user_rate = "You have exceeded your user request quota for this service"
default allow_group_rate = "You have exceeded your group request quota for this service"

# Based on the architecture, an identity provider will convert
# tokens held by the user to a user identifier. In this example, the
# user is directly accepted as a header.
user := input.headers.user

# Authorization context for this user is fetched from a Data Source,
# for example an AWS DynamoDB table.
user_ctx := data.datasources.internal.users[user]
group_ctx := data.datasources.internal.groups[user_ctx.group]

# Handle basic user check.
allow_user = x {
user_ctx
x := true
}

# Handle request time conditions.
epoch_ms = input.requestContext.requestTimeEpoch
request_time := time.clock(epoch_ms*1000000)

allow_time {
# The starting and ending time, in 24-hour UTC format, is defined for each user.
start_t := user_ctx.start_time
end_t := user_ctx.end_time

request_time[0] >= start_t
request_time[0] < end_t
}

# Handle request geolocation conditions.
ip := input.requestContext.identity.sourceIp

allow_geo {
geo := build.geo_from_ip(ip)

# Match the IP address geolocation to the allowed geolocations
# for this user.
geo.Subdivisions[_].IsoCode == user_ctx.subdivisions[_]
}

stateless_checks = [allow_user, allow_time, allow_geo]

# The following rules affect state, specifically the Redis cache cluster
# used for rate limiting. They should only be evaluated if the previous
# 'stateless' rules have passed successfully.
stateless_checks_failed {
check := stateless_checks[_]
not check == true
}

# Handle user rate limiting conditions.
allow_user_rate {
not stateless_checks_failed

key := concat("", ["user:", user])
limit := build.rate_limit(key, user_ctx.rate)

limit == false
}

# Handle group rate limiting conditions.
allow_group_rate {
not stateless_checks_failed

key := concat("", ["group:", user_ctx.group])
limit := build.rate_limit(key, group_ctx.rate)

limit == false
}

stateful_checks = [allow_user_rate, allow_group_rate]

all_checks = array.concat(stateless_checks, stateful_checks)

# This rule verifies all required conditions, and also decides
# the message to be shown to the user based on the type of auth denial.
allow {
passed := [x | x := all_checks[_]; x == true]
count(passed) == count(all_checks)
}

allow = message {
failed := [x | x := all_checks[_]; x != true]
message := failed[0]
}

0 comments on commit 980f6db

Please sign in to comment.