-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from build-security/build-and-doc
Add build files, policy code and documentation
- Loading branch information
Showing
5 changed files
with
280 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |