For general info on Secretless plugins, please refer to the plugin README.
Secretless has built-in support for PostgreSQL, MySQL, Microsoft SQL Server, APIs that use basic HTTP authentication, and many other target services.
If you want to use Secretless with a target service that is not supported out of the box, you can create Service connector plugins to extend Secretless to support any target service.
If you know Go and understand your target service's authentication protocol, you can write a connector for it. This guide provides all of the information you'll need.
You can also use our connector templates to add your new connector plugin.
The Secretless team is continually adding support for new databases and services, but we encourage outside contributions as well. If you write a connector plugin that you'd like to share with the community, please let us know or consider sending us a PR!
As described in the plugin README, Secretless uses Go plugins
to produce shared object library files (with .so
extensions) instead of a normal executable. The .so
library files can
be loaded at runtime to expand the number of target services your Secretless instance supports.
There are two types of service connector plugins that we currently support - TCP and HTTP. A good general rule for deciding which to implement is that TCP is appropriate for databases and HTTP for APIs. If you want to write a plugin and aren’t sure which option to choose, please get in touch with us.
Technically, a Secretless service connector plugin is just a Go shared library file that implements two functions.
-
TCP connectors implement:
PluginInfo() map[string]string GetTCPPlugin() tcp.Plugin
where
tcp.Plugin
is defined as follows (fromgithub.com/cyberark/secretless-broker/pkg/secretless/plugin/connector/tcp
):package tcp type Plugin interface { NewConnector(connector.Resources) Connector } type Connector interface { Connect(net.Conn, connector.CredentialValuesByID) (net.Conn, error) }
-
HTTP connectors implement:
PluginInfo() map[string]string GetHTTPPlugin() http.Plugin
where
http.Plugin
is defined as follows (fromgithub.com/cyberark/secretless-broker/pkg/secretless/plugin/connector/http
):package http type Plugin interface { NewConnector(connector.Resources) Connector } type Connector interface { Connect(*http.Request, connector.CredentialValuesByID) error }
We'll get into the details below, but at a high level your connector is what is returned by 'NewConnector'. It is an interface with one method that knows how to transform an unauthenticated connection or request into an authenticated one.
To get that job done, Secretless provides you with connector.Resources
(detailed below) as well as the current credential values -- the secrets
you'll need to authenticate. Your plugin users specify the location of those
secrets in secretless.yml
, as described
here
and here.
At runtime, Secretless fetches the values of those secrets and passes
them into your Connector
function.
This section describes the interfaces and types you'll need to use when authoring a plugin.
This top level function is always required. It returns basic information about your plugin. Its signature is:
func PluginInfo() map[string]string
The returned map must have the following keys:
version
: The version of the plugin itself. This allows plugin authors to version the plugins they write.pluginAPIVersion
: The version of the Secretless plugin API your plugin is written for. This allows the Secretless plugin API to change over time without breaking plugins.type
: This must be either the string"connector.tcp"
or the string"connector.http"
id
: A short, clear, unique name for use in logs and thesecretless.yml
config file. Allowed characters are: lowercase letters,_
,:
,-
, and~
.description
: A short summary of the plugin, not to exceed 100 characters. This may be used in the future by the secretless cmd line tool to list available plugins.
In both the tcp.Plugin
and http.Plugin
interfaces, NewConnector
returns a
Connector
, a one-method interface that performs the actual authentication.
The method signature is slightly different depending on which interface you're
implementing.
When Secretless runs, it calls NewConnector
once, and then holds onto the
returned Connector
. That Connector
(remember: it's just a a single method)
is then called each time a new client connection requires authentication.
Both NewConnector
methods take only one argument -- connector.Resources
--
described below.
The real work is done by the Connector
functions they return.
This is the one method interface returned by tcp.Plugin
's NewConnector()
,
and it's where your TCP authentication logic lives. The method's signature is:
func(net.Conn, connector.CredentialValuesByID) (net.Conn, error)
That is, it's passed the client's net.Conn
and the current
CredentialValuesById
, and returns an authenticated net.Conn
to the target
service. The authentication stage is complete after Connector
is called.
Secretless now has both the client connection and an authenticated connection to the target service. The relationship between the client connection, Secretless, and the authenticated target service connection looks like this:
clientConn <--> Secretless <--> authdTargetServiceConn
At this point, Secretless becomes an invisible proxy, streaming bytes back and forth between client and target service, as if they were directly connected.
This is the one method interface returned by http.Plugin
's NewConnector()
,
and it's where the http authentication logic lives. The method's signature is:
func(*http.Request, connector.CredentialValuesById) error
Here we are passed a pointer to an http.Request
and CredentialValuesById
, and
are expected to alter that request so that it contains the necessary
authentication information. Typically, this means adding the appropriate
headers to a request -- for example, an Authorization
header containing a
Token, or a header containing an API key.
Since HTTP is a stateless protocol, Secretless calls this function every time a client sends an http request to the target server, so that every request is authenticated.
Everything that your Connector needs from the Secretless framework is exposed
through the connector.Resources
interface, which is passed to your plugin's
constructor. You need to retain a reference to connector.Resources
, via
a closure, inside the Connector
function returned by your constructor.
Here's the connector.Resources
interface:
package connector
type Resources interface {
Config() []byte
Logger() secretless.Logger
}
Logger()
provides a basic logger that you can use for debugging and informational
logging. Config()
provides you resources specified in your secretless.yml
file.
Let's break down each method:
Config()
- Some connectors require additional, connector-specific configuration. Anything specified in your connector'sconfig
section is passed back via this method as a raw[]byte
. Your code is responsible for casting those bytes back into a meaningfulstruct
that your code can work with.Logger()
- Returns an object similar to the standard library'slog.Logger
. This lets you log events to stdout and stderr. It respects Secretless's command linedebug
flag, so that callingDebugf
orInfof
does nothing unless you started Secretless in debug mode. See below for details.
Your code should never use the fmt
or log
packages, or write directly to
stdout or stderr. Instead, call Logger()
on your connector.Resources
to get
a secretless.Logger
with the following interface:
type Logger interface {
Debugf(format string, v ...interface{})
Debug(msg string)
Debugln(msg string)
Infof(format string, v ...interface{})
Info(msg string)
Infoln(msg string)
Warnf(format string, v ...interface{})
Warn(msg string)
Warnln(msg string)
Errorf(format string, v ...interface{})
Error(msg string)
Errorln(msg string)
Fatalf(format string, v ...interface{})
Fatal(msg string)
Fatalln(msg string)
}
The three Debug
methods and the three Info
methods do nothing unless you started
Secretless with the Debug
command line flag set to true. If you did start
Secretless in debug mode, the methods write to stdout.
The Warn
, Error
, and Fatal
methods all write to stderr, regardless of
the current Debug
mode.
All the Fatal
methods call os.Exit(1)
after printing their message.
The Logger
automatically prepends the service name specified in your secretless.yml
for the currently-running service to all messages. For example, if your
secretless.yml
looks like:
version: "v2"
services:
sample-service:
protocol: pg
listenOn: unix:///sock/.s.PGSQL.5432
...
then the Logger
prepends sample-service
to all messages.
To see an example external connector, please take a look at our test plugin. You can also look at our internal plugins that also implement this interface:
To get started with adding your new connector plugin, please take a look at our connector templates.