sudo を使いこなすためのメモ

細かい制御が可能なので、su じゃなくて sudo を使いましょう。

sudo 設定のお約束

  • sudo の設定ファイルは /etc/sudoers ですが、/etc/sudoers.d/ 内に置いたファイルもファイル名順に読み込まれます。
  • これらの設定ファイルをテキストエディタで直接編集してはいけません。文法ミスとかすると二度と sudo が使えなくなり、詰みます。必ず visudo コマンド経由で編集してください。編集終了時に自動的に編集結果をチェックして、文法ミスなどがあれば編集結果の反映をブロックしてくれます。
  • メインの設定ファイル /etc/sudoers を編集したいときは、sudo visudo --strict を実行します。
  • システムデフォルトのテキストエディタが nano とかになっている場合に vi を使いたければ、sudo EDITOR=vi visudo --strict とすれば OK です。
  • /etc/sudoers.d/ 内の設定ファイルを新規作成/編集するときは、sudo visudo --strict -f /etc/sudoers.d/foobar みたいにします。
  • 基本的に /etc/sudoers は触らず、/etc/sudoers.d/ に個別設定ファイルを作ることをオススメします。
  • ただし、/etc/sudoers の中で「editor 設定の無効化」だけはしておいたほうが良いみたいです。 ⇒ 参考記事
  • /etc/sudoers の中にある #includedir /etc/sudoers.d という行の行頭のシャープ記号 (#) は、「コメントアウト」の意味ではないので削除してはいけません。
  • 設定ファイルの変更は即時反映されます。システムの再起動などは不要です。
  • 設定ファイルの中では各種 Alias を使うと楽です。

なお、/etc/sudoers に追加しておいたほうが良いのは、次の2行:

1
2
Defaults    !env_editor
Defaults editor=/usr/bin/vi

環境変数(特に PATH)を引き継ぎたい

perlbrew とか nvm とか pyenv とか便利ですよね?

まぁ、最近流行の Docker を使えば話は簡単なのかもしれませんが、古い Linux カーネルの環境だと Docker が使えなかったりするので、perlbrew などの需要は今後もあると思います。仮想化方式が OpenVZ の VPS では Linux カーネルが更新できない、という話もありますし。

Ubuntu 16.04 LTS のデフォルト設定では、sudo 時に環境変数 PATH がリセットされてしまいます。したがって、ファイル先頭に #!/usr/bin/env perl などと書かれた Perl スクリプトを sudo で起動すると、perlbrew は無視されてしまい、システムデフォルトの perl(/usr/bin/perl とか)が使われます。この挙動は /etc/sudoers の中の Defaults ディレクティブで reset_envsecure_path が指定されているためです。sudo コマンドには -E オプションがありますが、これらの Defaults 設定が有効な限り、環境変数 PATH には効きません。

特権ユーザ root に sudo する場合、環境変数のリセットはセキュリティ的に妥当です。しかし、ある目的のために「特定のプログラムを特定の一般ユーザの権限で」sudo したいだけなら、環境変数を引き継ぐことも許容できると思います。sudo の設定を変える(ルールを追加する)ことで、これを実現できます。

たとえば、「ユーザ foo-runner の権限でプログラム /home/foo-builder/bin/foo を実行する」ことをユーザ bar に許可する場合、以下のような内容のファイルを /etc/sudoers.d/ 内に置きます:

