調べてみようと思ったきっかけ
最近、SSHエージェントの転送機能を利用している際に気になったことがありました。
SSHエージェントの転送というのは、以下のようなコマンドの実行によりローカルにある秘密鍵を接続先のサーバでも利用可能にするための機能のことです。
$ eval `ssh-agent`
Agent pid プロセスID
$ ssh-add 秘密鍵
$ ssh -A ユーザ名@接続先サーバ
このSSHエージェントの転送を可能にするには接続先サーバの /etc/ssh/sshd_config
で「AllowAgentForwarding」を許可しておく必要があります。
AllowAgentForwarding yes
なるほど、と思い。接続先サーバの /etc/ssh/sshd_config
を確認してみると、「AllowAgentForwarding」の設定項目はコメントアウトされていました。
$ sudo cat /etc/ssh/sshd_config |grep 'AllowAgentForwarding'
#AllowAgentForwarding yes
sshdコマンドの「-T」オプション(拡張テストモード)を利用すると、各設定項目の値が表示できます。
-T Extended test mode. Check the validity of the configuration file, output the effective configuration
to stdout and then exit. Optionally, Match rules may be applied by specifying the connection parame‐
ters using one or more -C options.
なので、sshdコマンドの「-T」オプションを利用して「AllowAgentForwarding」を確認してみると「yes」になっていました。
恐らくこの設定値が「yes」なので /etc/ssh/sshd_config
内でコメントアウトされていてもSSH鍵の転送が問題なく実行できるのだと思います。
$ sudo sshd -T |grep -i 'AllowAgentForwarding'
allowagentforwarding yes
しかし、この値はどこからやって来たのだろう?という疑問が湧き、sshdの中身が気になったのでソースを読んでみることにしました。(普段はPHPerなのでC言語わかんないけど)
ソースを読む、その前に
sshdのソースを読む前に、sshdがどのパッケージに含まれているコマンドなのかを知る必要があるでしょう。
まず、sshdコマンドのフルパスを確認します。
$ which sshd
/usr/sbin/sshd
sshdコマンドのフルパスが /usr/sbin/sshd
だとわかったので、この情報を元にどのパッケージに含まれているかを確認します。
$ rpm -qf /usr/sbin/sshd
openssh-server-8.0p1-6.el8_4.2.x86_64
$ dpkg -S /usr/sbin/sshd
openssh-server: /usr/sbin/sshd
$ dpkg -l |grep openssh-server
ii openssh-server 1:8.2p1-4ubuntu0.2 amd64 secure shell (SSH) server, for secure access from remote machines
/usr/sbin/sshd
は「openssh-server」というパッケージに含まれるコマンドであるということがわかりました。
つまり、sshdコマンドを知るにはOpenSSHのソースを解析していくとよさそうです。
OpenSSHのソースファイルを取得
OpenSSHのwebサイトにアクセスし、左メニューの「For other systems」の「Linux」をクリックすると、ページ内にダウンロードリンクが表示されます。
rpm -qf
で確認した内容と同じバージョンの「openssh-8.0p1.tar.gz」をダウンロードします。
※今回利用した接続先のサーバはCentOS8なのでRPMコマンドを前提として説明を進めます。
「openssh-8.0p1.tar.gz」のリンクをクリックしてもよいですし、curlコマンドでもよいでしょう。
$ curl -LO https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-8.0p1.tar.gz
ダウンロードが完了したら展開しておきます。
$ tar zxvf openssh-8.0p1.tar.gz
$ cd openssh-8.0p1
これでソースファイルを読む準備が整いました。
OpenSSHのソースファイルを読んでみる
まずは今回のキーワードである「AllowAgentForwarding」を grep
で検索してみると、いきなりそれっぽいのが表示されました。
$ grep -n 'AllowAgentForwarding' * -r
servconf.c:503: sUsePrivilegeSeparation, sAllowAgentForwarding,
servconf.c:609: { "allowagentforwarding", sAllowAgentForwarding, SSHCFG_ALL },
servconf.c:1658: case sAllowAgentForwarding:
servconf.c:2603: dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
sshd_config:84:#AllowAgentForwarding yes
sshd_config.0:36: AllowAgentForwarding
sshd_config.0:656: AllowAgentForwarding, AllowGroups, AllowStreamLocalForwarding,
sshd_config.5:100:.It Cm AllowAgentForwarding
sshd_config.5:1110:.Cm AllowAgentForwarding ,
sshd_config
の検索結果を見てみると84行目の「#AllowAgentForwarding yes」という内容からわかるように、設定ファイルの /etc/ssh/sshd_config
そのもののようです。
今回知りたいことは sshd_config
についてではありません。このことから sshd_config.0
、 sshd_config.5
についても一旦無視することとし、まずは servconf.c
の中身を見ていきます。
servconf.c
に対して「AllowAgentForwarding」のgrep検索を実行すると、以下の行が該当しました。
$ grep -n 'AllowAgentForwarding' servconf.c
503: sUsePrivilegeSeparation, sAllowAgentForwarding,
609: { "allowagentforwarding", sAllowAgentForwarding, SSHCFG_ALL },
1658: case sAllowAgentForwarding:
2603: dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
servconf.c
の2603行目の「dump_cfg_fmtint」を見てピンと来ました。前後の行を見ると以下のようになっています。
2601 dump_cfg_fmtint(sUseDNS, o->use_dns);
2602 dump_cfg_fmtint(sAllowTcpForwarding, o->allow_tcp_forwarding);
2603 dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
2604 dump_cfg_fmtint(sDisableForwarding, o->disable_forwarding);
2605 dump_cfg_fmtint(sAllowStreamLocalForwarding, o->allow_streamlocal_forwarding);
この表示順は sshd -T
を実行した際に表示される設定項目の順番と一致しているのです。
$ sudo sshd -T
port 22
addressfamily any
・
・
usedns no
allowtcpforwarding yes
allowagentforwarding yes
disableforwarding no
allowstreamlocalforwarding yes
・
・
つまり、「dump_cfg_fmtint」の周辺を見ていくと「allowagentforwarding yes」の謎が解けそうな気がしてきました。
次に「dump_cfg_fmtint」の箇所が sshd -T
の実行時に実際に呼び出されているのかを確認してみます。
「AllowAgentForwarding」に関係してそうな2603行目は「dump_config」という関数の中で実行されている内容だとわかりました。
2534 void
2535 dump_config(ServerOptions *o)
2536 {
2537 char *s;
2538 u_int i;
・
・
2602 dump_cfg_fmtint(sAllowTcpForwarding, o->allow_tcp_forwarding);
2603 dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
2604 dump_cfg_fmtint(sDisableForwarding, o->disable_forwarding);
servconf.c
の中で「dump_config」を検索してみると該当箇所は見つかりませんでした。
つまり、「dump_config」は別のファイルから呼び出される関数だということが予想されます。
ということは、「dump_config」はsshdのソースから呼び出されているのではないかという仮説が立てられます。
なので、今度は「dump_config」というキーワードを元に grep
で検索してみます。
すると、本命の sshd.c
の1847行目が該当しましたので、今度は sshd.c
を見ていきましょう。
$ grep -n 'dump_config' * -r
servconf.c:2535:dump_config(ServerOptions *o)
servconf.h:274:void dump_config(ServerOptions *);
sshd.c:1847: dump_config(&options);
「dump_config」が呼び出されている前後を見ると、「test_flag」という変数が1より大きい場合に「dump_config」が呼び出されるようです。
1839 if (test_flag > 1) {
1840 /*
1841 * If no connection info was provided by -C then use
1842 * use a blank one that will cause no predicate to match.
1843 */
1844 if (connection_info == NULL)
1845 connection_info = get_connection_info(ssh, 0, 0);
1846 parse_server_match_config(&options, connection_info);
1847 dump_config(&options);
1848 }
変数「test_flag」の定義元を見てみると、予想が当たっていたことがわかる説明が書いてありました。
「if (test_flag > 1)」というのはsshdコマンドの「-T」オプションのことを指していました。
148 /*
149 * Indicating that the daemon should only test the configuration and keys.
150 * If test_flag > 1 ("-T" flag), then sshd will also dump the effective
151 * configuration, optionally using connection information provided by the
152 * "-C" flag.
153 */
154 static int test_flag = 0;
ちなみに、 sshd -T
を実行した場合、変数「test_flag」に何がセットされるかについては、引数毎のswitch文が記述されていて、その中で「2」がセットされています。これにより、 if (test_flag > 1)
の判定文が真になり、「dump_config」関数が呼び出されるという仕組みのようです。
1468 /* Parse command-line arguments. */
1469 while ((opt = getopt(ac, av,
1470 "C:E:b:c:f:g:h:k:o:p:u:46DQRTdeiqrt")) != -1) {
1471 switch (opt) {
1472 case '4':
1473 options.address_family = AF_INET;
1474 break;
1475 case '6':
1476 options.address_family = AF_INET6;
1477 break;
・
・
1548 case 'T':
1549 test_flag = 2;
1550 break;
次に sshd -T
を実行した際に servconf.c
の「dump_config」関数で、どのように「allowagentforwarding yes」と出力しているかを見ていきます。
先程見ていたservconf.c
の2603行目に再び着目してみます。
2603 dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
sshd -T
の実行時に2603行目に到達し、その出力結果が恐らく以下のような結果になっているはずですので、「dump_cfg_fmtint」関数の定義を見てみます。
allowagentforwarding yes
「dump_cfg_fmtint」関数の中の printf("%s %s\n"
の部分で2つの文字列を出力してるようです。 lookup_opcode_name(code), fmt_intarg(code, val));
の部分でキー名と値を取得していることが予想できます。
2458 static void
2459 dump_cfg_fmtint(ServerOpCodes code, int val)
2460 {
2461 printf("%s %s\n", lookup_opcode_name(code), fmt_intarg(code, val));
2462 }
「lookup_opcode_name」関数の定義を見てみると、 keywords
という配列のようなものを走査して、 keywords[i].opcode
と引数の code
が一致したら keywords[i].name
を返却しています。
677 static const char *
678 lookup_opcode_name(ServerOpCodes code)
679 {
680 u_int i;
681
682 for (i = 0; keywords[i].name != NULL; i++)
683 if (keywords[i].opcode == code)
684 return(keywords[i].name);
685 return "UNKNOWN";
686 }
次は keywords
の定義を見ていきましょう。
struct
という記述があるので keywords
はどうやら構造体と呼ばれるもののようです。
この構造体の定義の中に事前に調べておいた609行目の内容が含まれていました。
520 /* Textual representation of the tokens. */
521 static struct {
522 const char *name;
523 ServerOpCodes opcode;
524 u_int flags;
525 } keywords[] = {
・
・
608 { "allowtcpforwarding", sAllowTcpForwarding, SSHCFG_ALL },
609 { "allowagentforwarding", sAllowAgentForwarding, SSHCFG_ALL },
610 { "allowusers", sAllowUsers, SSHCFG_ALL },
・
・
662 };
ということは2603行目で「dump_cfg_fmtint」関数の第一引数に「sAllowAgentForwarding」を渡していますが、これにより「lookup_opcode_name」関数を経由して、「"allowagentforwarding"」が出力されることがわかりました。
2603 dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
残るは sshd -T
の実行時に出力される「allowagentforwarding yes」の「yes」の部分がどのように出力されるかを突き止めるだけです。
2603行目の「dump_cfg_fmtint」関数の第2引数は「o->allow_agent_forwarding」となっていました。
第2引数の「o->allow_agent_forwarding」の元となっている変数「o」の出どころですが、実はちょっと前に登場していました。
sshd.c
から呼び出されている「dump_config」関数の第1引数です。
2535 dump_config(ServerOptions *o)
sshd.c
の中で「dump_config」関数を呼び出している箇所は「main」関数に含まれています。
「dump_config」関数の引数で渡している変数optionsは、「main」関数の1466行目の「initialize_server_options」関数で初期化されています。
1413 /*
1414 * Main program for the daemon.
1415 */
1416 int
1417 main(int ac, char **av)
1418 {
・
・
1465 /* Initialize configuration options to their default values. */
1466 initialize_server_options(&options);
・
・
1635 parse_server_config(&options, rexeced_flag ? "rexec" : config_file_name,
1636 cfg, NULL);
1637
1638 /* Fill in default values for those options not explicitly set. */
1639 fill_default_server_options(&options);
・
・
1839 if (test_flag > 1) {
1840 /*
1841 * If no connection info was provided by -C then use
1842 * use a blank one that will cause no predicate to match.
1843 */
1844 if (connection_info == NULL)
1845 connection_info = get_connection_info(ssh, 0, 0);
1846 parse_server_match_config(&options, connection_info);
1847 dump_config(&options);
1848 }
・
・
2197 exit(0);
2198 }
servconf.c
の「initialize_server_options」関数を見ると、140行目で「options->allow_agent_forwarding」に「-1」がセットされています。
77 /* Initializes the server options to their default values. */
78
79 void
80 initialize_server_options(ServerOptions *options)
81 {
・
・
140 options->allow_agent_forwarding = -1;
・
・
183 }
「initialize_server_options」関数を通ったあとに、1635行目の「parse_server_config」関数が呼ばれているんですが、ここでは「options->allow_agent_forwarding」の値に変化はないようなので、1639行目の「fill_default_server_options」関数を見ていきます。
「Fill in default values」というコメントから察するに、いかにもデフォルト値を設定してそうです。
1465 /* Initialize configuration options to their default values. */
1466 initialize_server_options(&options);
・
・
1635 parse_server_config(&options, rexeced_flag ? "rexec" : config_file_name,
1636 cfg, NULL);
1637
1638 /* Fill in default values for those options not explicitly set. */
1639 fill_default_server_options(&options);
「initialize_server_options」関数では「-1」をセットしていましたが、 options->allow_agent_forwarding
が「-1」の場合は「1」で上書きしているようです。
273 fill_default_server_options(ServerOptions *options)
274 {
・
・
380 if (options->allow_agent_forwarding == -1)
381 options->allow_agent_forwarding = 1;
・
・
473 }
そして、ようやく sshd.c
の「dump_config」関数を呼び出しているところに戻ってきました。
「dump_config」関数を呼び出す直前で「parse_server_match_config」を呼んでいますが、引数のoptionsの値に変化はなさそうだったのでスキップします。(parse_server_match_configのコードは少々複雑で読むのが若干つらかったというのが正直なところ)
1839 if (test_flag > 1) {
1840 /*
1841 * If no connection info was provided by -C then use
1842 * use a blank one that will cause no predicate to match.
1843 */
1844 if (connection_info == NULL)
1845 connection_info = get_connection_info(ssh, 0, 0);
1846 parse_server_match_config(&options, connection_info);
1847 dump_config(&options);
1848 }
「dump_config」関数の中で「AllowAgentForwarding」のデフォルト値を出力している処理の中身をもう一度見ていきましょう。
2534 void
2535 dump_config(ServerOptions *o)
2536 {
2537 char *s;
2538 u_int i;
・
・
2602 dump_cfg_fmtint(sAllowTcpForwarding, o->allow_tcp_forwarding);
2603 dump_cfg_fmtint(sAllowAgentForwarding, o->allow_agent_forwarding);
2604 dump_cfg_fmtint(sDisableForwarding, o->disable_forwarding);
「dump_cfg_fmtint」関数は、このような構成になっており、「lookup_opcode_name」関数で「"allowagentforwarding"」が出力されることまではわかっています。
なので、「fmt_intarg」関数を見ていきます。
2458 static void
2459 dump_cfg_fmtint(ServerOpCodes code, int val)
2460 {
2461 printf("%s %s\n", lookup_opcode_name(code), fmt_intarg(code, val));
2462 }
「fmt_intarg」関数の中の2439行目についに見つけました。「yes」の文字列です。
ここまでの処理で「o->allow_agent_forwarding」には「1」がセットされています。
2419行目のswitch〜caseを見ると、「sAllowAgentForwarding」の条件は定義されていませんので、defaultを通ります。
ということは、2438行目の「case 1:」に該当し、「yes」の文字列がreturnされます。
2414 static const char *
2415 fmt_intarg(ServerOpCodes code, int val)
2416 {
2417 if (val == -1)
2418 return "unset";
2419 switch (code) {
2420 case sAddressFamily:
2421 return fmt_multistate_int(val, multistate_addressfamily);
2422 case sPermitRootLogin:
2423 return fmt_multistate_int(val, multistate_permitrootlogin);
2424 case sGatewayPorts:
2425 return fmt_multistate_int(val, multistate_gatewayports);
2426 case sCompression:
2427 return fmt_multistate_int(val, multistate_compression);
2428 case sAllowTcpForwarding:
2429 return fmt_multistate_int(val, multistate_tcpfwd);
2430 case sAllowStreamLocalForwarding:
2431 return fmt_multistate_int(val, multistate_tcpfwd);
2432 case sFingerprintHash:
2433 return ssh_digest_alg_name(val);
2434 default:
2435 switch (val) {
2436 case 0:
2437 return "no";
2438 case 1:
2439 return "yes";
2440 default:
2441 return "UNKNOWN";
2442 }
2443 }
2444 }
ここまで長い道のりでしたが、 sshd -T
が実行されると sshd.c
の「dump_config」関数が呼ばれ、それにより servconf.c
の「dump_cfg_fmtint」関数を通り、以下のような出力結果が得られることがわかりました。
allowagentforwarding yes
まとめ
ほんのちょっとした疑問からOpenSSHのソースを読みはじめてみましたが、慣れてない言語ということでわからないことも多く大変でした。
ですが、「この値はどこからやって来たのだろう?」という疑問の答えに到達することができたので非常に満足感が得られました。
アプリケーションエンジニアをやっていると、フレームワークやライブラリ、DBやwebサーバなどのミドルウェアなど様々なツールを利用することがあります。
そういった際にはドキュメントを見ることになると思います。利用方法について詳しく説明が書かれている場合もあれば、概要っぽく書かれていることもあり、時には誤りが書かれていたり、バージョンアップに追従していなくて内容が古くなっていることもあるでしょう。
そんな時に「よーし、いっちょソースを読んでやるか」という気持ちになるのか、「うぇー、ソース読まないとダメなのかー」という気持ちになるのか、問題の解決に向かうには前者の方が好ましいでしょう。
ということで、ふとしたきっかけで仕事では使ってない言語を読んでみるとなかなか楽しかったので、これからも気軽に知らない言語を読むクセをつけていきたいなと思いました。ちなみに、次にC言語を読むときはgdbの使い方を学ぼうと思います。
おまけ
「/etc/ssh/sshd_config
」でコメントアウトされている値について話してたら、「man sshd_config
を実行して、知りたいキーワードを検索したらデフォルト値がどうなってるかはすぐにわかるんじゃないですか?」と言われちゃいました。
まず真っ先に「ドキュメントを読む」ということをやりましょうね。はい。ごめんなさい。
(いや、違うんだ。今回はそのデフォルト値がどこから来てるのかが知りたかったんだ。)
$ man sshd_config
SSHD_CONFIG(5) BSD File Formats Manual SSHD_CONFIG(5)
NAME
sshd_config — OpenSSH SSH daemon configuration file
・
・
・
AllowAgentForwarding
Specifies whether ssh-agent(1) forwarding is permitted. The
default is yes. Note that disabling agent forwarding does not
improve security unless users are also denied shell access, as
they can always install their own forwarders.