Skip to content

Commit

Permalink
Allow PLAIN auth with mTLS (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
jandelgado committed Jun 8, 2022
1 parent 1205aea commit 3ff0c4d
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 97 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ jobs:
with:
github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov

- name: Build release artifacts
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: build --rm-dist --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: actions/upload-artifact@v3
with:
name: rabtap-binaries
path: dist/*
3 changes: 0 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
Expand Down
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and exchanges, inspect broker.
* [Usage](#usage)
* [Basic commands](#basic-commands)
* [Broker URI specification](#broker-uri-specification)
* [Authentication](#authentication)
* [Format specification for tap and sub command](#format-specification-for-tap-and-sub-command)
* [Environment variables](#environment-variables)
* [Default RabbitMQ broker](#default-rabbitmq-broker)
Expand Down Expand Up @@ -240,9 +241,9 @@ Arguments and options:
--speed=FACTOR Speed factor to use during publish [default: 1.0].
--stats include statistics in output of info command.
-t, --type=TYPE type of exchange [default: fanout].
--tls-cert-file=CERTFILE A Cert file to use for client authentication.
--tls-key-file=KEYFILE A Key file to use for client authentication.
--tls-ca-file=CAFILE A CA Cert file to use with TLS.
--tls-cert-file=CERTFILE A Cert file to use for client authentication (PEM).
--tls-key-file=KEYFILE A Key file to use for client authentication (PEM).
--tls-ca-file=CAFILE A CA Cert file to use with TLS (PEM).
--uri=URI connect to given AQMP broker. If omitted, the
environment variable RABTAP_AMQPURI will be used.
-v, --verbose enable verbose mode.
Expand All @@ -268,7 +269,7 @@ Examples:
rabtap info --filter "binding.Source == 'amq.topic'" --omit-empty
rabtap conn close "172.17.0.1:40874 -> 172.17.0.2:5672"
# use RABTAP_TLS_CERTFILE | RABTAP_TLS_KEYFILE | RABTAP_TLS_CAFILE environments variables
# use RABTAP_TLS_CERTFILE | RABTAP_TLS_KEYFILE | RABTAP_TLS_CAFILE environment variables
# instead of specifying --tls-cert-file=CERTFILE --tls-key-file=KEYFILE --tls-ca-file=CAFILE
```

Expand Down Expand Up @@ -304,6 +305,15 @@ Note that according to [RFC3986](https://tools.ietf.org/html/rfc3986) it might b
necessary to escape certain characters like e.g. `?` (%3F) or `#` (%23) as otherwise
parsing of the URI may fail with an error.

#### Authentication

Authentication is either by the username and password provided in the broker
URI as desribed above (RabbitMQ `PLAIN` method), or by mTLS providing a client
certificate and key using the `--tls-key`, `--tls-cert` options (RabbitMQ
`EXTERNAL` method). If both mTLS and a username and password is provided, then
rabtap will use mTLS and `PLAIN` authentication with the given username and
password.

### Format specification for tap and sub command

The `--format=FORMAT` option controls the format of the `tap` and `sub`
Expand Down Expand Up @@ -351,14 +361,15 @@ $ rabtap info

#### Default RabbitMQ TLS config

The default TLS config certificates path can be set using the
The default TLS certificates path can be set using the
`RABTAP_TLS_CERTFILE` and `RABTAP_TLS_KEYFILE` and `RABTAP_TLS_CAFILE`
environments variables. Example:
environments variables. All certificate and key files are expected in PEM
format. Example:

```console
$ export RABTAP_TLS_CERTFILE=/etc/rabbitmq/ssl/cert.pem
$ export RABTAP_TLS_KEYFILE=/etc/rabbitmq/ssl/key.pem
$ export RABTAP_TLS_CAFILE =/etc/rabbitmq/ssl/ca.pem
$ export RABTAP_TLS_CERTFILE=/path/to/certs/user.crt
$ export RABTAP_TLS_KEYFILE=/path/to/certs/user.key
$ export RABTAP_TLS_CAFILE =/path/to/certs/ca.crt
$ echo "Hello" | rabtap pub --exchange amq.topic --routingkey "key"
...
```
Expand Down
19 changes: 13 additions & 6 deletions pkg/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rabtap

import (
"crypto/tls"
"net/url"
"time"

amqp "github.com/rabbitmq/amqp091-go"
Expand All @@ -15,17 +16,23 @@ const (
// DialTLS is a Wrapper for amqp.DialTLS that supports EXTERNAL auth for mtls
// can be removed when https://github.com/streadway/amqp/pull/121 gets some day
// merged.
func DialTLS(url string, amqps *tls.Config) (*amqp.Connection, error) {
var sasl []amqp.Authentication
func DialTLS(uri string, tlsConfig *tls.Config) (*amqp.Connection, error) {

u, err := url.Parse(uri)
if err != nil {
return nil, err
}

if amqps.Certificates != nil {
// client certificate are set to we must use EXTERNAL auth
// if client certificates are specified and no explicit credentials in the
// amqp connect url are given, then request EXTERNAL auth.
var sasl []amqp.Authentication
if tlsConfig.Certificates != nil && u.User == nil {
sasl = []amqp.Authentication{&amqp.ExternalAuth{}}
}

return amqp.DialConfig(url, amqp.Config{
return amqp.DialConfig(uri, amqp.Config{
Heartbeat: defaultHeartbeat,
TLSClientConfig: amqps,
TLSClientConfig: tlsConfig,
Locale: defaultLocale,
SASL: sasl,
})
Expand Down
78 changes: 78 additions & 0 deletions pkg/dial_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (C) 2022 Jan Delgado

// +build integration

package rabtap

import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"testing"

amqp "github.com/rabbitmq/amqp091-go"
"github.com/stretchr/testify/assert"
)

// relative location of certificates used during the integration tests
const certDir = "../inttest/pki/certs/"

func TestDialTLSFailsWithInvalidURL(t *testing.T) {
_, err := DialTLS(":invalid", nil)
assert.Error(t, err)
}

func TestDialTLSConnectsToNonTLSEndpoint(t *testing.T) {
conn, err := DialTLS("amqp://guest:password@localhost:5672", &tls.Config{})
assert.NoError(t, err)
conn.Close()
}

func TestDialTLSConnectsToTLSEndpoint(t *testing.T) {

testcases := []struct {
certFile, keyFile string
url string
err error
}{
// credentials in URL will force PLAIN auth
{"unknown.crt", "unknown.key", "amqps://guest:password@localhost:5671", nil},
{"unknown.crt", "unknown.key", "amqps://invalid:pass@localhost:5671",
&amqp.Error{Code: 403, Reason: "username or password not allowed"}},

// client cert with unknown user in RabbitMQ will not proceed
{"unknown.crt", "unknown.key", "amqps://localhost:5671",
&amqp.Error{Code: 403, Reason: "username or password not allowed"}},

// client cert with known user in RabbitMQ will proceed
{"testuser.crt", "testuser.key", "amqps://localhost:5671", nil},

// client cert with known user in RabbitMQ but unknown credentials will not proceed
{"testuser.crt", "testuser.key", "amqps://invalid:pass@localhost:5671",
&amqp.Error{Code: 403, Reason: "username or password not allowed"}},
}

for _, tc := range testcases {
// given
tlsConfig := &tls.Config{}
caCert, err := ioutil.ReadFile(certDir + "ca.crt")
assert.NoError(t, err)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
tlsConfig.BuildNameToCertificate()

cert, err := tls.LoadX509KeyPair(certDir+tc.certFile, certDir+tc.keyFile)
assert.NoError(t, err)
tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.BuildNameToCertificate()

// when
conn, err := DialTLS(tc.url, tlsConfig)

// then
assert.Equal(t, tc.err, err)

conn.Close()
}
}
79 changes: 0 additions & 79 deletions pkg/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ package rabtap
import (
"context"
"crypto/tls"
"crypto/x509"
"io/ioutil"
"testing"
"time"

"github.com/jandelgado/rabtap/pkg/testcommon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// relative location of certificates used during the integration tests
const certDir = "../inttest/pki/certs"

func TestSessionProvidesConnectionAndChannel(t *testing.T) {

ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -122,76 +116,3 @@ func TestSessionNewChannelReturnsNewChannel(t *testing.T) {
assert.NotNil(t, session.Channel)
assert.NotEqual(t, chanOld, session.Channel)
}

func TestSessionNewChannelReturnsNewChannelWithTLS(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

log := testcommon.NewTestLogger()
url := "amqps://localhost:5671/"
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
}

// certificates created for by testdata/pki/mkcerts.sh
// server certificates issued for localhost.
// client certificate uses CN testuser.

// load client certificate, key and ca certificate
cert, err := tls.LoadX509KeyPair(certDir+"/testuser.crt", certDir+"/testuser.key")
require.NoError(t, err)

tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.BuildNameToCertificate()

// Load CA cert
caCert, err := ioutil.ReadFile(certDir + "/ca.crt")
require.NoError(t, err)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
tlsConfig.BuildNameToCertificate()

sessions := redial(ctx, url, tlsConfig, log, FailEarly)

sessionFactory := <-sessions
session, ok := <-sessionFactory
require.True(t, ok)
assert.NotNil(t, session.Channel)
chanOld := session.Channel
session.NewChannel()
assert.NotNil(t, session.Channel)
assert.NotEqual(t, chanOld, session.Channel)
}

func TestSessionNewChannelFailsWithCertificateWithUnknownUser(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

log := testcommon.NewTestLogger()
url := "amqps://localhost:5671/"
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
}

// load client certificate, key and ca certificate
cert, err := tls.LoadX509KeyPair(certDir+"/unknown.crt", certDir+"/unknown.key")
require.NoError(t, err)

tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.BuildNameToCertificate()

// Load CA cert
caCert, err := ioutil.ReadFile(certDir + "/ca.crt")
require.NoError(t, err)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
tlsConfig.BuildNameToCertificate()

sessions := redial(ctx, url, tlsConfig, log, FailEarly)

sessionFactory := <-sessions
_, ok := <-sessionFactory
assert.False(t, ok)
}

0 comments on commit 3ff0c4d

Please sign in to comment.