Skip to content

Commit

Permalink
0.2.3
Browse files Browse the repository at this point in the history
  • Loading branch information
weisdd committed Mar 10, 2021
1 parent 7cf3b02 commit 4e5873f
Show file tree
Hide file tree
Showing 17 changed files with 1,513 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.md
.vscode
LICENSE
Makefile
acl.yaml
build
temp
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

temp
acl.yaml
.vscode
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# CHANGELOG

## 0.2.3

* Added `/federate` to a list of requests that should be rewritten.

## 0.2.2

* Moved to go 1.16;
* Bumped dependencies;
* Improved build caching.

## 0.2.1

* Adjusted request rewrite logic, so now all requests containing `/api/` are rewritten, whereas previously only those starting with `/api/`. So, non-standard URIs are taken into account now.
* Explicitly specified flush interval for reverse proxy;

## 0.2.0

* Added support for extra authorization headers (X-Forwarded-Access-Token, X-Auth-Request-Access-Token).

## 0.1.1

* Bugfix for doubling URI-path while proxying in case UPSTREAM_URL has non-empty URI.

## 0.1.0

* Initial release.
42 changes: 42 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
ARG ALPINE_VERSION=3.13.1
ARG GOLANG_VERSION=1.16.0-alpine3.13

FROM golang:${GOLANG_VERSION} as builder

ARG VERSION

WORKDIR /go/src/lfgw/
COPY go.mod go.sum ./
RUN go mod download
COPY . .

ENV CGO_ENABLED=0

RUN go install \
-installsuffix "static" \
-ldflags " \
-X main.Version=${VERSION} \
-X main.GoVersion=$(go version | cut -d " " -f 3) \
-X main.Compiler=$(go env CC) \
-X main.Platform=$(go env GOOS)/$(go env GOARCH) \
" \
./...

FROM alpine:${ALPINE_VERSION} as runtime

RUN set -x \
&& apk add --update --no-cache ca-certificates tzdata \
&& echo 'Etc/UTC' > /etc/timezone \
&& update-ca-certificates

ENV TZ=/etc/localtime \
LANG=en_US.utf8 \
LC_ALL=en_US.UTF-8

COPY --from=builder /go/bin/lfgw /
RUN chmod +x /lfgw

RUN adduser -S appuser -u 1000 -G root
USER 1000

ENTRYPOINT ["/lfgw"]
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test:
./build/test.sh
128 changes: 126 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,126 @@
# lfgw
A trivial reverse proxy based on httputil and VictoriaMetrics/metricsql with a purpose of dynamically rewriting requests to Prometheus-like backends
# LFGW

lfgw is a trivial reverse proxy based on `httputil` and `VictoriaMetrics/metricsql` with a purpose of dynamically rewriting requests to Prometheus-like backends.

More specifically, it manipulates label filters in metric expressions to reduce the scope of metrics exposed to an end user.

## Key functions

* non-opinionated proxying of requests with valid jwt tokens - there's Prometheus to decide whether request is valid or not;
* ACL-based request rewrites with implicit deny;
* supports Victoria Metrics' PromQL extensions;
* since it's based on `VictoriaMetrics/metricsql` library, which has way simpler interface than `prometheus`, there is no need to write a separate implementation for every type of MetricExpr on the planet;
* it's based on the middleware pattern, so it's easy to implement other, non-OIDC, modes should the need be;
* support for different headers with access tokens (X-Forwarded-Access-Token, X-Auth-Request-Access-Token, Authorization);
* requests to sensitive endpoints are blocked by default;
* requests to both `/api/*` and `/federate` endpoints are protected (=rewritten).

## Current limitations

* there's no OIDC callback, so it can only proxy requests that already have a jwt token;
* other modes (POSITIVE_REGEXP, NEGATIVE_REGEXP, NAMESPACE) are deprecated.

## TODO

* tests for handlers;
* improve naming;
* log slow requests;
* metrics;
* add CLI interface (currently, only environment variables are used);
* configurable JMESPath for the `roles` attribute;
* OIDC callback to support for proxying Prometheus web-interface itself;
* structured logging (it'll require an intermediate interface for logging `httputil`'s error logs);
* simple deduplication if there is any performance issue (another option: use `trickster` for request optimizations).

## Similar projects

### Prometheus filter proxy

