Security API: Difference between revisions

From wiki.zmanda.com
Jump to navigation Jump to search
(→‎Overview: no half-closed streams)
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
== Introduction ==
= Introduction =


This is a document of the API for defining and utilizing multiple security and transport mechanisms for the Amanda network protocol.
This is a document of the API for defining and utilizing multiple security and transport mechanisms for the Amanda network protocol. The goal of this API is to allow several different forms of communication and authentication to exist between the Amanda server and its clients.  This is variously known as the "Secure API", "Security API", and "Authentication"


The goal of this API is to allow several different forms of communication and authentication to exist between the Amanda server and its clients.
= Overview =


== Context ==
The security API connects two Amanda processes over the network.  One process, the initiator, connects to the other, the target.  During backup operations, the initiator is the serverFor {{man|8|amrecover}}, the client is the initiator.
Amanda communication takes place between two hostsThe communication channel consists of a message-based ''protocol'' coupled with an arbitrary number of ''streams''.  The protocol is used to set up any streams that will be used in a conversation.  While the initiator of a connection can be any application, the target is always an amandad instance.  Amandad implements the Amanda protocol, and implements numerous special cases for common Amanda applications.
 
Once two processes are connected and any necessary authentication has taken place, the communication channel consists of a message-based ''protocol'' coupled with an arbitrary number of ''streams''.  The protocol is used to set up any streams that will be used in a conversation.  In practice, the initiator of a connection can be any application, the target is always an amandad instance.  Amandad implements the Amanda protocol, and implements numerous special cases for common Amanda applications.
 
Note that streams cannot be half-closed.  All uses of streams in Amanda are currently terminated by the service, although nothing in the protocol itself dictates this.


For the user-level perspective on the Security API, see {{man|7|amanda-auth}}.
For the user-level perspective on the Security API, see {{man|7|amanda-auth}}.


Both Amanda backup {{man|8|amdump}} and recovery process {{man|8|amrecover}} use this protocol. Psuedo code to how to do security setup on the Amanda server and security setup from Amanda service on the client are in the next two sections.
= Implementation =
 
The implementation of the Security API is some of the most tangled, buggy code you have ever seen.  It works, but only because the bugs are carefully balanced against one another.  See [[/Insanity]] for Dustin's notes on just how bad this is.
 
= Driver Details =
Amanda provides several bundled drivers, with names like "BSD", "BSDTCP", or "SSH".  This section describes the operation of these drivers, as bounded by TCP/IP (or UDP/IP) and the Amanda Protocol.
 
== BSDTCP ==
This driver multiplexes the protocol and all streams over a single TCP connection. This is accomplished using [[Security API/TCPM Framing|TCPM Framing]] to tokenize writes for each stream. A connection is initiated by making a TCP connection from a reserved port to port 10080 on the remote system.  In general, inetd or xinetd takes care of invoking amandad in response to this connection, but this is just an implementation detail.
 
Any REQ packet is prefixed before transmission with the string
SECURITY USER ''username''\n
 
On receipt, a REQ packet is scanned for this string, and the usual BSD, ahem, "authentication" steps are taken:
* verify that the peer port is reserved (less than 1024)
* verify forward and reverse DNS for the peer address match
* verify the supplied username appears in .amandahosts, paired with the matching hostname
 
== LOCAL ==
This is similar to BSDTCP, but instead of using a TCP connection, the initiator directly executes amandad with pipes set up for its input and output.  The protocol and all streams are multiplexed over these pipes, again using [[Security API/TCPM Framing|TCPM Framing]] to tokenize writes for each stream.
 
No authentication is performed.
 
== SSH ==
This is similar to LOCAL, but instead of invoking amandad directly, the initiator invokes an SSH command designed to execute amandad on the remote system.  Everything else is identical to LOCAL auth.


=== Starting amandad ===
Again, no authentication (beyond that done by SSH itself) is performed.
The mechanism by which amandad is started depends on the authentication.  For the BSD* authentications, it's started from (x)inetd with its stdin and stdout tied to the socket that triggered the startup.  For most other authentications, like ssh or local, it is started directly, but again stdin and stdout are used for communication.
 
== BSD, BSDUDP ==
 
