2014年4月17日木曜日

[linux]sshで鍵認証を行う際、commandに任意の引数を与えたい!

自分は、以前は{Windows命}だったのですが、最近linuxを使うようになって、便利なツールやコマンドが大量にあるので、{あれ、もしかしてなんか自動でやらせたりする時にはすっごく便利かも}と思うようになってきました。
もちろん、コマンドがいっぱいあり過ぎてとても全貌はつかめないのですが、自分が思いつくような処理はだいたいやる方法があります。
今回は、あるマシンから別のマシンのコマンドをある程度セキュリティを保ちながら、ビシバシ自動で実行するというところまで、結構大変だったのでメモをだらだらと残します。

あるクライアントマシン(以下C)からサーバー(以下S)上のコマンドをC上のプログラムやらcronやらから自動で実行したいとします。
話が長いので先にまとめると、
・ CからSのいろんなコマンドを自動で実行したい。
→ Sにログインするとき、自動でsshのパスワード入力ができなくて困る。
→ パスフレーズ無しの秘密鍵を使用したssh鍵認証を使用する。
→ パスフレーズ無しなので、秘密鍵ファイルさえあればS上でやりたい放題なので困る。
→ Sのauthorized_keysにcommandを書いてそのコマンドしか実行できないようにする。
→ CからSに投げたいコマンドは1つじゃないし、パラメータとかも変えたいので困る。
→ sshの機能を使いCからSに環境変数を渡して解決。

1: ssh鍵認証でログインできるようにする。
まず、C上で、公開鍵と秘密鍵を作成します。(参考にしたページ)
client$ ssh-keygen -t rsa
# 途中で秘密鍵のパスフレーズを聞かれるので、ただエンターすればパスフレーズ無しになる。 
# ホームの.sshにできた、id_rsa.pubが公開鍵で、id_rsaが秘密鍵。
今度は、Sに先ほどCで作った公開鍵(id_rsa.pub)をどうにかして持っていって、ログインしたい「Sのユーザー」のホーム下の.ssh/authorized_keys
の一番最後に、id_rsa.pubの内容をコピーします。
.sshディレクトリやauthorized_keysがない時は、
server$ cd
server$ mkdir .ssh
server$ chmod 700 .ssh
server$ touch .ssh/authorized_keys
server$ chmod 600 .ssh/authorized_keys
# 他のユーザーが中身を見られないようにちゃんとchmodしておく。
これでクライアント側から
client$ ssh -i ~/.ssh/id_rsa user@serverAddress 
なんてやるとパスワード入力無しでログインできます。
ただし、秘密鍵がパスフレーズ無しなので、id_rsaを持っているひとはS上でやりたい放題になってしまい危険です。

2: 鍵認証でログインしたユーザーが好きなコマンドを打てないようにする。
これは、Sの~/.ssh/authorized_keys にオプションを書くことにより行います。
authorized_keysというファイルは、かなり高機能なオプション設定ができまして、このファイルをいじるだけで、「使用される公開鍵ごとに」接続ホストを限定できたり、対話コンソール禁止にしたり、ポートフォワーディングを禁止にしたりいろいろできます。
接続ホストを限定すれば、だいぶ安心なのですが、それだと誰かが勝手にCのマシンを使った時にパスワード無しでSに侵入できるので、今回は、commandオプションを使います。
commandオプションを設定すると、sshで接続してきたら特定のコマンド(サーバー側で設定)だけを実行して接続を切るようになります。sshコマンドで実行コマンドが指定されていても無視します。(参照:sshdのman)
オプションを書く場所は、authorized_keysの先ほどの公開鍵をコピーした行の先頭で、例えば
command="ls -al" ssh-rsa AAABBBCCC...
# ssh-rsa以降が公開鍵部分
としておくと、クライアント側から
client$ ssh -i ~/.ssh/id_rsa user@serverAddress 
なんてやると、ログイン先のホームディレクトリの内容がずらずらと出て、すぐにSからログアウトされます。
これで、誰かが秘密鍵を使ってログインしても好きなことが出来なくなります。
しかし、この状態だと自分もcommandで指定したコマンドしか実行できないし、コマンドに可変で引数とか渡せないので不便です。

3: sshの機能でサーバーに環境変数を渡す。
sshの機能でCからSに環境変数を渡すことができます。(ここのページをヒントにしました。)
まず、S側の/etc/ssh/sshd_config を下のようにいじります。
# 環境変数の変更を許可する設定
PermitUserEnvironment yes
# クライアントから値を受け取る環境変数の指定
# SSH_ARGは自分でテキトーに決めた環境変数名
AcceptEnv SSH_ARG
PermitUserEnvironmentはyes/noなので、値を上書きします。
AccveptEnvは、上書きでなく追加してください。追加した環境変数がクライアントから変更可能になります。
なお、sshd_configを変更したらsshdの再起動が必要です。
これで今度は、authorized_keysのcommandを、例えば
command="echo SSH_ARG=$SSH_ARG" ssh-rsa AAABBBCCC...
# ssh-rsa以降が公開鍵部分
としておくと、クライアント側で
client$ env SSH_ARG=AHO ssh -i ~/.ssh/id_rsa -oSendEnv="SSH_ARG" \
     user@serverAddress  // ここまでコマンド