1
2
3
4
5
6
7
8
Runas_Alias FOO_RUNNERS = %foo-runner, !foo
Cmnd_Alias FOO_COMMANDS = /bin/kill, /usr/bin/test, /home/foo-builder/deploy/*/bin/foo

Defaults>FOO_RUNNERS !env_reset
Defaults>FOO_RUNNERS !secure_path
Defaults>FOO_RUNNERS !authenticate

foo ALL = (FOO_RUNNERS) FOO_COMMANDS

Defaults ディレクティブを使ってシステムデフォルトの設定を限定的に上書きしているのがポイントです。あと、/bin/kill の実行を許可しておかないと、起動したプロセスを制御できなくなってしまうので、ご注意ください。test コマンドについても許可しておくのがオススメです(後述)。

ファイル名はわかりやすく /etc/sudoers.d/foo とかにしておきましょう。前述のとおり、このファイルを作成/編集するときは、必ず visudo コマンドを使いましょう(具体的には sudo visudo --strict -f /etc/sudoers.d/foo と実行しましょう)。ファイルのパーミッション等は visudo が適切に設定してくれます。

sudo 実行用シェルスクリプトの例

関連ディレクトリ/ファイルへのアクセス権を確認してから起動するのがオススメです。上述の設定ファイル例で test コマンドの実行を許可しておいたのは、このため。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/bin/sh

set -e
set -x

RUNNER=foo-runner
FOO_ROOT=/home/foo-builder/deploy/stable

CMD=$FOO_ROOT/bin/foo
CONF=./conf/foo.conf
LOGS_DIR=./logs
PID_FILE=$LOGS_DIR/pid
DATA_DIR=./data
PREDICT="/usr/bin/nice -10"

if [ -z "$RUNNER" ]; then
RUNNER=$USER
fi
if [ "$RUNNER" != "$USER" ]; then
PREDICT="$PREDICT sudo -u $RUNNER"
fi

if expr "x$0" : '^x/' >/dev/null; then
cd "`dirname \"$0\"`"
else
cd "`dirname \"./$0\"`"
fi

get_existent_pid() {
test ! -z "$PID_FILE" || {
echo "ERROR: BUG: invaild PID_FILE : $PID_FILE" 1>&2
return 99
}
test -e $PID_FILE || return 0
test -r $PID_FILE || {
echo "ERROR: can not read $PID_FILE (permission error)" 1>&2
return 99
}
local STATUS
STATUS=
local PID
PID="`cat $PID_FILE`" || STATUS="$?"
test -z "$STATUS" || {
echo "ERROR: can not read $PID_FILE (status=$STATUS)" 1>&2
return 99
}
expr "x$PID" : '^x[1-9][0-9]*$' >/dev/null || {
echo "ERROR: detect unexpected contents in $PID_FILE : $PID" 1>&2
return 99
}
ps --no-headers --quick-pid $PID -o comm,user,cmd |
while read ARG1 ARG2 ARG3 ARG4 ARG5 ARGS; do
COMM=perl
test "x$ARG1" = "x$COMM" || return 0
test "x$ARG2" = "x$RUNNER" || {
echo "ERROR: the owner of existent process (PID:$PID) is not $RUNNER : $ARG2" 1>&2
return 3
}
test "x$ARG3" = "xperl" -a "x$ARG4" = "x-x" -a "x$ARG5" = "x$FOO_ROOT/share/foo/start_server" || {
echo "WARNING: unexpected executable name of existent process (PID:$PID) : $ARG3 $ARG4 $ARG5" 1>&2
}
echo $PID
done
return 0
}

PID=`get_existent_pid` || exit $?

start_process() {
if [ ! -z "$PID" ]; then
echo "ERROR: foo is already running" 1>&2
exit 2
fi
for DIR in $DATA_DIR $LOGS_DIR; do
chgrp -R $RUNNER $DIR 2>/dev/null || true
chmod -R g+w $DIR 2>/dev/null || true
$PREDICT test -d $DIR
$PREDICT test -r $DIR
$PREDICT test -x $DIR
$PREDICT test -w $DIR
done
$PREDICT test -r $CONF
$PREDICT test -f $CMD
$PREDICT test -r $CMD
$PREDICT test -x $CMD
export FOO_ROOT
umask 007
$PREDICT $CMD -c $CONF -t
$PREDICT $CMD -c $CONF -m daemon
}

send_signal() {
local SIGNAL
SIGNAL="$1"
test "x$SIGNAL" = 'x-TERM' -o "x$SIGNAL" = 'x-HUP'
if [ -z "$PID" ]; then
echo "ERROR: foo is not running" 1>&2
exit 2
fi
$PREDICT /bin/kill $SIGNAL $PID
}

case "$1" in
start)
start_process
;;
stop)
send_signal -TERM
wait $PID || echo "WARNING: status=$?" 1>&2
;;
reload)
send_signal -HUP
;;
restart)
"./`basename \"$0\"`" stop
"./`basename \"$0\"`" start
;;
*)
echo "Usage: $0 (start|stop|reload|restart)" 1>&2
exit 1
;;
esac

参考記事