These protocols are old, crusty, and difficult to debug.  Avoid using them, and certainly avoid implementing anything new with them!
 
= Protocol =
 
See [[Amanda Protocol]] for the protocol that is used with a security handle.
 
= Implementation =
 
== Starting amandad ==
 
In most cases, the target is implemented by amandad.  The mechanism by which amandad is started depends on the authentication.  For the BSD* authentications, it's started from (x)inetd with its stdin and stdout tied to the socket that triggered the startup.  For most other authentications, like ssh or local, it is started directly, but again stdin and stdout are used for communication.


For some authentication mechanisms, amandad expects to be started as root, and immediately drops root privileges.  For others, it expects to start as CLIENT_LOGIN.
For some authentication mechanisms, amandad expects to be started as root, and immediately drops root privileges.  For others, it expects to start as CLIENT_LOGIN.
Line 26: Line 66:
If any other command-line options are present, they are assumed to be service names, and amandad will only launch the named services.  <tt>amdump</tt> is a shortcut for the services required for amdump: <tt>noop</tt>, <tt>sendsize</tt>, <tt>sendbackup</tt>, and <tt>selfcheck</tt>.
If any other command-line options are present, they are assumed to be service names, and amandad will only launch the named services.  <tt>amdump</tt> is a shortcut for the services required for amdump: <tt>noop</tt>, <tt>sendsize</tt>, <tt>sendbackup</tt>, and <tt>selfcheck</tt>.


Once amandad is started, it begins listening for protocol packets on the [[Security API]] connection.  It creates streams as required by the service - see the [[Amandad Service Protocol]].
Once amandad is started, it begins listening for protocol packets.  It creates streams as required by the service - see the [[Amandad Service Protocol]].
 
== Using the API ==
 
The Security API is (as of this writing) only available from C code.  The interface is well-described in {{src|common-src/security.h}}.
 
=== Initiating a Security API Connection ===
To initiate a connection:


==Server pseudo code==
* Get a security_driver for the auth.
* Get a security_driver for the auth.
   serdrv = security_getdriver(auth);
   serdrv = security_getdriver(auth);
* Build a REQ string.
* Build a REQ string.
   req = "text for the REQ packet";
   req = "text for the REQ packet";
* Send the req to server_name, schedule program_response to be called on receipt of the REP packet.
* Send the REQ to ''server_name'' and schedule ''program_response'' to be called on receipt of the REP packet.
   protocol_sendreq(server_name, secdrv, generic_get_security_conf,
   protocol_sendreq(server_name, secdrv, generic_get_security_conf,
                   req, STARTUP_TIMEOUT, program_response, &response);
                   req, STARTUP_TIMEOUT, program_response, &response);
* It's protocol_run() that will do the amanda protocol exchange with the client, it will call program_response() when the REP packet is received.  
* It's protocol_run() that will do the amanda protocol exchange with the client; it will call ''program_response'' when the REP packet is received.  
   protocol_run();
   protocol_run();


* In program_response, you should
* In ''program_response'', you should
   check the REP packet
   check the REP packet
   call security_close_connection() if you don't plan to send another request to that host
   call ''security_close_connection'' if you don't plan to send another request to that host
   open the security_stream if needed.
   open any security streams required


* You should now register some function to read from all security_stream otherwise you will lose data
* You should now register some function to read from all security_stream otherwise you will lose data
Line 50: Line 96:
   event_loop(0);
   event_loop(0);


It's possible to not register readers for all stream and use security_stream_read_sync() which is like event_loop(0) but return as soon as something is read for that security_stream.
The {{man|8|amservice}} utility's source code provides a simple example of this process.
 
==Client pseudo code==
 
* On the client side, it's amandad does all the security setup, your service should only work with 8 fd opened by amandad.
  fd 0:  Where you read the REQ packet
    1:  Where to write your REP packet.
    50: writer fd for the first security_stream
    51: reader fd for the first security_stream
    52: writer fd for the second security_stream
    53: reader fd for the second security_stream
    54: writer fd for the third security_stream
    55: reader fd for the third security_stream
 
* Read the REQ packect from fd 0 (stdin) until EOF.
 
* Write the REP packet to fd 1 (stdout) and close it.  To stream data to the server, the REP packet must have a "CONNECT" line.
  printf("CONNECT TAG1 %d TAG2 %d\n", DATA_FD_OFFSET, DATA_FD_OFFSET+1);