SSH_ARG=AHO  // ← これが表示結果
client$ 
となり、めでたく環境変数が渡せました。もちろん複数の環境変数も渡せます。
ここで、例のechoの代わりに自分で書いたscript等を使えば、CからSを自由にコントロールでき、また、意図しないコマンドの実行を抑えることが出来ます。

2014年4月11日金曜日

[scala][akka] microkernelを使って独立したプロセスに立てたRemote Actorとの通信サンプル

自分は、akkaの「Let it crash」というという発想が大好きなんです。
初めて「Let it crash」というスローガンを見たとき
{だよね。それでいいんだよね。}
と感激しました。

ただ、crashさせるためのSupervisor(docサンプル)の仕組みは、Actorはcrashさせるけど、jvmまでは殺してくれない。自分の場合、ヘビーな計算をscalaからloadLibraryしたc++のモジュールをJNA経由で使ってやっているので、jvmごとcrashさせたいんです。(外部libraryのunloadはできないようなので・・・)

そこで、登場するのがakkaのmicrokernelで、こいつを使うと簡単に独立したプロセスのjvmでActorを立てることが出来る。
必要に時は、こいつをjvmごとkillして復活させればいい ← もっとスマートな仕組みがきっとある気がする

で、HelloWorldのサンプルを書いてみた。
listenポートを可変にしたかったので、confファイルを使わない形で作ってみた。
application.confとかに設定が分かれてないほうがサンプルとしてもわかりやすいし。
まず、リモートで接続される側のmicrokernel
パラメータは、起動時に引数として渡せないので、起動する時は、
env AKKA_PORT=12345 akka hello.world.Sample.HelloLauncher
て感じで、環境変数経由にする。
あと、実行時にクラスが見つかんないみたいに怒られた時は、とりあえずjarに固めて、akkaのdeployフォルダに置けば見つかるようになります。
package hello.world.Sample

import akka.actor.{ Actor, ActorSystem, Props }
import akka.kernel.Bootable

class HelloLauncher extends Bootable {
    val port = System.getenv("AKKA_PORT")

    val conf = ConfigFactory.parseString(s"""
      akka.actor.provider = akka.remote.RemoteActorRefProvider
      akka.remote.netty.tcp.hostname = 127.0.0.1
      akka.remote.netty.tcp.port = ${port}
    """.stripMargin).withFallback(ConfigFactory.load());    
    val system = ActorSystem("HelloSystem", conf)

    def startup = {
        system.actorOf(Props[Hello], "hello")
    }

    def shutdown = {
        system.shutdown()
    }  
}

class Hello extends Actor {
  def receive = {
    case msg: String =>
      println("Hello: msg = " + msg)
  }
}
つぎに、Helloにメッセージを投げる側のサンプル
env AKKA_PORT=12345 akka hello.world.Sample.HelloLauncher
env AKKA_PORT=23456 akka hello.world.Sample.HelloLauncher
のように、別コンソールで2つのmicrokernelを起動した前提で動作させる。
object HelloCaller {

import akka.actor.{ Actor, ActorSystem, Props }
    
  def main(args: Array[String]) {
    System.setProperty("akka.actor.provider", "akka.remote.RemoteActorRefProvider")
    // このポートはリモート側でsenderにメッセージを戻す時に使用する
    System.setProperty("akka.remote.netty.tcp.port", "10000")
    val system = ActorSystem("CallerSystem")
    val actor1 = system.actorSelection("akka.tcp://HelloSystem@127.0.0.1:12345/user/hello")
    val actor2 = system.actorSelection("akka.tcp://HelloSystem@127.0.0.1:23456/user/hello")
    actor1 ! "Hello,"
    actor2 ! "Hello,"
    Thread.sleep(1000)
    actor1 ! "World."
    actor2 ! "Another actor."
    Thread.sleep(1000)
    actor1 ! "Yeaaahhh!!!"
    actor2 ! "Hooooooo!!!"
    Thread.sleep(1000)
    system.shutdown
  }

}
なお、mainから呼ぶ側のlibraryDependenciesには、akka-kernel, akka-actor, akka-remote を追加しています。

[sacla] akkaのmicrokernelに引数を渡す

akkaのmicrokernelは、Actorを別プロセスで簡単にバンバン立てられるので、便利なんですけど、今のバージョン(akka 2.3.2)だと、引数を渡せないので困った。

自分はlinuxのコマンドに不慣れで、解決法を忘れそうなのでメモ。

まず、akkaのbinにパスが通っているとして、普通のmicrokernelの起動は
akka com.xxx.packageName.ClassName
でやるんだけど、引数を渡したい時に、
akka com.xxx.packageName.ClassName arg1 arg2
とかやっても、Bootableにargsを渡してくれない。(そういうインターフェースになっていない)
なんでかというと、akkaコマンドが、複数のパラメータが
akka Class1 Class2 Class3
なんてあったときに、3つのBootableを一気に立ち上げるというふうに使っているから(参照)っぽい。

で、解決としては、linuxのenvコマンド(環境を変更してプログラムを実行する)を使用してmicrokernelを
env AHO=xxx BAKA=yyy akka com.xxx.packageName.ClassName
のように起動し、Bootableの中で
  val aho = System.getenv("AHO")
  val baka = System.getenv("BAKA")
とやって引数を取得しました。