Skip to content

Commit

Permalink
tune up, modernization
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonhancock committed Aug 24, 2023
1 parent d70c92d commit 581daf6
Show file tree
Hide file tree
Showing 34 changed files with 1,151 additions and 637 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/pullrequests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Tests
on: [pull_request]

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: stable

- name: Go Tests
run: go test -v ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
amproxy
packaging/*.rpm
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

128 changes: 51 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,67 @@
# amproxy - Authenticated Metrics Proxy

[![GoDoc](https://godoc.org/github.com/jasonhancock/amproxy?status.svg)](https://godoc.org/github.com/jasonhancock/amproxy)
[![Build Status](https://travis-ci.org/jasonhancock/amproxy.svg?branch=master)](https://travis-ci.org/jasonhancock/amproxy)
[![Go Reference](https://pkg.go.dev/badge/github.com/jasonhancock/amproxy.svg)](https://pkg.go.dev/github.com/jasonhancock/amproxy)
[![Go Report Card](https://goreportcard.com/badge/jasonhancock/amproxy)](https://goreportcard.com/report/jasonhancock/amproxy)

A proxy for Graphite's Carbon service that authenticates messages before passing
them on to Carbon or dropping them on the floor.

This is a prototype and is just an example of what could be possible. It is
quite literally the first code beyond a "Hello World" that I've written in Go.
A proxy for Graphite's Carbon service that authenticates messages before passing them on to Carbon or dropping them on the floor.

## What does this do?

Carbon listens on port 2003 and doesn't offer any sort of authentication.
Usually this is manageable by firewalling off the service to only allow
connections from hosts you trust. The problem is that I want to build a device
that my friends/family run on their networks at home (without static IPs) and
report metrics to my Carbon server. I could run some sort of dynamic dns client
on each device and dynamically manage my firewall, but I don't really want to
deal with that.

Instead, I run Carbon bound to 127.0.0.1:2003 and run amproxy on port 2005
exposed to the internet. The client devices are each given a public/private key
pair that can be used to generate signed messages. These signed messages are
sent to amproxy which authenticates the message by validating the signature
and whether or not the specified metric is authorized for the given key pair,
and if so, forwards the metric on to Carbon.
Carbon listens on port 2003 and doesn't offer any sort of authentication. Usually this is manageable by firewalling off the service to only allow connections from hosts you trust. The problem is that I want to build a device that my friends/family run on their networks at home (without static IPs) and report metrics to my Carbon server. I could run some sort of dynamic dns client on each device and dynamically manage my firewall, but I don't really want to deal with that or with doing something like mTLS, or using something like MQTT.

Instead, I run Carbon bound to 127.0.0.1:2003 and run amproxy on port 2005 exposed to the internet. The client devices are each given a public/private key pair that can be used to generate signed messages. These signed messages are sent to amproxy which authenticates the message by validating the signature and whether or not the specified metric is authorized for the given key pair, and if so, forwards the metric on to Carbon.

## Configuration

All configuration is done via flags (will be updated for env vars soon):
```none
$ amproxy server --help
Starts the server
Usage:
amproxy server [flags]
$ ./amproxy --help
Usage of ./amproxy:
-addr string
interface/port to bind to (default ":2005")
-auth-file string
Location of auth file (default "/etc/amproxy/auth_file.yaml")
-carbon-addr string
Carbon address:port (default "127.0.0.1:2003")
-skew float
amount of clock skew tolerated in seconds (default 300)
Flags:
--addr string The interface and port to bind the server to. Can be set with the ADDR env variable (default "127.0.0.1:2005")
--auth-file string The path to the auth file. Can be set with the AUTH_FILE env variable (default "/etc/amproxy/auth_file.yaml")
--carbon-addr string The address of the carbon server. Can be set with the CARBON_ADDR env variable (default "127.0.0.1:2003")
-h, --help help for server
--log-format string The format of log messages. (logfmt|json) (default "logfmt")
--log-level string Log level (all|err|warn|info|debug (default "info")
--skew duration The amount of clock skew tolerated. Can be set with the MAX_SKEW env variable (default 5m0s)
```

## Auth File Format

```
```yaml
---
apikeys:
my_public_key:
secret_key: my_secret_key
metrics:
metric1: 1
metric2: 1
- metric1
- metric2
my_public_key2:
secret_key: my_secret_key2
metrics:
metric3: 1
metric4: 1
- metric3
- metric4
```
In the example above, my_public_key is authorized for metric1 and metric2 and
uses the `my_secret_key` private key.
In the example above, `my_public_key` is authorized for `metric1` and `metric2` and uses the `my_secret_key` private key.

If the AUTH_FILE is updated on disk, it will automatically get reloaded within
60 seconds.
If the `AUTH_FILE` is updated on disk, it will automatically get reloaded within 60 seconds.

## Protocol

Messages going over the wire are in the form:

```
```none
metric value timestamp public_key base64_signature
```

### Example:
### Example

```
```none
metric = foo
value = 1234
timestamp = 1425059762
Expand All @@ -85,60 +71,48 @@ secret_key = my_secret_key

The message for which we will generate the signature becomes

```
```none
foo 1234 1425059762 my_public_key
```

We can generate a signature with some perl code:
We can generate a signature:

```
#!/usr/bin/perl
use strict;
use warnings;
use Digest::SHA qw(hmac_sha256_base64);
my $digest = hmac_sha256_base64('foo 1234 1425059762 my_public_key', 'my_secret_key');
# Fix padding of Base64 digests
while (length($digest) % 4) {
$digest .= '=';
}
print $digest;
```shell
KEY_PRIVATE=my_secret_key amproxy client signature "foo 1234 1425059762 my_public_key"
```

Which outputs the following:
```

```none
lT9zOeBVNfTdogqKE5J7p3XWprfu/gOI5D7aWRzjJtc=
```

The message going over the wire becomes:

```
```none
foo 1234 1425059762 my_public_key lT9zOeBVNfTdogqKE5J7p3XWprfu/gOI5D7aWRzjJtc=
```

## Building/Testing
## Testing

To build in the vagrant environment, do the following:
To start a graphite/carbon stack, run:

```
cd /vagrant/src/amproxy/amproxy
go install
```shell
docker-compose up
```

This will generate the `/vagrant/bin/amproxy` binary. You can then run the binary:
Then access the graphite webui at [localhost:8080](http://localhost:8080). Username and password are both `root`.

```
AUTH=public_key1:private_key1 /vagrant/bin/amproxy
Start the server in another terminal:

```shell
AUTH_FILE=packaging/redhat/auth_file.yaml go run main.go server
```

And ship your signed metrics to localhost:2005
Start the test client in another terminal:

## Ideas
```
KEY_PUBLIC=my_public_key KEY_PRIVATE=my_secret_key go run main.go client test-client
```

This was just a proof of concept. Ideas for the future would be some sort of
pluggable backend to fetch the public/private keypairs from. As I'm still
prototyping, I didn't want to build out a complicated system that tied into
MySQL, Redis, Memcached, or some other backend API.
The test-client will send a metric named `metric1` every 60 seconds with a random value between 30 and 100.
64 changes: 0 additions & 64 deletions cmd/amproxy/main.go

This file was deleted.

20 changes: 20 additions & 0 deletions cmd/client/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package client

import (
"github.com/spf13/cobra"
)

// NewCmd initializes a new command and sub-commands.
func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "client",
Short: "Client operations.",
}

cmd.AddCommand(
cmdSignature(),
cmdTestClient(),
)

return cmd
}
47 changes: 47 additions & 0 deletions cmd/client/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package client

import (
"errors"
"fmt"
"os"

"github.com/jasonhancock/amproxy/pkg/amproxy"
"github.com/jasonhancock/go-helpers"
"github.com/spf13/cobra"
)

const envKeyPrivate = "KEY_PRIVATE"

func cmdSignature() *cobra.Command {
var privateKey string

cmd := &cobra.Command{
Use: "signature <message>",
Short: "Generates a signature for the provided input message",
SilenceUsage: true,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if privateKey == "" {
return errors.New("private key not specified")
}

msg, err := amproxy.Parse(args[0])
if err != nil {
return err
}

fmt.Println(msg.ComputeSignature(privateKey))

return nil
},
}

cmd.Flags().StringVar(
&privateKey,
"key-private",
os.Getenv(envKeyPrivate),
helpers.EnvDesc("The API private key.", envKeyPrivate),
)

return cmd
}
Loading

0 comments on commit 581daf6

Please sign in to comment.