To understand this document

This document describes how a remote instance of op's jacket stamp may be used to provide remote authorization for a client. The roapmux service mocks a stampctl tableau based on the login, groups, netgroups, domain, and query, presented by a specific client host.

If you have never read the jacket HTML document, please read it before you continue with this document. Also the stamp(8) and stampctl(7) manual pages might provide more context.

To install this service you must be able to edit system configuration files (viz. /etc/inetd.conf, /etc/tcpmux.conf, or /etc/xinetd.d/roapmux.conf). Then you must be able to restart (reload) the apropos service.

Most implementations of inetd had an implementation of tcpmux included, until recently. Since some versions of xinetd do not include one, so I coded a stand-alone version that you may configure to support local services. See tcpmux, if your local version of inetd doesn't internally support the fabulous RFC1078 mux.

What is roapmux?

The manual page describes roapmux as providing "authorization tableau to specific network clients". This allows recognized clients to request a list of abstract tokens. Each of these tokens represents an authorization to perform some (set of) privileged commands (usually under op or sudo). The client must have an IP address that reverse-maps to a hostname contained in the meta-configuration data on the server, and it must specify a legitimate combination of credentials.

This sounds like it could be a security issue, but a list of abstract tokens is hardly confidential, when sent over a local network -- if you can't trust a local IP address, you have larger issues.

When would a client connect to roapmux?

Clients connect to roapmux to pull a current list of the actions some person has granted to some other person (or automation) for a limited window. That list could be fairly static, or completely dynamic.

The default generator is hxmd, which provides direct access to m4, make, and sh processing. An option is available to replace the default processor, but it might be better to code a replacement service, if you don't like hxmd.

Setup your own instance

As the superuser, I added this line to /etc/tcmpmux.conf (note that the backslashes should be removed, as it is really a single line):
tcpmux/roap stream tcp nowait ksb:source /usr/local/libexec/roapmux \
	roapmux -C/tmp/ksb/test.cf -R/tmp/ksb/test.rev ENV1=testing \
	/tmp/ksb/gen.sh /tmp/ksb/data.host
That is a pretty long spell, but it allows you to test most of the features you'll need. You'll need to replace "ksb:source" with your mortal account and a valid group.

Then I built test files to make that work:

$ mkdir /tmp/ksb
$ cd /tmp/ksb
$ touch test.cf test.rev data.host gen.sh
$ chmod +x gen.sh
$ vi test.cf
	%HOST
	localhost
	w02.example.com
$ vi test.rev
	# $127.0.0.1(*): ${echo:-echo} localhost
	# $*(*): ${echo:-echo} w02.example.com
$ vi data.host
	dnl map a host to a realm, test with earth for now.
	`REALM=earth
	LOGIN='ROAP_LOGIN`
	'ifelse(-1,index(` 'ROAP_GROUPS` ',` wheel '),,`SUPER=yes
	')dnl
$ vi gen.sh
	#!/usr/bin/env ksh
	# Below we set: max connect, session, idle
	echo "+10,180,180"
	cat "$@"
	exit 0

Then I tested those with a spell which mocks most of the command-line options roapmux passes hxmd:

$ hxmd -G w02.example.com -C./test.cf -D!ROAP_LOGIN=test gen.sh data.host
REALM=earth
LOGIN=test
$ 
That proves that we should see something sane. Then I tested the tcpmux interface to this:
$ muxcat localhost roap "test:charon::"
REALM=earth
LOGIN=test
$ 
That shows the basic mechanics of the data-flow. Then I put my test account in group wheel:
$ muxcat localhost roap "test:charon wheel::"
REALM=earth
LOGIN=test
SUPER=yes
$ 

This shows how a change in the credentials sent might change the output authorization tokens.

These are just test values, but they do represent most of the features you need to scale this out. Assume that the credentials passed are from a trusted source, because if they are not, it doesn't matter. If a Bad Guy pokes at your authorization service for hours they may find out the names of several authorization tokens that some credentialed logins have access to -- but those tokens do not have to have meaningful names -- even if they do, the information radiated is less useful than the list of people in groups 0, staff or operator (which are all in the /etc/group file).

How do clients connect to roapmux?

Clients may connect with muxcat, as above; see muxcat(1l). However, most access is via stampctl's -R option.

