Op
uses syslog
(3) to
log every access attempt to LOG_AUTH
and
it always uses the name "op" (even when called by another name).
I direct this to the system console, not a local file or network service. Then I pick it up on the serial port and log it through my console server (see conserver.com for a more widely supported version than mine). This prevents Bad Guys from covering their tracks by network attack or by deletion of the log files (since they are not on a host that is on the same network as the host they are attacking).
I then produce reports that show who is assuming which role and how often they do it. This helps close-the-loop on broken applications, abuse of access, and some other political issues.
While this is pretty easy to setup, it is beyond the scope of this document.
Return to the main page.
Op
's configuration file syntax is designed to
be overly simple.
By keeping the syntax super simple we are trying to limit the
chance of complex expressions leading to breaches in security policy.
We want the op
"firewall" to
keep Bad Guys from gaining access they shouldn't have, while
allowing Good Customers to work without useless constraints.
I looked at the usage of many other similar programs
(super
,
sudo
,
pfexec
,
sud
,
sudoscript
,
other versions of op
) to see what features
they provided that this version of op
lacked.
After that review I added the in-line script feature, which I still
believe is a mistake. I liked some of the features
of sud
, but I think jackets actually do about the same
task (run a non-setuid program to authorize setuid access).
If you want to know what an ecalated process did, then use
snoopy
's
preload to get an acurate trace. See the
snoopy homepage
for more details.
Remember to duplicaterulename command ; ... $LD_PRELOAD=/usr/local/lib/snoopy.so
syslogd
's
authpriv.info
off-host to make it harder to
tamper with the stream.
#
) to end-of-line, and are removed
like most UNIX ™ configuration files.
The octothorp is not special when contained in
a word
.
A word
is a sequence of non-white-space
characters. A mnemonic
is limited
in that it must start in column 1, and that, in
addition to white-space, it is also terminated by either a
semicolon (;
)
or an ampersand (&
).
All words are broken at white-space: there is no quote convention
to protect white-space inside a word
.
There is one other lexical construct: an in-line script.
An in-line script is only parsed as the next item immediately after
a mnemonic
. It groups shell code between
curly braces to form a block of shell code.
It begins with an open curly ({
) as
a word
and ends at the next line which starts with
a close curly (}
) as the first non-white-space
character. The delimiting curly braces are not included in the resulting
text.
As a BNF:
file
::=rule
* ;
rule
::=DEFAULT
option
* |mnemonic
command
arg
* (;
|&
)option
* ;
option
::=word
;
arg
::=word
;
command
::=word
| '{' ... '\n' white-space * '}' ;
arg
or option
words as they are read. The semantic analysis is deferred until after
all the files are input, up until that time all arg
,
option
, and command
terms are just words
.
The special DEFAULT
form omits the
command
part from the rule stanza: this is
because it only expresses default options
for subsequent rules (so it doesn't need any args
).
The semicolon (;
) that terminates
the arg
list may be expressed as
an ampersand (&
) to force a
daemon
option into
the option
list. This shorthand
is clearer in the rule-base when it is repeated for many commands.
In the normal command mode op
examines each
rule in turn looking for a literal string match for the requested
mnemonic
against all those listed in the
rule-base. Only if one matches, are any of
the options
examined. At that point
only the $# and
$
N
options are analyzed (as well as any !#
or !
N
).
Only when the command requested has the correct number of parameters
and matches the command-line arguments are any
DEFAULT
options merged into the rule.
All subsequent authorization and command construction only
looks at that single rule. For all intents the rest of the
rule-base is forgotten.
A sanity check report (under -S
) processes each
rule by merging its DEFAULT
before it
is processed. The merged rule is checked for many possible configuration
botches. Similarly the list options (-l
,
-r
, and -w
) display
their output based on merged stanza. The lists include inaccurate results if
any of $#
, !#
,
$
N
, or
!
N
,
are applied from a DEFAULT
stanza. But
-S
warns of this, so check that first.
In general anything with a leading dollar sign ($
)
refers to the textual value of a term, a leading percent
(%
) refers to the term's
value in a wider or meta sense (%f.perms
matches
the file permission of -f
's
file
parameter), and a leading
exclaim (!
) refers to that wider value in
the negative (!f.perms
limits file permission).
Return to the main page.
#!/bin/sh # $Id: showme.sh,v 2.29 2008/12/31 20:51:12 ksb Exp $ # $Doc: sed -e 's/&/A''MPamp;/g' -e 's/</\\</g' -e 's/>/\\>/g' <%f| sed -e 's/[A][M][P]/\\&/g' ( if [ $# -eq 0 ] ; then echo "Process #$$ was executed as \"$0\", with no arguments" else echo "Process #$$ was executed as \"$0\", with arguments of" for A do echo "$A" |sed -e 's/\([\\"$`]\)/\\\1/g' -e 's/^/ "/;s/$/"/' done fi echo " as login" `id -rnu`", effective" `id -nu`" [uid" \ `id -ru`", "`id -u`"]" echo " with group" `id -rng`", effective" `id -ng`" [gid" \ `id -rg`", "`id -g`"]" echo " supplementary groups:" `id -nG` "[`id -G`]" echo ' $PWD='`pwd` echo ' umask '`umask` renice 0 $$ 2>&1 |sed -e 's/old/nice/' -e 's/,.*//' env ) |${PAGER:-more} exit 0
Return to the main page.
op
to deny or allow an escalation.
Jacket processes take much the same arguments, but may
persist while the escalated access is running. The template
code in this section may be adapted to serve as either a
jacker or a helmet.
The example jacket parses the command-line options, offers
-h
, and -V
as
all good user interfaces should, and has comments in spots where
you should edit to make your checks.
Copy the jacket.pl file to a working file,
edit it and search for each check-point to add your checks.
/CHECKS AND REPARATIONS
stdout
, before any
exit (as all the check above did).
/CAPTURE START DATA
exit
'd.
/CLEANUP
$status
holds the exit
code, while
$?
holds the raw wait
status. Log anything you need to log, cleanup anything as needed.
If you need to exit non-zero because the access failed this would be the place.
$Id: ...
) to your local flavor.
Feel free to leave a credit in for the template, if you like.
Check it into your local revision control system and install it as local policy demands.
#!/usr/bin/perl -T # An example perl jacket/helmet script (parses the options for you). (ksb) # Note that this code will most be run under the taint rules, see perlsec(1). # KS Braunsdorf, at the NPCGuild.org # $Doc: sed -e 's/&/A''MPamp;/g' -e 's/</\\</g' -e 's/>/\\>/g' <%f| sed -e 's/[A][M][P]/\\&/g' use lib '/usr/local/lib/sac/perl'.join('.', unpack('c*', $^V)), '/usr/local/lib/sac'; use Getopt::Std; use strict; my($hisPath) = $ENV{'PATH'}; $ENV{'PATH'} = '/usr/bin:/bin:/usr/local/bin:/usr/local/sbin:/sbin'; my($progname, %opts, $usage); $progname = $0; $progname =~ s/.*\///; getopts("VhP:u:g:f:R:C:", \%opts); $usage = "$progname: usage [-P pid] [-u user] [-g group] [-f file] [-R root] ". "-C config -- mnemonic program euid:egid cred_type:cred"; if ($opts{'V'}) { print "$progname: ", '$Id: ...', "\n"; exit 0; } if ($opts{'h'}) { print "$usage\n", "C config which op configuration file sourced the rule\n", "f file the file specification given to op, as an absolute path\n", "g group the group specification given to op\n", "h standard help output\n", "P pid the process-id of the jacketed process (only as a jacket)\n", "R root the directory we chrooted under\n", "u user the user specification given to op\n", "V standard version output\n", "mnemonic the requested mnemonic\n", "program the program mapped from the mnemonic\n", "euid:egid the computed effective uid and gid\n", "cred_type the credential type that granted access"," (groups, users, or netgroups)\n", "cred the matching group, login, or netgroup\n"; exit 0; } my($MNEMONIC, $PROGRAM); shift @ARGV if ('--' eq $ARGV[0]); if (scalar(@ARGV) != 4) { print STDERR "$progname: exactly 4 positional parameters required\n"; print "64\n" if $opts{'P'}; exit 64; } if ($ARGV[0] !~ m|^([-/\@\w.]+)$|o) { print STDERR "$progname: mnemonic is zero width, or spelled badly\n"; print "64\n" if $opts{'P'}; exit 64; } $MNEMONIC = $1; if ($ARGV[1] !~ m|^([-/\@\w.]+)$|o) { print STDERR "$progname: program specification looks bogus\n"; print "64\n" if $opts{'P'}; exit 64; } $PROGRAM = $1; if ($ARGV[2] !~ m/^([^:]*):([^:]*)$/o) { print STDERR "$progname: euid:egid $ARGV[2] missing colon\n"; print "65\n" if $opts{'P'}; exit 65; } my($EUID, $EGID) = ($1, $2); if ($ARGV[3] !~ m/^([^:]*):([^:]*)$/o) { print STDERR "$progname: cred_type:cred $ARGV[3] missing colon\n"; print "76\n" if $opts{'P'}; exit 76; } my($CRED_TYPE, $CRED) = ($1, $2); # Now $MNEMONIC is mnemonic, $PROGRAM is program, also $EUID, $EGID, # $CRED_TYPE, $CRED are set -- so make your checks now. # # There are 5 actions you can take, and leading white-space is ignored: # 1) As above you can output an exit code to the process: # print "120\n"; # 2) You can set an environment variable [be sure to backslash the dollar]: # print "\$FOO=bar\n" # The same line without a value adds the client's $FOO (as presented): # print "\$FOO\n"; # 3) You can remove any environment variable: # print "-FOO\n"; # 4) You can send a comment which op will output only if -DDEBUG was set # when op was built [to help you, Mrs. Admin]: # print "# debug comment\n"; # 5) You and send a redirection of stdin, stdout, stderr: # print "&0</dev/null\n"; # stdin from the null device # print "&1>$SOME_FILE\n"; # print "&2>/dev/null\n"; # Any descriptor above 2 is taken as the lower bound of fds to allow # to be passed to the escalated process: # print "&3\n"; # close fds 3 and above # If you want the escalated process to read from this process build a # domain socket (/tmp/myselfXXXXXX), and listen on it, then redirect # stdin (0) from it (output is parallel): # print "&0</tmp/myselfXXXXXX\n"; # 6) Use op to signal your displeasure with words, making op prefix your # comment with "op: jacket: " ("op: helmet: "): # print "Permission lost!\n"; # (This suggests an exit code of EX_PROTOCOL.) # # Put your checks and payload here. Output any commands to the co-process, # be sure to send a non-zero exit code if you want to stop the access! # CHECKS AND REPARATIONS #e.g. check LDAP, kerberos, RADIUS, or time-of-day limits here. # If we are a helmet you can just exit, if you exit non-zero op will view that # as a failure to complete the access check, so it won't allow the access. exit 0 unless $opts{'P'}; # We must be a jacket, and the requested access is not yet running. # You could set a timer here, or capture the start/stop times etc. # CAPTURE START DATA #e.g. call time or set an interval timer #e.g. block signals # Let the new process continue by closing stdout, if the last exitcode # you wrote to stdout was non-zero op won't run the command, I promise. open STDOUT, ">/dev/null"; # We can wait for the process to exit, we are in perl because the shell # (ksh,sh) can't get the exit code from a process it didn't start. my($kid, $status); $kid = waitpid $opts{'P'}, 0; $status = $? >> 8; # Do any cleanup you want to do here, after that the jacket's task is complete. # Mail a report, syslog something special, restart a process you stopped # before the rule ran, what ever you need. On a failure you should exit # with a non-zero code here. # CLEANUP #e.g.: print STDERR "$kid exited with $status\n"; #e.g.: use Sys::Syslog; ... log something clever # This is the exit code that goes back to the client, since this jacket # became the op process (as all jackets do). exit 0;
We could drop uid to a vanilla login (like nobody
)
as soon as we don't need special permissions. That would be a good idea,
if you can manage it. There is a fine line here, you don't really want
to drop to the original login's uid, because then they can mess with
your process and the point of a jacket is that the client can't
ptrace
(2) you.
The implementation of pam
sessions
in op
holds yet another
process open (because we execve
(2),
for escalated program), so op
can
call pam_close_session
(3). In that
case your jacket is wrapping a built-in jacket.
Return to the main page.
%A Tom Christiansen %T Op: A Flexible Tool for Restricted Superuser Access %P 89-94 %I USENIX %B Large Installation Systems Administration III Workshop Proceedings %D September 7-8, 1989 %C Austin, TX %W Convex Computer Corporation
We dividing functions that might give away unlimited privilege into
the necessary separate steps. Each step is represented by a rule in
the op
rule-base or my an external process
run by a different group.
We delegate the tasks to different people (or teams) to
provide an organizational barrier to fraud and role confusion.
We assign access to each rule base on a separation of the roles:
op
binary or
syslog
configuration, but then other audit
cycles would catch that. The audit of access logs falls to another group:
it is published globally for all to review.
The publication of all escalations (at least tally for each group or person) keeps management aware of the process and the volume of changes.
For the reasons below I try not to put in-line scripts in any access rule-base.
/usr/local/libexec/op
) then
the Bad Guy can't see the text of the script to aid her in
subornation of the code. If you put the code in-line it shows
up in ps
output while running.
-V
and each non-program file holds a revision tag in comments at
the top of the files. By putting code without
the -V
hook in the configuration file
I am overloading the revision tag in that file to denote
both the revision of the rule-base and the revision of the code.
sh
,
bash
, ksh
,
csh
, or perl
) in
the configuration file we confuse the quoting rules with
op
's lack of quotes. I see a larger
number of misspelled rules when in-line scripts are included.
This issue is not as clear at a small site where
the op
policy is coded by
the same administrator that would code any in-line script.
op
do the same thing
op
, so I used curly braces. My bad.
Finally it is easy to make the rule-base work without them:
here is an example from another version of op
:
umount ... case $1 in cdrom) /sbin/umount /mnt/cdrom ;; dvd) /sbin/umount /mnt/dvd ;; burner) /sbin/umount /mnt/burner ;; *) echo "op: you do not have permission to unmount \'$1\'" ;; esac
In this version of op
I would use an RE match
on $1
:
umount /sbin/umount /mnt/$1 ; $1=^(cdrom|dvd|burner)$ uid=root gid=operator
Or if I need to limit this to different Customer populations I might use two netgroups:
umount /sbin/umount /mnt/$1 ; $1=^(cdrom|dvd)$ netgroups=readers uid=root gid=operator umount /sbin/umount /mnt/$1 ; $1=^(burner)$ netgroups=writers uid=root gid=operator
This also gives the Customer a better usage message under
-l
and -r
because
it shows the Customer only what they can do, and with a shell-like
usage format:
$ op -l op umount cdrom|dvd ...
Return to the main page.
Start with the assumption that the rule-base is distributed based on the "type" of host that needs the rules, don't assume that the same files are installed on every host, or that the whole rule-base must be defined in a single file. This allows you to use the same mnemonic name on more than one class of server to do that same thing for different Customers. And it allows you to reuse whole files when you need them.
To allow Customers to have different roles used group membership. Leverage your accounting system to add/remove logins from groups: remove all the login names from the rule-base.
When that doesn't work fall back to netgroups (really). I know netgroups is old-school, but it solves several issues:
/etc/group
,
/etc/netgroup
, and]
/etc/passwd
are usually viewed as under the control of the local Admin, while the
escalation rule-base may be under InfoSec.
Group on-demand tasks by facility (like application name) and verb (like "start", "stop", "kill") then code matching rules to take the correct action with the lowest privilege process that can get the task done. Don't accept any parameters that you don't really need.
Don't run anything as the superuser unless you must. Find a mortal login to run each facility.
For tasks you really must run as root be more picky about who can access the rule, and much more picky about parameters.
Never ask for a password unless the rule cannot possibly be used
in automation. I use op
to start and stop
processes as each host boots -- if those rules ask for a password
you can't do that.
You may choose to put in-line scripts in your local policy,
but I think it's a better practice to use regular expression
matches against $1
to pick the correct command within op
itself. See in-line above and note that
I almost never do that.
Op
forces you to start each new escalation
rule in column 1. After that the format is largely up to you.
I try to phrase rules by this style guild:
args
all on the first line.
grep
:
grep '^target-rule' *.cf
options
list (via $*
,
$
N
).
op
-l
the
information it needs to output a great usage message by matching
whole words when you can:
is shorter for you to type, but represents the parameter as "$1", while$1=^-[abc]$
represents the parameter as "-a|-b|-c". Later you might want to add another rule with the same$1=^(-a|-b|-c)$
mnemonic
and another set of
values for $1
.
$#
with any $*
.
op
so
I always put $#
in or
!
N
to limit the
number of command line parameters.
!*
and
!
N
)
../
to path parameters is
not what we want. You can thwart their attempts with an
explicit rejection:
run /opt/safe/bin/$1 ; !1=^\.\./,/\.\./ ...
groups
,
netgroups
, and
users
options
op
matches the rule, and
it helps explain to the reader why her request is being rejected.
The only users
specification I like is "anyone":
Usingusers=^.*$
groups
is always way better (even for
the superuser, include a group for that in your accounting system).
Use netgroups
,
pam
or
a helmet
specification before you fall into
the trap of listing login names in your configuration.
helmet
, jacket
, pam
, or password
options
helmet=/usr/local/libexec/op/timebox $TIMEBOX_INSIDE=0100<=%H%M<0500
uid
, gid
)
nice
value of the process then that should come first in this section.
We are trying to explain to the reader why
we are using op
to grant special access,
so be clear about what is important and what is a "by the way".
For example setting an environment variable might be either,
by putting it at the top of the list you are
telling those-that-cleanup-after-you what you were thinking.
initgroups
or
PAM
session
and cleanup
,
to make a more complete
environment, should be explicit in the rule (I never put those in a global
DEFAULT
stanza.
The cleanup
setting is never used by
sudo
, very few PAM modules need it (pam_ssh.so
really wants it). It costs an extra process to hold the session open
as the super user, then close it after the escalated process exits.
DEFAULT
at the top of each file
If an auditor gets to that comment and it is not true then, like Lucy, you've got some `splaining to do.# All rules using defaults from line 3.
DEFAULT
DEFAULT
in
access.cf
, since that one covers all the other
files (without one).
I'd put a comment to remind readers of that fact above that rules, as well as
at the top of any file that really wants to use the defaults from
access.cf
.
For example on some hosts the Apache program might be installed in another
location (viz. /opt/apache-version
).
The native configuration file doesn't have an easy way to mark that up, but
m4
(1) sure
does.
I use msrc
(8)
(see the HTML document) to
send my rule-base out to each host. Each host only gets the rules
it needs, and each rule may be marked-up to change parts of the
specification on that host, or type of host.
For example here is an abstraction of the rules to start or stop
the npcguild webserver:
One tip here: put any`# $Revision information... DEFAULT 'include(class.m4)dnl define(`local_PRIVGROUP',`ifelse(IS_TEST(HOST),yes,``^wiz$,'',``'')')dnl `web /opt/npcguild/sbin/web $1 ; $1=^(configtest|start|stop|restart)$ users=^flame$,^root$ groups='local_PRIVGROUP`^wheel$ uid=httpd gid=proxy 'dnl
m4
ifelse
markup above
the rule that uses it (as above).
Any sanity processor may be taught to ignore "local_PRIVGROUP" and
the m4
quotes, it it is harder to ignore the
arbitrary expressions in the ifelse
logic.
The alternative is to process every rule-base file though
hxmd
for every node that might receive it.
Such markup allows the same source file
(aka web.cf.host
) to be sent to
both test and production hosts, but end up with additional
groups
on the test hosts.
Likewise I might tune any other aspect of the rule with similar
markup.
The use of a heavy duty configuration management structure,
like msrc
, in place of a kludge
(viz. replicating the same file to every host) makes a world of
difference when you manage more than 10 machines, or more
than 2 applications per host.
Without reguard for how complex the management of moving the contents to each host is, if you are just moving the same file to every host -- you are not solving the problem.
op
when we have ...?First sudo
factors the configuration
by login access rather than by command access.
If you want to know who can run date
as
root (to set the system clock) you must check every configuration file.
If you want to know know what commands a given person can run you
still need to check every configuration file.
To understand what a sudo
or
super
rule does you must know everything
about the context of the invocation: the IP address of the host, the
contents of (seemingly unrelated) environment variables and the whole
of the sudoers
(or super.tab
) file.
Keeping lists of allowed login names in a file is asking for trouble: it will always be out of date and updates will be painful. This is caused by the whole-sale lack of certainty in the use of each definition. This is also why I almost always use group membership as the key to access in my rule-base: my accounting system lets me add (delete) groups from my Customer's logins pretty much at will. If your accounting system is lacking you should invest some time in getting a better one, not fight tactical issues forever.
Contrast this to op
's stanza definitions.
Most of what you need to know to explain a rule is expressed in a
short stanza all in the same place in the file. To eliminate any
impact from a DEFAULT
stanza just add
a one above the rule:
We know for certain that the "clean-rule" is not modified by any taint from theDEFAULT clean-rule /usr/local/... ; groups=^root$ ...
DEFAULT
in
access.cf
or above it in the current file.
There is no limit to the unexpected impact a "simple" change might
have in sudoers
. Using the escalation
configuration to change the rules based on the host it is installed on
is a poor excuse for configuration management --
when you want two machines to share all the same files, you might really want
one bigger machine, so buy one. The larger machine is cheaper than the
first security issue caused by the lack of control over
your escalation policy.
It is far more secure to to configure precisely the rules needed on each and every host: not the same superset on all hosts.
Then use op
's -S
option
to sanity check each host for missing programs, directories, or nonsensical
rules.
You should be sanity checking your access.cf
and/or your sudoers
files. And you should be
doing it on each host periodically.
Op
version 2.x is as compatible with version
1.x as I can make it. I believe any existing version 1 configuration
will do exactly the same thing under my version, if you rename the
file to access.cf
from what ever else it
was named.
The single configuration exception would be any rules that abut
the delimiting semicolon (;
) against the end of
the last word in the parameter specification: the older versions of
op
allowed that to end the specification, the new
version forces the semicolon or ampersand &
to
stand alone as a word.
The path to op
and the configuration directory
are now under /usr/local/lib
because local
convention requires it. There is no good reason you could not
recompile the program to live under some other location, override
RUN_LIB
in the build process.
The older parser tried to
use the universal escape (backslash, \
)
to quote dollar and comma with limited success. Now we use a
double-comma, and a double-dollar to quote those characters. We don't
make backslash special except after
a dollar (e.g. $\t
).
The use of open curly ({
) and close curly
(}
) to quote an in-line script is not
identical to recent branches of op
, but I
believe it is clear, and avoids any use of backslash inside the script.
(It is always safe to put a semicolon before a leading close curly in a shell
script or perl
program.)
In the following sections I point out how to convert from other
escalation programs to op
.
super
to op
Super
filters the environment with some
hard coded rules (for TERM
,
LINES
, and some others.
The DEFAULT
stanza below should make
some of that less a problem:
DEFAULT # super compat mode environment=^LINES=[0-9]+$,^COLUMNS=[0-9]+$,TERM=[-/:+._a-zA-Z0-9]+$ $IFS=$\s$\t$\n $USER=$t $LOGNAME=$t $HOME=$H $ORIG_USER=$l $ORIG_LOGNAME=$l $ORIG_HOME=$h $ORG_SHELL=${SHELL} $PATH=/bin:/usr/bin $SUPERCMD=$0
There is no way to emulate super
's shebang
#!
magic with op
.
Just use "op script" and let the rule set the program path.
This is more secure in the long run.
sudo
to op
sudoers
files I've helped convert
tend to range from wildly insecure to limitless, allowing unlimited
system access to nearly every user (usually inadvertently).
First stop using "sudo ..." as a prefix for "just run this as root" and start using other mortal (application) logins, and limited commands. Then see the configuration tips above.
To set an environment that looks like sudo
's:
DEFAULT # look more like sudo $USER=$t $HOME=$H $SUDO_COMMAND=$_ $SUDO_USER=$l $SUDO_UID=$L $SUDO_GID=$R # PS1=${SUDO_PS1}
If you want more command-line compatibility you can look for the
sudop
command that tries to make
op
look more like sudo
.
Mayhap installed in sudop.
See also the section in the configuration page.
pfexec
to
op
getuserattr
(3) manual page
stinks of YANGNI
code (like the many "reserved for future use" fields in the structures).
If I want to keep a list of who can do what in a file, I'll
use op
's configuration and skip all the
extra cheese in a generic feature thats looking for
a problem to solve.
With pfexec
it is way too easy to give away
more access than you thought you were.
And you always have to manage roles by login name, which is
the hardest way to manage escalation rules.
Build op
rules for the roles people really
need and skip the generic functions that give away the show.
If you've been putting stuff in /etc/user_attr
on
your hosts, then you are in a hell all of your own making.
Lucky for you op
is an easy way out.
su2
to
op
I agree that it is useful for a user to shift to a shared login for
some tasks: but I'd rather not make it easy for Customers to
Trojan each other without a setuid-bit on the created file.
The truth is that an op
rule to allow a
magic shell for any user that has a file name .su2rc
in their home directory is relatively easy. And the 15 year old version of
the code I found doesn't compile on a modern version of
gcc
.
The rule to emulate a "~www/.su2rc", is a fine example:
This almost wholey emulatessu2 MAGIC_SHELL ; uid=%d gid=%d initgroups=%d session=%d cleanup=%d users=^.*$ %f.path=^/.*/.su2rc$,^/etc/super-users$ %f.perms=^-r.S------$ %f.nlink=^1$ environtment $SU2CHECK=$l:$f helmet=/usr/local/libexec/jackets/su2check
su2
,
given that the su2check helmet checks to
be sure $l
(or *
) is
listed in the file $f
or
/etc/super-users
.
Even better the helmet could check that $f is owned by an account listed in
the password file with the correct home directory, but that really doesn't add
much security.
It would take a few more mnemonic definitions to do
-h
or -d
, or
-
niceval
, and you would chain
rules to make that work.
That makes su2
the script
Or just drop support for the superuser and tell your Customers to run:#!/bin/ksh typeset User if [ $# -eq 0 -o _-c == _"$1" ] ; then exec op -f /etc/super-users "$@" # assumes /etc owned by root fi User=${1:-root} shift 2>/dev/null eval 'exec op -f ~'$User'/.su2rc su2 "$@"'
op -f ~whom/.su2rc su2 [-c command]
$A
$B
op
had when started.
A copy of op
might be installed (by any login)
setuid and/or setgid to provide a limited escalation policy for that
login (and/or group). In that case this macro represents the
privileged group's gid.
$C
access.cf
file.
The dirname
of $c
.
$D
file
.
$E
op
has a setuid bit on it, this is the
uid that owns the file.
$F
file
.
This is represented an a small integer for shell file duplication, as
in 1>&%F
.
$G
group
specified on the command-line.
$H
$I
initgroups
(3) call
from the rule.
$J
op
versions better than 2 this is used for
more job specifications.
$K
$L
$M
op
's original process label.
$N
setgroups
(2).
$O
$P
$Q
op
.
This string is never a source of secure data, since a symbolic link
to the binary could have almost any spelling.
$R
$S
/bin/sh
.
Which maybe overridden by an environment specification of
$SHELL
.
$T
$U
login
provided to
-u
. When this is requested the command
specification must include that option.
$V
op
. This is
mostly used as a forward/backward compatibility parameter to jackets.
$W
$X
chroot
set
this expands to "/", and logs a configuration error via
syslog
(3).
$Y
umask
set when op
was executed. This allows an in-line script to restore the original
umask
, as needed.
$Z
pid
of the op
process. This may allow inspection of that process by helmets. Note that
called getppid
(2) may return a jacket pid, not
the original parent process's id.
$a
$b
op
is install to manage group escalation
(see $B above) this expands to the name of that group.
Note that in sentinel mode this happens to be the name of the sentinel group.
$c
-V
. Usually "/usr/local/lib/op/access.cf".
$d
file
specified under -f
.
$e
op
is
setuid to, usually "root".
$f
file
specified on the command line.
$g
group
on
the command-line.
$h
$i
initgroups
(3) call
from the rule.
$j
op
this is the
job
specification. Version 2 doesn't
have -j
.
$k
$l
$m
mac
option.
The mac
, like $S
is expanded as an environment expression would be.
$n
setgroups
(2).
$o
$p
$q
-V
.
This is used only to allow a jacket to check compatiblity with
op
, but $V
is a
better choice.
$r
$s
script
body specified for the current
rule. (When the first parameter is a curly-brace form.)
$t
$u
login
provided to
-u
by name. When this is requested the command
specification must include that option. If the specification
is a uid (that is a number) it must resolve to a valid login.
$v
op
source file.
$w
$x
dir
option. When no such option is in effect "." is assumed.
$y
ttyname
(3).
Op
prefers
stderr
, then stdin
,
then stdout
-- which is not the common order.
The escalation fails when none of these are attached to a tty.
$z
$^
op
.
This is used by helmets to check for compatible interfaces. The format
is a comma separated list of option names prefixed with "no" if
they are not compiled into the binary. The last element in the
list is the msrc
HOSTTYPE
macro followed by the HOSTOS
in parenthesis.
For example:
This was backported fromoptions,sentinal,showrules,nomortal,pam,nodebug,FREEBSD(90100)
op
version 3. Versions
of op
less than 2.145 do not support this expander.
The value "nooptions" is possible, but unlikely. This macro radiates litte
information as it is usually passed to helmets to allow them to check for
features (along with $V
),
so it it usually only visible in the process table for a very short time.
The information included is avaliable under -V
or
from uname
.
$~
op
was
executed under (usually root's home directory). Op
may be installed setuid to another user (usually by a different name):
in this case it acts as a less powerful application service, but still
retains much of its effectiveness.
${
ENV
}
ENV
as it was presented in the original environment. Note that no
checks are implied here, to check the value of
an existing environement variable map it to the escalated environment and
force a %{
env
}
or
!{
env
}
check, which fails the escalation before any harm is done.
$
number
$0
is the mnemonic name selected.
$#
$$
$.
$@
$*
.
In the context of an
environment variable this is replaced with $*
.
$*
$-
$+
$@
only contains the parameters
to the right of the last named
$
N
specification,
$-
contains all the mnemonic and the
positional parameters broken into words as they were on the command-line.
To pack all those words into a single shell word use
$+
.
The most common use for $-
is to pass all
the parameters to an in-line script. This passes the mnemonic as
$0
so feedback from the script includes
the mnemonic name to help trace errors.
the shell the mnemonic name:
sanity { if some-check ; then echo "$0: some error message" 1>&2 exit 65 fi } $- ; options
$-
is
replaced with $+
.
$_
MAGIC_SHELL
).
This may not be used to define itself, of course. This is
handy to allow a environment variable to pass the target program path on
to a helmet or jacket.
$,
The depricated expander $,
is the same as
$-
but skips the mnemonic word. It is
included for backwards compatibility only.
$\
escape
tr
(1) backslash notation to
specify a special character. The letter 's' is also allowed for
a space character and 'd' for a double-quote "
.
For those that use m4
to
markup the rule-base: 'o' produces an open quote `
and 'q' produces a close quote '
. I've never
needed these escapes, as I used changequote
.
$|
Return to the main page.
REs
.
All the limits that start with a percent (%
)
disallow any value that doesn't match at least 1 of the listed
REs
listed.
See the description below for a list of the possible
attrs
.
%d=
REs
!d=
REs
%d.
attr
=
REs
!d.
attr=
REs
-f
's file
.
The default attr
is path
.
%f=
REs
!f=
REs
%f.
attr
=
REs
!f.
attr
=
REs
-f
's
file
specification.
As above, the default attr
is
path
.
%g=
REs
!g=
REs
%u=
REs
!u=
REs
%m=
REs
!m=
REs
%{
env
}=
REs
!{
env
}=
REs
$
PATH
to
have no relative components:
Which say "Not: the empty string, or a dot by itself, or dot-dot by itself, or a leading dot in the list, or a leading dot-dot in the list, or dot later in the list, or dot-dot later in the list, or dot on the end, or dot-dot on the end, or the empty string at the beginning, in the list or at the end, or a literal dot-dot in any path element." Yes we could compress that expression with!{PATH}=^$,^\.$,^\.\.$,^\.[/:],^\.\.[/:],:\.[/:],:\.\.[/:],:\.$,:\.\.$,^:,::,:$,/\.\./
?
or
{range}
markup, but that is way harder for
an auditor to read.
Any attempt to access a rule with a relative PATH
results in a failure with a message like:
op: environment limits forbid this access
%_=
REs
!_=
REs
%_.
attr
=
REs
!_.
attr
=
REs
$_
's
(the target program's) attributes.
op
may matchstat
return strucure, see stat(2).
path
-- limit by absolute path
The text matched is the absolute path to the target.
perms
-- limit by permissions
The text matched is the file permissions as ls
might display them under -l
. For example
"-rwxr-xr--" for a plain file with the octal modes 754. The specail
perms "n---------" are presented for a file that does not exist.
Note that the Solaris convention for that 'l' replaces 'x' for any
setgid directory is not emulated (op
uses 's').
access
-- limit by access
The text matched is built from 4 the access
(2)
requests made for the file. If each is successful the text would be
"rwxf": for each failed request the corresponding letter is changed to
a dash (-
). So a file that exists with
permissions of 0 would produce "---f", and a file with doesn't
exists "----". Calls to access(2) are made with the real uid and gid
of the client.
type
-- limit by type
The text matched is of variable length from 1 to 4 characters.
The first letter ls
would output in the
permissions list under -l
:
'-
' for a plain file,
'd
' for a directory,
'b
' for a block device, as listed
in ls(1) under
"The Long Format", with the addition that a nonexistant file
is given as 'n
'.
If the file is a symbolic link the next letter is the type of
the node that link resolves to (as above). So a link to a
directory is spelled 'ld
', and a
link to a plain file is 'l-
'.
If the type of the file (or the resolution of a link) is a directory and
that directory is a mount-point the next letter is
an 'm
'.
If the type of the file (or resolution) is a directory and
that directory is empty then the next letter is
'e
'.
Any check below this line requires the file to exist, and will fail the escalation when it doesn't.
dev
-- limit by device number
The text matched is the device number in decimal.
ino
-- limit by inode number
The text matched is the inode number in decimal.
nlink
-- limit by link count
The text matched is the link count as a decimal number.
atime
mtime
ctime
btime
birthtime
-- limit by timestamp
The text matched is the time stamp in decimal. Not every target
operating system supports birthtime, on those system
mtime
is assumed.
size
-- limit by file size in bytes
The text matched is the time size of the file in decimal.
blksize
-- limit by I/O size
The text matched is the time I/O block size of the file in decimal.
blocks
-- limit by size in blocks
The text matched is the decimal number of blocks the file consumes, usually in 512-byte blocks.
uid
-- limit by user-id
The text matched is the owner of the file, by uid in decimal.
login
-- limit by login name
The text matched is the owner of the file, by login name form the password file. An unmappable uid stops any escalation.
gid
-- limit by group number
The text matched is the group owner of the file, by gid in decimal.
group
-- limit by group name
The text matched is the group that owns the file, by group name form the group file. An unmappable gid stops any escalation.
owners
-- limit by limit login:group combinations
The text matched is the owner and group combination that owns the file
separated with a colon (:
). For example,
"ksb:source" or "root:kmem". An unmappable uid or gid stops any escalation.
login@g
-- limit by group member list
Multiple matches are attempted, one for each member of the group that
ownes the file. Any member matching an RE
completes the match.
mode
-- limit by octal mode
The text matched is the file permissions as a 4 digit octal number.
Return to the main page.
sudo
to test op
op
is run without any setuid or setgid
bits it tries to gain superuser privileges by indirecting through
sudo
. This allows sites that forbid any
other setuid prgrams to configure sudo
to
allow testing of op
.
The rule op
expects in
the sudoers
file is:
Defaults stay_setuid, preserve_groups, !env_reset ALL ALL = (root) NOPASSWD : /usr/local/bin/op Defaults !stay_setuid, !preserve_groups, env_reset
That preserves the whole environment and group list, and the original uid.
This is fine, since op
cleans everything.
If you have other options in-scope that break op
you'll have to work it out.
Op
allows two types of strength reduction, that
is to say rather than running with effective uid 0 (the superuser) it is
possible to install rule-set that run with a different uid (and/or gid).
op
is run as the superuser it looks for
a directory named the same as the program name in the configuration
directory. If it finds such a directory and that directory has a
group ownership that matches the name of the directory, then it
assumes that group as the new effective gid, and the uid on
the directory as the new effective uid. This is often used to
shed superuser rights in favor of an application login and group.
By using sentinels one can learn most of the configuration language before
mistakes are made with superuser rights. If group membership is used to
manage projects, you can do a lot with a few sentinel instances of
op
named for each group, I never call them "op".
In actual operation, sentinels are best used to allow members of
a workgroup, or other support structure, to share resources.
They may have rules to chown
or
chgrp
files, or start, stop or signal service process, or
even install programs or configuration files.
There is also no reason why a sentinel escalation cannot
recursively execute op
.
op
configuration
directory is /usr/local/lib/op
we could build a directory
to manage a rule-set for the login "bin" in group "wheel":
# cd /usr/local/lib/op # install -m 750 -g wheel -o bin wheel # touch -m 440 wheel/access.cf # chown bin:wheel wheel/access.cf
In this case the administrator must manage the rules, as only the superuser is allowed access to the configuration directory.
To implement a sentinal directory for my login (ksb) one might chose to symbolic link to a directory under my home directory:
With that built I can add rules to my own# cd /usr/local/lib/op # ln -s ~ksb/lib/op ksb # install -dr -o ksb -g ksb -m 0750 ~ksb/lib/op # touch -m 440 ~ksb/lib/op/access.cf # chown ksb:ksb ~ksb/lib/op/access.cf
access.cf
or add new rule-sets to the directory.
In both cases someone has to build a symbolic link to op
named for the group to be accessed: in my case my login and group names are
the same, so one might imagine they are using the login name, a harmless
fiction. For example to build the "wheel" link:
# ln -s /usr/local/bin/op /opt/admin/bin/wheel
op
op
configured with a different path to
the escalation database, running as a mortal user is also a sentinel.
Use the spell below from that master source to build a sentinel.
You'll need to know the local site policy for building remote to your host,
or you need a local copy of the msrc_base
package installed on your machine. I'll assume you have the
latter, given that the former implies more skill.
From the master source directory for op, install_base*/local/bin/op,
you'll run something like (change the group
and
paths to fit your situation):
mmsrc -Cauto.cf -y INTO=/tmp/op.$USER \ make DEBUG=-DRUN_AS_MORTAL RUN_LIB=/home/$USER/lib \ RUN_BIN=/home/$USER/bin all cd /tmp/op.$USER install -o $USER -g group -m 2511 op /home/$USER/bin/group install -c -m 0440 -g group /dev/null /home/$USER/lib/access.cf cd .. rm -rf op.$USER ~/bin/group -V
If that failed you may not have the pam-devel
package
installed on your machine (or some other compiler tool). Find the
missing parts and you should be good to go. I did mention that you
need msrc_base
, right? I also mentioned that
the link to a directory is just as secure and much easier to
do, right?
To test that you'll need to either be sure the group ownership of
the binary is a group other than your primary login
group, or login as another user. Because running
the program as yourself tries to hook into sudo
to gain some privilege to manage. We tried to turn that off with the
RUN_AS_MORTAL, but people make mistakes.
I used "www" (which is not my primary login group). If you want
to go crazy you can make program mode 6511 (ug+s
).
Then you could manage rules that let other people run escalations as your
login and a special group. With more than 1 sentinel you can chain them
together to get very complex interations.
Since setgroups
requires superuser access,
tripple group membership is still impossible without the help of an
administrator.
pam
logic work. Try as much as
you can with other people in you work group, or build rules to
start, stop, and reconfigure you application test levels. Have fun.
Note that
indirection through sudo
breaks
the internal sentinel indirection. You'll have to build separate binary
files and put in multple indirect sudo
rules, or
each of them setuid (and/or setgid) to the specific user (group).
op
for each sentinel directory. This
rule lists all the sentinels configured without giving away any
other information:
If you do not have mysentinels { cd $0 && /usr/local/bin/glob -s "*/" | tr -d / | sed -e "/^OLD$/d" } $C ; groups=^.*$ uid=0 gid=. initgroups=root
glob
program installed
you can replace that line with:
find * -type d -print -prune |
Use this to automate building the required links by turning the list into
mk
marked commands in a recipe file:
Theop sentinels | xapply -f 'mk -mLink -l0 -d%1 recipe-file' -
recipe-file
code be a makefile or a
special-purpose file with descriptions of the purpose for each
sentinel rule-base and the path to the link (which might be in
a private directory, so modes and owners can matter).
In either case this might trigger other actions to build-out the supporting structures. This allows the existance of the sentinel configuration to trigger additional CM actions, which is one possible direction: the existance of the support structures may trigger the installation of the sentinel directory, either way may be correct based on local site policy.
$Id: refs.html,v 2.102 2012/10/30 21:56:16 ksb Exp $