The TAG can be anything, there is a maximum of 3 data streams.
 
See [[Amandad Service Protocol]] for more detail on this interaction.
 
== The API ==
 
The security API was designed to be a layer in between the core logic of Amanda and the transport and authentication of the protocol and dumps.
 
The component server and client programs now deal with abstract concepts instead of concrete udp and tcp handles.
 
The prefix "security_" is reserved for use as the namespace of this API.
protocol packet transmission functions
 
These functions exist for transmitting pkt_t's between the client and server.
 
These functions operate on security_handle_t objects. These objects are described later.
 
=== security_getdriver ===
 
const security_driver_t *security_getdriver(const char *drivername);
 
Given a security type ("KRB4", "BSD", "SSH", etc), returns a pointer to that type's security_driver_t (section 4.1), or NULL if no driver exists.
 
=== security_connect ===
 
void security_connect(const security_driver_t *h, const char *hostname, char *(*conf_fn)(char *arg, void *arg), void (*fn)(void *arg, security_handle_t *h, security_status_t s), void *arg);
 
Given a security driver, and a hostname, calls back with a security_handle_t (section 4.2) that can be used to communicate with that host. The status arg to the callback is reflects the success of the request. Error messages can be had via security_geterror().
 
This is expected to be the Amanda server's interface for setting up connections to clients.
 
conf_fn is used to determine configuration information. If NULL, no configuration information is available.
 
=== security_accept ===
 
void security_accept(const security_driver_t *h, int in, int out, void (*callback)(security_handle_t *, pkt_t *));
 
Given a security driver, an input file descriptor, and an output file descriptor, and a callback, when new connections are detected on the given file descriptors, the function is called with a newly created security handle and the initial packet received.
 
This is expected to be the Amanda daemon's interface for setting up incoming connections from the Amanda server. The file descriptors are typically 0 and 1 (stdin/stdout).
 
This function uses the event interface, and only works properly when event_loop() is called later in the program.
 
=== security_close ===
 
void security_close(security_handle_t *h);
 
Closes a connection created by a security_connect() or security_accept().
 
=== security_sendpkt ===
 
int security_sendpkt(security_handle_t *h, const pkt_t *pkt);
 
Transmits a pkt_t over a security handle. Returns 0 on success, or negative on error. A descriptive error message can be obtained via security_geterror().
 
=== security_recvpkt ===
 
int security_recvpkt(security_handle_t *h, void (*callback)(void *arg, pkt_t *pkt, security_status_t), void *arg, int timeout);
 
Requests that when incoming packets arrive for this handle, the given function is called with the given argument, the received packet, and the status of the reception.
 
If a packet does not arrive within the number of seconds specified in the 'timeout' argument, RECV_TIMEOUT is passed in the status argument of the timeout.
 
On receive error, the callback's status argument will be set to RECV_ERROR. An error message can be retrieved via security_geterror().
 
On successful reception, RECV_OK will be passed in the status argument, and the pkt argument will point to a valid packet.
 
This function uses the event interface. Callbacks will only be generated when event_loop() is called.
 
=== security_recvpkt_cancel ===
 
int security_recvpkt_cancel(security_handle_t *h);
 
Cancels a previous recvpkt request for this handle.
 
=== security_geterror ===
 
const char *security_geterror(security_handle_t *h);
 
Returns a descriptive error message for the last error condition on this handle.
 
=== security_seterror ===
 
void security_seterror(security_handle_t *h, const char *msg, ...);
 
Sets the string that security_geterror() returns.
 
=== security_handleinit ===
 
void security_handleinit(security_handle_t *, const security_driver_t *);
 
Initializes a security_handle_t. This is meant to be called only by security drivers to initialize the common part of a newly allocated security_handle_t.
 
== stream functions ==
 
These functions exist for transmitting random data over a stream-like connection.
 
These functions operate on security_stream_t objects, which are described later.
 
=== security_stream_server ===
 
security_stream_t *security_stream_server(security_handle_t *h);
 
Creates the server end of a security stream, and will receive a connection from the host on the other end of the security handle passed.
 