Stampctl builds a credential list from the real uid, gid, and the list returned by getgroups, the information in the local /etc/netgroup file, and the YP/NIX domain fetched from getdomainname (or sysctl's MIB kern.domainname). The last field query is a token passed to the generator that has local meaning. The query is sent in plain-text, so include only public data.

To fetch the tableau from roapmux, it opens a connection to the roap server specified under -R. It presents the service name "roapmux" to the mux service, which should reply with a positive service start. By convention the positive reply is the name the service believes the client to be. The client presents the credentials collected above. The service replies with a positive reply, which may include suggested limits on the life of the credentials, followed by a list of tableau entries.

How does it provision a client?

This program provides the structure to complete the mux protocol required to process incoming client requests, but it does not provide any site policy. That policy is provided by logic constructed around an hxmd instance.

The selection of hxmd as the driver provides these facilities:

Host selection via -G
This is provided by the hostname derived from the client's incoming IP address. This allows the generator logic to focus on providing a recognized client with the correct authorization tableau.
Pre-processes parameters via -D
After parsing the clients credentials, roapmux converts them into m4 macro definitions under -D!ROAPMUX_LOGIN=login and the like. See below.
Access to meta data via -C, -Z, and -X
These options may be presented (once) as options to roapmux, and presented multiple times as part of the generator specification. A single selected host specification (under hxmd's -G option) selects all the meta-data for the client host.
Error recovery via -N, -K/-Q/-r
When the selected host is not in the base configuration, these options provide an opportunity to send a default or error tableau.
Flexible interface under -F
The generator command may have more (or fewer) literal options and markup files, or even include cache directories (see hxmd's HTML document).

The structure of the generator options to hxmd specify the files, cache directories, and commands that form the reply. Those elements combine to do the "heavy lifting" of forming the per-request policy, rejecting disallowed authorizations, and reporting access requests.

The command-line presented to hxmd looks like:

hxmd -Greverse \
	-C config \
	-D!ROAP_HOST=reverse \
	-D!ROAP_IP=IP \
	[-D!ROAP_LOGIN=login] \
	[-D!ROAP_GROUPS=groups] \
	[-D!ROAP_NETGROUPS=netgroups] \
	[-D!ROAP_DOMAIN=domain] \
	-D!ROAP_QUERY=query \
	-D!ROAP_MASK=mask \
	[-X ex-config] \
	[-Z zero-config] \
	generator
With HXMD_LIB set by -L and other environment variables set by envs

The explict macro definitions on the command line (above) are passed down to allow the generator to expose only the authorization tokens allowed to the client. Several are left undefined when they are presented as either the empty string or the sentinel string dot (.). Those are ROAP_LOGIN, ROAP_GROUPS, ROAP_NETGROUPS, and ROAP_DOMAIN. The host and IP are always passed down.

The exclaimation point markup in the definition prevents these values from being passed on under hxmd's -o option. This means any transfer of these to a child process or recursive call to hxmd must explicitly send them down.

More testing with our local instance

I put a pstree command in my generator script to trace the process tree. Here is the relevant output (I truncated the long lines):
`-xinetd -stayalive -pidfile /var/run/xinetd.pid
    `-perl /usr/local/libexec/roapmux -C/tmp/ksb/test.cf -R/tmp/ksb/test.re
        `-hxmd -G w02.example.com -C/tmp/ksb/test.cf -D!ROAP_HOST=w02.examp
            |-hxmd -G w02.example.com -C/tmp/ksb/test.cf -D!ROAP_HOST=w02.e
            `-xclate -ms -N >&6 -- xapply -fzmu -sP6 -2 %+ - -
                `-xapply -fzmu -sP6 -2 %+ - -
                    `-xclate -s 0
                        `-ksh /tmp/ksb/gen.sh /tmp/hxtfKjejjx/K7RZ5i/data.h
                            `-pstree -Aa
This shows that the process tree is not "simple", but the complexity is managed by hxmd.

I have yet to find an authorization rule I cannot encode using this structure. Here are some example support functions I include from the generator command-line:

dnl $Id...
dnl roap support macros						(ksb)
dnl
dnl Does client have any named group?  If so output "$1", else nothing.
pushdef(`rp_in_group',`ifelse(-1,index(` 'ROAP_GROUPS` ',` '$2` '),`ifelse($#,2,`',`rp_in_group($1,shift(shift($@)))')',`$1')')dnl
dnl
dnl Does client have any named netgroup? Same as rp_in_group mostly.
pushdef(`rp_in_netgroup',`ifelse(-1,index(` 'ROAP_NETGROUPS` ',` '$2` '),`ifelse($#,2,`',`rp_in_netgroup($1,shift(shift($@)))')',`$1')')dnl
dnl
dnl Does client need us to check ypcat for their domain?
pushdef(`rp_have_nis_groups',`rp_in_netgroup(`ifdef(`ROAP_DOMAIN',`$1')',`+')')dnl
dnl
dnl Withdraw the remote op authorization protocol macro support
pushdef(`rp_pop',`popdef(`rp_in_group')popdef(`rp_in_netgroup')popdef(`rp_pop')popdef(`rp_have_nis_groups')popdef(`rp_pop')')dnl
Given those support macros it is easier to read rules like these:
dnl Can client su to anyone?
dnl  On Solaris hosts use group 0 as root, otherwise 0 is wheel;
dnl  members of group 0 can su to anyone (via a superuser shell).
rp_in_group(`SU_any=yes
',ifelse(HOSTTYPE,SUN5,`root',`wheel'))dnl
dnl
dnl Database support staff can su to sqladm
rp_in_group(`SU_sqladm=yes
',dba,dbm,wheel,root)dnl
dnl
dnl Anyone in hostmaster can reload the nameserver
rp_in_group(`CTL_named=yes
',hostmast,hostmaster,root,wheel)dnl
dnl
dnl The owner of a workstation can reboot it
rp_in_netgroup(`REBOOT=yes
',workstation)dnl
dnl If netgroups are not local look at ypcat domain....

If you don't like m4 for mapping, you can use any language you like, the driver script could be the only item in the generator command, and that program could be any shell command you like. Getting the params from hxmd is easy. Let's build an m4 parameter file to build a configuration file you can read, I'll make one of the "ini" style files:

$ vi data.host
	dnl $Id...
	dnl get params from hxmd
	`[client]
	'ROAP_HOST
	`[ip]
	'ROAP_IP
	ifdef(`ROAP_LOGIN',``[login]
	'ROAP_LOGIN
	')dnl
	ifdef(`ROAP_GROUPS',``[groups]
	'patsubst(ROAP_GROUPS
	,` ',`
	')')`'dnl
	ifdef(`ROAP_NETGROUPS',``[netgroups]
	'patsubst(ROAP_NETGROUPS
	,` ',`
	')')`'dnl
	ifdef(`ROAP_DOMAIN',``[doamin]
	'ROAP_DOMAIN
	')dnl
	dnl We actually always get a query, but be paranoid.
	ifdef(`ROAP_QUERY',``[query]
	'ROAP_QUERY
	')dnl
	dnl if we were called under -M we get one of these
	ifdef(`ROAP_MASK',``[mask]
	'ROAP_MASK
	')dnl
	dnl no other data to convert here, except host macros
$ muxcat localhost roap "test:charon wheel:::thing"
[client]
w02.example.com
[ip]
127.0.0.1
[login]
test
[groups]
charon
wheel
[query]
thing
$ 
Replace the cat shim with any command that takes the "ini" format file and you win. Adapters are nice to get started, eventually you'll just code the rule in m4.

More power might be too much to handle

If you really want to go for broke, you might build cache directory logic for some of your rules. This allows authorizations to have more persistent state, cleanup processes, and some much complexity you'll never explain it to anyone else, even with the make recipe and all the documentation in the world. I've tried, but it is always an option. On the plus side, it allows the use of nuclear weapons against the problem at hand.

On the otherhand, building a shell script with m4 markup is more traditional. So let's play it that way first.

Recall that stampctl can fetch values from an existsing stamp under -Q. If the query string (which has no other specified meaning) is taken as a key to find a local stamp socket, then the authorization may be based on the tableau of that stamp.

Say the query string is the relative name of a authorizing stamp under /var/local/creds. We'll ask that stamp for the tableau entry for OK_login. I'll use some m4 in data.host to build a command to find such an authorization stamp. While roapmux strips shell meta-characters from the (untrusted) login and query values it doesn't know how the generator might use the query. So we'll have to check for directory climbing ourselves:

$ vi data.host
	dnl lookup local authorization for login from query's stamp	(ksb)
	ifelse(-1,regexp(ROAP_QUERY,`^\.\./|/\.\./|/\.\.$'),`',
	`errprint(`query may not include dot-dot in path')m4exit(78)')dnl
	ifdef(`ROAP_QUERY',``stampctl -Q /var/local/creds/'`'dnl
	patsubst(ROAP_QUERY,` .*',`')` OK_'ifdef(`ROAP_LOGIN',`ROAP_LOGIN',`root')')`
	'dnl
$ muxcat localhost roap "test:charon wheel:::thing"
stampctl -Q /var/local/creds/thing OK_test
$ muxcat localhost roap "test:charon wheel:::../other/thing"
muxcat: localhost: test:charon,wheel:::../other/thing: query may not include dot-dot in path
If that didn't produce the failure we expected, then you have a version of m4 which doesn't have the regexp macro. Install a better version of m4, like the FreeBSD one (I fixed math in that one too). Then set the PATH in the tcpmux.conf line (replace "ENV1=testing" with the new PATH assignment). the command, because we didn't setup the "thing" stamp. Build one with stampctl's -M option and you can actually run the file specified by $1 in gen.sh to fetch the value.

That puts more of the checking in shell code, which is harder to make really secure. At that point I'd use the shell shim to call Perl, C, or Python.

What if the reverse lookup for my hosts doesn't work?

This is actually quite common, so I fixed it. Provide a reverse map file under -R on the roapmux command-line. This forces the mux to call mk to select a shell command from the reverse file to map the host to the correct name in the config file:
mk -s -l0 -mhostname -sIP-address -DCONFIG=config reverse
Note that the hostname will be @ for unmapped IP addresses.

If the reverse specified on the command line is dot (.), then the configuration file is searched for a matching marked line. All the normal mk judo works: build a script to find the name or match it from a map file of regular expressions, or chain to some other program. The client connection is actually still open on stdin, but this is not a good way to augment the protocol, really.

Example reverse map file

The reverse map structure is exactly the same as the one used in msrcmux, so see the description in that HTML document. That document also explains how to configure xinetd to provide RFC1078 service.

Summary

The roapmux service provides client-based limits for authorization access, granular to login, group, netgroup, domain, and a client provided query string. The client's op configuration authenticates the request, runs stampctl -R to request an authorization tableau, thus additional authorization (access) is allowed for the life of the stamp.
$Id: roapmux.html,v 1.3 2012/11/02 21:14:30 ksb Exp $