Link: [Prometheus filter proxy](https://github.com/hoffie/prometheus-filter-proxy)

Minuses:

* based on Prometheus library, so might not support some of Victoria Metrics' extensions;
* it's an overly simple implementation that relies only on URI paths to define the scope of available metrics, so a user might potentially get access to any metrics should the URL become exposed;
* it's based on http client, thus unlikely to be ready for high volumes of requests;
* does not allow to filter out requests to sensitive endpoints (like `/admin/tsdb`);
* no longer maintained.

### Prometheus ACLs

Link: [Prometheus ACLs](https://github.com/bitsbeats/prometheus-acls)

Pluses:

* based on `httputil` reverse proxy;
* supports label filter deduplication;
* extensive ACL syntax, which allows to specify any label filters, not necessarily limit to `namespace`;
* has an OIDC callback and `gorilla/sessions`, so it's possible to obtain a token through the application itself;
* validates jwt-token;
* metrics;
* configurable JMESPath.

Minuses:

* based on Prometheus library, so might not support some of Victoria Metrics' extensions;
* does not allow to filter out requests to sensitive endpoints (like `/admin/tsdb`);
* does not rewrite requests to `/federate` endpoint (at least, at the time of writing);
* http server time-outs are not configured, thus might retain http sessions for much longer than needed.

## Configuration

OIDC roles are expected to be present in `roles` within a jwt token.

### Environment variables

| Module | Variable | Default Value | Description |
| -------------------- | ------------------- | ------------- | ------------------------------------------------------------ |
| **General settings** | | | |
| | `LFGW_MODE` | `oidc` | Currently, only `oidc` mode is supported. So, never mind. |
| | | | |
| **Logging** | | | |
| | `DEBUG` | `false` | Whether to print out debug log messages. |
| | | | |
| **HTTP Server** | | | |
| | `PORT` | `8080` | Port the web server will listen on. |
| | `READ_TIMEOUT` | `10s` | `ReadTimeout` covers the time from when the connection is accepted to when the request body is fully read (if you do read the body, otherwise to the end of the headers). [More details](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) |
| | `WRITE_TIMEOUT` | `10s` | `WriteTimeout` normally covers the time from the end of the request header read to the end of the response write (a.k.a. the lifetime of the ServeHTTP). [More details](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) |
| | | | |
| **Proxy** | | | |
| | `UPSTREAM_URL` | | Prometheus URL, e.g. `http://prometheus.microk8s.localhost`. |
| | `SAFE_MODE` | `true` | Whether to block requests to sensitive endpoints like `/api/v1/admin/tsdb`, `/api/v1/insert`. |
| | `SET_PROXY_HEADERS` | `false` | Whether to set proxy headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`). |
| | | | |
| **OIDC** | | | |
| | `ACL_PATH` | `./acl.yaml` | Path to a file with ACL definitions (OIDC role to namespace bindings). |
| | `OIDC_REALM_URL` | | OIDC Realm URL, e.g. `https://auth.microk8s.localhost/auth/realms/cicd` |
| | `OIDC_CLIENT_ID` | | OIDC Client ID (1*) |

(1*): since it's grafana who obtains jwt-tokens in the first place, the specified client id must also be present in the forwarded token (the `audience` field). To put it simply, better to use the same client id for both grafana and lfgw.

### acl.yaml syntax

The file with ACL definitions has a simple structure:

```yaml
role: namespace, namespace2
```
For example:
```yaml
team0: .* # all metrics
team1: minio # only those with namespace="minio"
team2: min.* # only those matching namespace=~"min.*"
team3: minio, stolon # only those matching namespace=~"^(minio|stolon)$"
team4: min.*, stolon # only those matching namespace=~"^(min.*|stolon)$"
```
To summarize, here are the key principles used for rewriting requests:
* `.*` - all requests are simply forwarded to an upstream;

* `minio` - all label filters with the `namespace` label are removed, then `namespace="minio"` is added;
* `min.*` - positive regex-match label filters (`namespace=~"X"`) are removed, then `namespace=~"mi.*"` is added;
* `minio, stolon` - positive regex-match label filters (`namespace=~"X"`) are removed, then `namespace=~"^(minio|stolon)$"` is added;
* `min.*, stolon` - positive regex-match label filters (`namespace=~"X"`) are removed, then `namespace=~"^(min.*|stolon)$"` is added.
43 changes: 43 additions & 0 deletions build/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
set -o errexit
set -o nounset
set -o pipefail

TARGET=./cmd/lfgw

echo "Running tests:"
go test ./...
echo

echo -n "Checking go vet: "
ERRS=$(go vet ${TARGET} 2>&1 || true)
if [ -n "${ERRS}" ]; then
echo "FAIL"
echo "${ERRS}"
echo
exit 1
fi
echo "PASS"
echo

echo -n "Checking golint: "
ERRS=$(golint ${TARGET} 2>&1 || true)
if [ -n "${ERRS}" ]; then
echo "FAIL"
echo "${ERRS}"
echo
exit 1
fi
echo "PASS"
echo

echo -n "Checking golangci-lint: "
ERRS=$(golangci-lint run ${TARGET} 2>&1 || true)
if [ -n "${ERRS}" ]; then
echo "FAIL"
echo "${ERRS}"
echo
exit 1
fi
echo "PASS"
echo
Loading

0 comments on commit 4e5873f

Please sign in to comment.