Security

Three things: whether the connection is encrypted, which authority is trusted, and what credentials are presented.

import "github.com/AceMQ-Company/acemq-go-amqp/security"

mq, err := acemq.Connect(ctx, "amqps://broker.internal:5671/",
    acemq.WithSecurity(security.Required().
        TrustCertificateAuthorityFile("/etc/acemq/ca.crt").
        WithCredentials(security.EnvironmentCredentials("MQ_USER", "MQ_PASSWORD"))))

The security package has no dependencies outside the standard library and does not import the rest of the library, so it can be built and reasoned about on its own.

The three modes

They are named rather than being a boolean, because somebody reading a configuration should not have to work out which meaning a false carries.

security.Required() encrypt and verify. The one to use.
security.Insecure() encrypt, accept any certificate
security.Disabled() do not encrypt

Insecure is worth being precise about. The traffic cannot be read by somebody watching the network — but nothing stops that somebody from being the broker, and everything you publish then goes to them. It is a step above plaintext and a long way below verification. TrustCertificateAuthority is barely more work.

Disabled sends the password in the clear, along with everything else. Reasonable on a laptop; against a broker across a network you do not entirely control, it is not.

The URL has to agree

Asking for TLS and then connecting to amqp:// would give a plaintext connection with no warning at all — the quietest possible way to lose the thing that was configured most deliberately. So it is refused before the socket opens:

acemq: security is set to required but the URL is amqp://, which is plaintext.
Use amqps://, or security.Disabled() if plaintext is what you meant

The reverse is refused too.

Trusting one authority

security.Required().TrustCertificateAuthorityFile("/etc/acemq/ca.crt")

This replaces the system trust store rather than adding to it. That is the point: a broker holding a certificate from a public authority is not your broker, and the hundreds of authorities a machine trusts by default are hundreds of ways to be wrong.

A certificate from any other authority is refused, and there is a test that proves it — the line between "encrypted" and "encrypted to the right party" is exactly the thing that looks fine until somebody is on the network.

TrustCertificateAuthority takes an already-parsed *x509.Certificate when the authority comes from somewhere other than a file.

When the address is not the name

security.Required().
    TrustCertificateAuthorityFile("ca.crt").
    WithServerName("broker.internal")

For connecting through an IP address, a tunnel, or a container's internal name.

Client certificates

security.Required().
    TrustCertificateAuthorityFile("ca.crt").
    WithClientCertificateFiles("client.crt", "client.key")

For a broker using EXTERNAL authentication rather than a password.

Development certificates

Everything acemq-certs writes is stamped:

ACEMQ DEVELOPMENT ONLY - DO NOT TRUST

A certificate carrying that mark is refused on every path, including Insecure, unless you say otherwise:

security.Required().
    TrustCertificateAuthorityFile("certs/ca.crt").
    AllowDevelopmentCertificates()

The refusal under Insecure is the one that matters most. Insecure is reached for by somebody who could not get verification working — which is exactly the person most likely to be pointing at a development broker, and later to leave the setting in place against a real one.

Calling AllowDevelopmentCertificates is a deliberate statement that this process is not production. That is a statement worth making hard to make by accident.

The mark is looked for on the subject and the issuer, and on every certificate in the chain, so a development authority signing an ordinarily-named broker certificate is caught too.

Generating them

go install github.com/AceMQ-Company/acemq-go-amqp/cmd/acemq-certs@latest
acemq-certs --out certs --broker localhost --days 30
certs/ca.crt         trust this
certs/ca.key         the authority's key
certs/server.crt     the broker's certificate
certs/server.key     the broker's key
certs/client.crt     for EXTERNAL authentication
certs/client.key     the client's key
certs/rabbitmq.conf  mount at /etc/rabbitmq/rabbitmq.conf

The command prints the docker run that serves them and the Go that connects to it. Keys are written readable only by their owner: a development key is still a key.

Thirty days by default, deliberately short. A development certificate that lasts a year is one that ends up somewhere it should not; regenerating is a second's work.

A certificate for localhost also covers 127.0.0.1 and ::1, because a broker reached by one name is very often reached by another and a certificate covering only one fails in a way that reads like a trust problem rather than a naming one.

Credentials

security.Required().
    TrustCertificateAuthorityFile("ca.crt").
    WithCredentials(security.EnvironmentCredentials("MQ_USER", "MQ_PASSWORD"))

Credentials given here override whatever the URL carried, which is how a password stays out of the URL, and so out of logs, error messages and process listings.

StaticCredentials(user, pass) the same every time. Fine for development.
EnvironmentCredentials(userVar, passVar) read from the environment on every connection
FileCredentials(path, user) read from a file on every connection
CredentialsFunc(fn) anything else — a secret manager, a token exchange

They are read at the moment a connection is made, not once at start-up. A password rotated by a sidecar is only useful if something asks for it again.

FileCredentials is how a mounted Kubernetes or Docker secret arrives. The file holds the password alone, or username:password when it carries both. Trailing whitespace is trimmed — every editor adds a newline, and a password with one on the end fails in a way nobody enjoys diagnosing.

The secret does not print

creds := security.Of("orders", "hunter2")
fmt.Println(creds)          // Credentials{orders}
fmt.Println(sec)            // security.Options{mode=required, credentials=provided}

That is the whole reason Credentials is a type rather than two strings. A struct printed with %v, a connection logged on failure, an error wrapping the configuration — none of them should put a password somewhere a colleague can read it. There is a test asserting the secret never appears, including in the error from a failed login.

A production checklist

Encrypted bodies are portable

crypto and the Java, .NET, Python and Ruby equivalents all write application/vnd.acemq.encrypted. This package frames a message the way all four of them do, byte for byte:

0xAE   0x01   len   key id   12-byte nonce   ciphertext + 16-byte tag

Magic byte, version, a one-byte key id length, the key id in UTF-8, the nonce, and AES-GCM's ciphertext with its 128-bit tag. The header is authenticated as associated data but not encrypted, so an altered key id makes the message fail to open rather than quietly opening as something else, and crypto.KeyIDOf reads it back without needing any key. A Java producer and a Go consumer can share a key, and the test suites of all five libraries pin the same vector.

An AES key here is 16, 24 or 32 bytes, the same three lengths the other libraries accept, so a key that already works in Java works here unchanged. crypto.NewKey draws 32.

.NET used to be the exception and no longer is. It wrote AES-256-CBC with a separate HMAC-SHA-256 up to its own 0.3.0 and moved to AES-GCM in this framing in the same round this library did, so a .NET producer and a Go consumer can share a key. What does not cross is .NET's own old bodies, which only .NET reads.

This is the only framing this library writes, and the only one it reads. A body that does not begin 0xAE is refused as what it is — a fatal error naming the framing, not a decryption failure — so a consumer pointed at a plaintext queue, or at bodies some other library framed its own way, is told what has actually happened rather than being left to suspect its keys.

What this library does not do

It secures the connection. It does not manage broker users or permissions, hold your keys, or decide who may publish what.

Reporting a problem: SECURITY.md.