Returns a security_stream_t on success, and NULL on error. Error messages can be obtained by calling security_geterror() on the security handle associated with this stream.
 
=== security_stream_accept ===
 
int security_stream_accept(security_stream_t *);
 
Given a security stream created by security_stream_server, blocks until a connection is made from the remote end.
 
Returns 0 on success, and -1 on error. Error messages can be obtained by calling security_stream_geterror().
 
=== security_stream_client ===
 
security_stream_t *security_stream_client(security_handle_t *h, int id);
 
Creates the client end of a security stream, and connects it to the machine on the other end of the security handle. The 'id' argument identifies which stream on the other end to connect to.
 
Returns a security_stream_t on success, and NULL on error. Error messages can be obtained by calling security_geterror() on the security handle associated with this stream.
 
=== security_stream_close ===
 
void security_stream_close(security_stream_t *s);
 
Closes a security stream and frees up resources associated with it.
 
=== security_stream_auth ===
 
int security_stream_auth(security_stream_t *s);
 
Authenticate a connected security stream.
 
Returns 0 on success, and -1 on error. Error messages can be obtained by calling security_stream_geterror().
 
=== security_stream_id ===
 
int security_stream_id(security_stream_t *s);
 
Returns an identifier which can be used to connect to this security stream with security_stream_client().
 
Typical usage would be for one end of a connection to create a stream with security_stream_server(), and then transmit the id for that stream to the other side. The other side will then connect to that id with security_stream_client().
 
=== security_stream_write ===
 
int security_stream_write(security_stream_t *s, const void *buf, size_t bufsize);
 
Writes a chunk of data to the security stream. Returns 0 on success, or negative on error. Error messages can be obtained by calling security_stream_geterror().
 
=== security_stream_read ===
 
void security_stream_read(security_stream_t *s, void (*callback)(void *arg, void *buf, int bufsize), void *arg);
 
Requests that when data is ready to be read on this stream, the given function is called with the given arg, a buffer full of data, and the size of that buffer.
 
On error, the bufsize will be negative. An error message can be retrieved by calling security_stream_geterror().
 
This function uses the event interface. Callbacks will only be generated while in event_loop().
 
=== security_stream_read_sync ===
 
size_t security_stream_read_sync(security_stream_t *s, void **buf);
 
Return a buffer of data read from the stream. This function will block until something can be read, but other event will be fired. A pointer to the data is returned in *buf and the size of the buffer is returned.
 
On error, the size will be negative. An error message can be retrieved by calling security_stream_geterror(). This function uses the event interface.
 
=== security_stream_read_cancel ===
 
void security_stream_read_cancel(security_stream_t *s);
 
Cancels a previous read request.
 
=== security_stream_geterror ===
 
const char *security_stream_geterror(security_stream_t *h);
 
Returns a descriptive error message for the last error condition on this stream.
 
===security_stream_seterror ===
 
void security_stream_seterror(security_stream_t *h, const char *msg, ...);
 
Sets the string that security_stream_geterror() returns.
 
== Data Types ==
 
All visible data types are meant to be opaque to the caller. At no time should a caller have to access a member of an
y data type directly. The API should always be used instead.
 
===security_driver_t ===
 
This is a static object containing function vectors that implement the API for a particular security type.
 
===security_handle_t===
 
This is an object that describes a protocol connection to a remote server. There is one security_handle_t per request, and there can be many to the same remote host.
 
=== security_stream_t ===
 
This is an object that describes a data connection to a remote host. It is always associated and derived from a security_handle_t. Arbitrary data can be passed over a security stream.
 
=== security_status_t ===
 
This is an enumerated type that is passed to the callback of security_recvpkt and security_connect. The possible values it can have are:
 
S_OK - the pkt_t was received fine S_TIMEOUT - no pkt_t was received within the time specified in the timeout argument to security_recvpkt(). S_ERROR - an error occurred during reception. Call security_geterror() for more information.
 
== SECURITY DRIVERS ==
 
Each security type is defined by a struct of function vectors. These methods implement the details of this security type.
 
This section will document each element of security_driver_t.
 
=== name ===
 
const char *name;
 
This is the name of the driver. This is used by security_getdriver() to associate a name with a driver type.
 
=== connect ===
 
void (*connect)(const char *hostname, void (*fn)(void *, security_handle_t *, security_status_t), void *);
 
