View Source Security
EventStoreDB has a few options for security which are now all enabled by default in EventStoreDB 20+:
- connections over TLS (SSL/HTTPS)
- username+password basic-auth credentials
- access control lists (ACLs) on streams or globally to an EventStoreDB
By default in EventStoreDB 20+, these are all enabled. The EventStoreDB
may be run with the --insecure
command line flag to disable all of them
together.
Setting up an EventStoreDB for security
The EventStore documentation now includes a helpful configuration wizard which will help set up custom or cloud installations of EventStoreDB with the proper networking, config, clustering, and certificates. For any production use-case, see the EventStore documentation.
The repository for Spear includes a certs/
directory with some generated
certificates. Note that the private keys are included in the repository, so the
certificates in that directory are not suitable for any real-world case.
Please only use them on a local machine not connected to the internet.
Assuming that you're running a Linux machine with the eventstore-oss
package installed, we can install the certificates in this repository like so.
Note that these commands will probably need to be run through sudo
.
$ cd /path/to/spear
$ mkdir -p /etc/eventstore/certs
$ cp -r ./certs/ca /etc/eventstore/certs/
$ cp ./certs/node1/* /etc/eventstore/certs
$ cp ./certs/eventstore.conf /etc/eventstore/
$ chown -R eventstore /etc/eventstore/
$ chgrp -R eventstore /etc/eventstore/
$ systemctl restart eventstore
Now the EventStoreDB should be set up to force TLS connections. You may
need to edit the existing service description to remove the --insecure
flag
if present.
Using custom TLS certificates with Spear
A Spear.Connection
takes an :mint_opts
option which is passed to
Mint.HTTP.connect/4
. We can inform mint of our custom ./certs/ca/ca.crt
CA certificate like so:
connection_config = [
connection_string: "esdb://localhost:2113?tls=true",
mint_opts: [
transport_opts: [
cacertfile: Path.join([__DIR__ | ~w(certs ca ca.crt)])
]
]
]
# as a supervisor spec:
{Spear.Connection, connection_config}
# or as a direct call of start_link
iex> {:ok, conn} = Spear.Connection.start_link(connection_config)
Note that this same configuration works with a Spear.Client
with this
configuration in application-config (config/*.exs
).
If you're following along with the guide and have set up an EventStoreDB with
the certificates from the certs/
directory in the spear repository, you
should now be able to spawn a connection which will force a TLS connection.
Using TLS certificates signed by a public CA
Certificates signed by a public certificate authority (CA) such as the Domain
Validation (DV) certificates produced by the wonderful Letsencrypt project
should work out-of-the-box if the castore
dependency is included in your project.
# mix.exs
..
def deps do
[
{:spear, "~> 0.1"},
{:castore, ">= 0.0.0"}
]
end
..
With castore
, you should not need to pass any transport_opts
to Mint.
connection_config = [
connection_string: "esdb://localhost:2113?tls=true"
]
Event Store Cloud uses
certificates signed by a public CA, so connections to Managed EventStoreDBs
(MESDBs) work out-of-the-box with castore
.
Credentials
Now that TLS is enabled, we can safely pass basic-auth credentials over the
network. Spear.Connection
accepts credentials through the connection string
or via the :username
and :password
options.
connection_config = [
connection_string: "esdb://admin:changeit@localhost:2113?tls=true",
mint_opts: [
transport_opts: [
cacertfile: Path.join([__DIR__ | ~w(certs ca ca.crt)])
]
]
]
Credentials can also be passed on a per-request basis for all core Spear
functions (except Spear.ping/2
; pings do not require/allow authentication
as they are not actual requests). All core functions allow a :credentials
option two-tuple of {username, password}
which override the connection-level
credentials if provided.
E.g. reading a stream as a user named "Aladdin" (assuming such a user exists):
# say we use `connection_config` from above: the connection has credentials
# of {"admin", "changeit"}
iex> {:ok, conn} = Spear.Connection.start_link(connection_config)
# the :credentials option overrides the connection-level credentials
iex> Spear.stream!(conn, "my_stream", credentials: {"Aladdin", "open sesame"}) |> Enum.take(1)
[%Spear.Event{}]
Access control lists
Now that we know how to operate the client as a user, how does EventStoreDB use credentials to allow or deny access to resources?
EventStoreDB uses access control lists (ACLs) to allow or deny access to various operations per user or group.
ACLs (like virtually everything in EventStoreDB) are just events in a stream. A simple ACL event body might look like
{
"$acl": {
"$w": "$admins",
"$r": "$all",
"$d": "$admins",
"$mw": "$admins",
"$mr": "$admins"
}
}
What do each of these mean?
value | resource |
---|---|
$w | write events to this stream |
$r | read events from this stream |
$d | delete this stream |
$mw | write metadata associated with this stream |
$mr | read metadata associated with this stream |
$admins | the group of admin users |
$all | all users (including anonymous users) |
Note that the ACL is controlled by writing stream metadata, so the $mw
permission allows a user to change the ACL of a stream.
The global ACL
The $streams
system stream may be used to change the default ACL applied to
all streams. By default, the global ACL is
{
"$userStreamAcl": {
"$r": "$all",
"$w": "$all",
"$d": "$all",
"$mr": "$all",
"$mw": "$all"
},
"$systemStreamAcl": {
"$r": "$admins",
"$w": "$admins",
"$d": "$admins",
"$mr": "$admins",
"$mw": "$admins"
}
}
Where $systemStreamAcl
applies to projected and otherwise system-created
streams and $userStreamAcl
applies to all other (user-created) streams.
This default is quite permissive: any user including clients that do not supply any credentials can read and write events to user-streams with the default ACL.
The global ACL can be changed by writing an event of type update-default-acl
with a content type of application/vnd.eventstore.events+json
with the above
body to the $streams
system stream.
Spear provides the Spear.Acl
struct and Spear.set_global_acl/4
function
to set this without dealing with the nitty-gritty details of the structure
of that event.
Attempting to access a resource with incorrect or invalid credentials will yield an HTTP 401 error.
iex> Spear.set_global_acl(conn, Spear.Acl.admins_only(), Spear.Acl.admins_only())
:ok
iex> Spear.append([my_event], conn, "some_stream", credentials: {"no one", "no pass"})
{:error,
%Spear.Grpc.Response{
data: "",
message: "Bad HTTP status code: 401, should be 200",
status: :unknown,
status_code: 2
}}
Attempting to access a resource with no credentials will yield a gRPC error
with a status of :permission_denied
.
Stream-level ACLs
EventStoreDB also allows more fine-grained stream-level ACLs to be defined on
a per-stream basis. See the Spear.set_stream_metadata/4
function for an
example of setting a stream-level ACL.