Configuring syndicate-server gatekeepers
A gatekeeper in Syndicate terminology is an object that “upgrades” long-lived cryptographic names into live references to some underlying object. There are analogous objects in CapTP. (In CapTP, gatekeepers might be built-in to the protocol rather than separate objects. I need to find out more.)
The syndicate-server
program
includes an implementation of the gatekeeper
protocol. This
post builds up a short example syndicate-server setup involving gatekeepers. (The following is
based on this example config file from the syndicate-server
git
repository.)
The plan
We will create a TCP listener on port 9222, which speaks unencrypted protocol and allows interaction with the default/system gatekeeper, which has a single noise binding for introducing encrypted interaction with a second gatekeeper, which finally allows resolution of references to other objects.
The outer/default/system gatekeeper will not expose any functionality apart from upgrading to an encrypted, authenticated reference to the inner gatekeeper. All the real services we want to expose will be available via the inner gatekeeper.
The stanzas below are written in the syndicate-server scripting
language. They can be placed in a .pr
file,
e.g. example.pr
, and run with syndicate-server -c example.pr
.
Creating an inner gatekeeper
First, build a space where we place bindings for the inner gatekeeper to expose.
let ?inner-bindings = dataspace
Next, start, secure, and publish the inner gatekeeper.
<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
<bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
service: world }>
$inner-gatekeeper #f>
]
The <bind ...>
assertion exposes the $inner-gatekeeper
object via the outer gatekeeper.
Clients have to speak the noise protocol and know the public key
#[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs]
to resolve a reference to the inner
gatekeeper. The noise protocol itself sets things up to rule out man-in-the-middle and prove to
the client it really is talking to the correct server.
You can use the syndicate-macaroon
tool (e.g. syndicate-macaroon noise
--service world --random
) to generate fresh keys for use in noise bindings.
Exposing the outer gatekeeper over unencrypted TCP
Now, expose the outer gatekeeper to the world, via TCP. The system gatekeeper is a primordial syndicate-server object bound to $gatekeeper.
<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>
Let’s imagine we are running on a host called syndicate.example
; this hostname will be used
in URLs later.
Binding a service to the inner gatekeeper
Finally, let’s expose some behaviour accessible via the inner gatekeeper.
We will create a service dataspace called $world
, and we will require clients to hold a
sturdyref to it. We will
configure the inner gatekeeper to resolve the sturdyref to the $world
dataspace.
let ?world = dataspace
We need to generate a strong name, a sturdyref, that we will bind to $world
. For this, we can
use the syndicate-macaroon
tool.
Let’s choose the passphrase hello
as our secret that no-one but the service itself should
know.
Running syndicate-macaroon mint --oid a-service --phrase hello
yields the sturdyref <ref
{oid: a-service, sig:
#[JTTGQeYCgohMXW/2S2XH8g]}>
. That will act as a root capability for $world
once we bind it
at the inner gatekeeper, using the corresponding
sturdy.SturdyDescriptionDetail
:
$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>
Configuration done!
That’s it! All together, here is the configuration we set up:
let ?inner-bindings = dataspace
<require-service <gatekeeper $inner-bindings>>
? <service-object <gatekeeper $inner-bindings> ?inner-gatekeeper> [
<bind <noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs],
secretKey: #[qLkyuJw/K4yobr4XVKExbinDwEx9QTt9PfDWyx14/kg],
service: world }>
$inner-gatekeeper #f>
]
<require-service <relay-listener <tcp "0.0.0.0" 9222> $gatekeeper>>
let ?world = dataspace
$inner-bindings += <bind <ref {oid: a-service, key: #"hello"}> $world #f>
Accessing the $world from some remote client
Now, we can hand out paths to $world
involving an initial noise step and a subsequent
sturdyref/macaroon step. Clients using such paths benefit from an encrypted, authenticated
channel (the noise layer), and use of the $world
service is authorized by possession of a
correctly-signed sturdyref.
The gatekeeper.Route
that a client might use would be something like:
<route [<ws "ws://syndicate.example:9222/">]
<noise { key: #[z1w/OLy0wi3Veyk8/D+2182YxcrKpgc8y0ZJEBDrmWs], service: world }>
<ref { oid: a-service, sig: #[CXn7+rAoO3Xr6Y6Laap3OA] }>>
There’s more that sturdyrefs can do: since they are based on Macaroons, they can be attenuated offline (!) to limit the kinds of things clients can assert at $world and the kinds of messages they can send to $world. The example config file gets into more detail.