This is the implementation of security_connect(). It actually sets up the connection, and then returns a structure describing the connection. The first element of this structure MUST be a security_handle_t, because it will be cast to that after it is passed up to the caller.
 
The first argument is the host to connect to. The second argument is a function to call when a connection is made. The third argument is passed to the callback.
 
The callback takes three arguments. The first is the caller supplied void pointer. The second is a newly allocated security handle. The third is a security_status_t flag indicating the success or failure of the operation.
 
=== accept ===
 
void (*accept)(int in, int out, void (*callback)(security_handle_t *handle, pkt_t *pkt));
 
This is the implementation of security_accept(). It is passed the input and output file descriptors and a callback. The callback takes a security handle argument and also an initial packet received for that handle.
 
=== close ===
 
void (*close)(void *handle);
 
The implementation of security_close().
 
=== sendpkt ===
 
int (*sendpkt)(void *handle, pkt_t *pkt);
 
The implementation of security_sendpkt(). Security information is usually added by the driver before transmission.
 
=== recvpkt ===
 
void (*recvpkt)(void *handle, void (*callback)(void *arg, pkt_t *pkt, security_status_t), void *arg);
 
The implementation of security_recvpkt(). It will typically be layered onto the event interface somehow. It can assume that a caller will eventually call event_loop().
 
=== recvpkt_cancel ===
 
void (*recvpkt_cancel)(void *handle);
 
The implementation of security_recvpkt_cancel(). Drivers should allow this to be run even if no recvpkt was scheduled, or if one was previously cancelled.
 
=== stream_server ===
 
void *(*stream_server)(void *handle);
 
Implementation of security_stream_server(). This function returns a object describing the stream. The first member of this object MUST be a security_stream_t, because it will be cast to that.
 
=== stream_accept ===
 
int (*stream_accept)(void *stream);
 
After calling stream_server, stream_accept must be called on the stream before it is fully connected.
 
===stream_client===
 
void *(*stream_client)(void *handle, int id);
 
Implementation of security_stream_client(). The id argument is something returned by security_stream_id(). Again, the handle is referenced counted.
 
This function returns a object describing the stream. The first member of this object MUST be a security_stream_t, because it will be cast to that.
 
=== stream_close ===
 
void (*stream_close)(void *stream);
 
Close and free up resources for an open stream.
 
=== stream_auth ===
 
int (*stream_auth)(void *stream);
 
Authenticate a connected stream.
 
=== stream_id ===
 
int (*stream_id)(void *stream);
 
Return a unique id for this stream. This is to be used by stream_client() to connect to this stream.
 
=== stream_write ===
 
int (*stream_write)(void *stream, const void *buf, size_t bufsize);
 
Implementation of security_stream_write.
 
=== stream_read ===
 
void (*stream_read)(void *stream, void (*callback)(void *arg, void *buf, int bufsize), void *arg);
 
Implementation of security_stream_read.


=== stream_read_cancel ===
It's possible to not register readers for all stream and use security_stream_read_sync() which is like event_loop(0) but returns as soon as something is read for that security_stream.


void (*stream_read_cancel)(void *stream);
=== Implementing a Service ===


Implementation of security_stream_read_cancel.
See [[Amandad Service Protocol]] for more detail on the interactions between amandad and service executables.

Latest revision as of 04:23, 14 October 2010

Introduction

This is a document of the API for defining and utilizing multiple security and transport mechanisms for the Amanda network protocol. The goal of this API is to allow several different forms of communication and authentication to exist between the Amanda server and its clients. This is variously known as the "Secure API", "Security API", and "Authentication"

Overview

The security API connects two Amanda processes over the network. One process, the initiator, connects to the other, the target. During backup operations, the initiator is the server. For amrecover(8), the client is the initiator.

Once two processes are connected and any necessary authentication has taken place, the communication channel consists of a message-based protocol coupled with an arbitrary number of streams. The protocol is used to set up any streams that will be used in a conversation. In practice, the initiator of a connection can be any application, the target is always an amandad instance. Amandad implements the Amanda protocol, and implements numerous special cases for common Amanda applications.

Note that streams cannot be half-closed. All uses of streams in Amanda are currently terminated by the service, although nothing in the protocol itself dictates this.

For the user-level perspective on the Security API, see amanda-auth(7).

Implementation

The implementation of the Security API is some of the most tangled, buggy code you have ever seen. It works, but only because the bugs are carefully balanced against one another. See /Insanity for Dustin's notes on just how bad this is.

Driver Details

Amanda provides several bundled drivers, with names like "BSD", "BSDTCP", or "SSH". This section describes the operation of these drivers, as bounded by TCP/IP (or UDP/IP) and the Amanda Protocol.

BSDTCP

This driver multiplexes the protocol and all streams over a single TCP connection. This is accomplished using TCPM Framing to tokenize writes for each stream. A connection is initiated by making a TCP connection from a reserved port to port 10080 on the remote system. In general, inetd or xinetd takes care of invoking amandad in response to this connection, but this is just an implementation detail.

Any REQ packet is prefixed before transmission with the string

SECURITY USER username\n

On receipt, a REQ packet is scanned for this string, and the usual BSD, ahem, "authentication" steps are taken:

  • verify that the peer port is reserved (less than 1024)
  • verify forward and reverse DNS for the peer address match
  • verify the supplied username appears in .amandahosts, paired with the matching hostname

LOCAL

This is similar to BSDTCP, but instead of using a TCP connection, the initiator directly executes amandad with pipes set up for its input and output. The protocol and all streams are multiplexed over these pipes, again using TCPM Framing to tokenize writes for each stream.

No authentication is performed.

SSH

This is similar to LOCAL, but instead of invoking amandad directly, the initiator invokes an SSH command designed to execute amandad on the remote system. Everything else is identical to LOCAL auth.

Again, no authentication (beyond that done by SSH itself) is performed.

BSD, BSDUDP

These protocols are old, crusty, and difficult to debug. Avoid using them, and certainly avoid implementing anything new with them!

Protocol

See Amanda Protocol for the protocol that is used with a security handle.

Implementation

Starting amandad

In most cases, the target is implemented by amandad. The mechanism by which amandad is started depends on the authentication. For the BSD* authentications, it's started from (x)inetd with its stdin and stdout tied to the socket that triggered the startup. For most other authentications, like ssh or local, it is started directly, but again stdin and stdout are used for communication.

For some authentication mechanisms, amandad expects to be started as root, and immediately drops root privileges. For others, it expects to start as CLIENT_LOGIN.

The program takes the following command-line arguments:

-auth=$auth
specifies the authentication mechanism in use (default "BSD")
-no-exit
do not exit when all requests have been satisfied (connection-oriented authentications only; all others wait 30s after the last packet before exiting)
-tcp=$port
-udp=$port
bind to and listen on a port (for debugging of BSD* auths)

If any other command-line options are present, they are assumed to be service names, and amandad will only launch the named services. amdump is a shortcut for the services required for amdump: noop, sendsize, sendbackup, and selfcheck.

Once amandad is started, it begins listening for protocol packets. It creates streams as required by the service - see the Amandad Service Protocol.

Using the API

The Security API is (as of this writing) only available from C code. The interface is well-described in common-src/security.h.

Initiating a Security API Connection

To initiate a connection:

  • Get a security_driver for the auth.
 serdrv = security_getdriver(auth);
  • Build a REQ string.
 req = "text for the REQ packet";
  • Send the REQ to server_name and schedule program_response to be called on receipt of the REP packet.
 protocol_sendreq(server_name, secdrv, generic_get_security_conf,
                  req, STARTUP_TIMEOUT, program_response, &response);
  • It's protocol_run() that will do the amanda protocol exchange with the client; it will call program_response when the REP packet is received.
 protocol_run();
  • In program_response, you should
 check the REP packet
 call security_close_connection if you don't plan to send another request to that host
 open any security streams required
  • You should now register some function to read from all security_stream otherwise you will lose data
 security_stream_read(security-stream, reg_read_function,)
  • Process all security events
 event_loop(0);

The amservice(8) utility's source code provides a simple example of this process.

It's possible to not register readers for all stream and use security_stream_read_sync() which is like event_loop(0) but returns as soon as something is read for that security_stream.

Implementing a Service

See Amandad Service Protocol for more detail on the interactions between amandad and service executables.