「この環境変数をあのサーバでも！」リモートサーバへのログイン時に環境変数を設定する方法

# TL;DR

リモートサーバにログインする時に、ローカルサーバの特定の環境変数を引き継いでくれると少し楽なんだけどなー。と思うことがたまーにあるのですが、そういった時にイケてる感じに環境変数を設定してログインする方法です。

# 前提条件

ローカルサーバもリモートサーバも Linux で動いている前提です。

# それって `~/.ssh/environment`?

いいえ、ここでは使いません。

というか、 `~/.ssh/environment` って、リモートサーバ側の sshd で `PermitUserEnvironment no` になっていることが多くてあまり使える場所がないです。。。
もちろんリモートサーバ側の sshd の設定を変えれば使えるのですが、商用サーバだったり多人数で触っているサーバだったりして設定をいじりにくいんですよね。。。

# `~/.ssh/environment` じゃないやり方

sshd の設定を変更できない(したくない)のなら別の方法をとりましょう。

## こうやるんだ！

`ssh` コマンドでちょっとおまじないを書けば、簡単に環境変数を渡すことができます。

```shell-session
[ktooi@local ~]$ VAR="HOGE"
[ktooi@local ~]$ ssh -t remote.example.com "VAR=$(printf %q "$VAR") bash"
[ktooi@remote ~]$ echo $VAR
HOGE
```

カンタンでしょう？

ちなみに、この方法で環境変数を引き継いだ場合には、リモートサーバ側のシェルではあたかも `export` した環境変数のようにふるまいます。

```shell-session
[ktooi@remote ~]$ cat <<\__EOT__ | bash
echo $VAR
__EOT__
HOGE
```

## 複数の環境変数の渡し方

`VAR=$(printf %q "$VAR")` と同様の書き方で、環境変数名を変えたうえで半角スペースで区切れば OK です。

```shell-session
[ktooi@local ~]$ VAR1="HOGE"
[ktooi@local ~]$ VAR2="FUGA"
[ktooi@local ~]$ ssh -t remote.example.com "VAR1=$(printf %q "$VAR1") VAR2=$(printf %q "$VAR2") bash"
[ktooi@remote ~]$ echo "$VAR1, $VAR2"
HOGE, FUGA
```

## もっとシンプルにやりたい？

え、こんなの毎回打ち込んでいられないですか？
なら [本稿のこの利用例](#bashrc-あたりに関数を登録してラクラク環境変数コピーライフを送る) を参考にしてみてください。だいぶ楽になると思います。

## 解説

上記のやり方を見て納得できた人はこの項目をスキップしましょう。
いまいちピンとこなかった人は読んでみてください。

では、下記のコマンドを例にして解説します。

```shell-session
$ VAR="HOGE"
$ ssh -t remote.example.com "VAR=$(printf %q "$VAR") bash"
```

### ■ `ssh` の `-t` オプション

このオプションで仮想端末を割り当てます。これがないと、リモートサーバにログインした後に下記のようにプロンプトが表示されなかったり、タブ補完が効かなかったりしていろいろと悲しい思いをします。。。

```shell-session
[ktooi@local ~]# ssh remote.example.com "VAR=$(printf %q "$VAR") bash"
hostname
remote.example.com
```

### ■ VAR=$(printf %q "$VAR") bash

この部分は、ローカルサーバで解釈されるところと、リモートサーバで解釈されるところがあるので2つに分けて考えます。

#### ■ $(printf %q "$VAR")

この部分はローカルサーバ側で解釈されます。
これは `$VAR` に含まれる文字を、必要に応じてエスケープしてくれる処理です。例えば ` `(半角スペース)や `;`, `#` などの記号をエスケープし、これらの文字が含まれていた場合に意図しない動作を防ぐ効果があります。 下記はこのエスケープ処理の例です。

```shell-session
$ VAR="STEINS;GATE 0"
$ printf %q "$VAR"
STEINS\;GATE\ 0
```
なお、エスケープが必要な文字が含まれないことが明らかな場合には、下記のように単に `$VAR` とすることも可能です。

```shell-session
$ ssh -t remote.example.com "VAR=$VAR bash"
```

但し、エスケープ処理を省略する時は必ず `$VAR` の値が信用できる場合のみにしてください。
そうでないと OS コマンドインジェクションされちゃうかもしれませんよ？

どういうことかいまいちわからない人は試しに `$VAR` に `HOGE ls -la;` を代入してから `ssh` してみましょう。

```
$ VAR="HOGE ls -la;"
$ ssh -t remote.example.com "VAR=$VAR bash"
```
リモートサーバにログインした直後に、リモートサーバのカレントディレクトリ内の一覧が表示されたはずです。
そう、 `$VAR` に含まれていたコマンドが実行されてしまったわけです。

もし `ls -la` の代わりに `rm -rf /` なんて値が設定されていたら目も当てられません。[^1]
**やるなよ、絶対にやるなよ。**[^2]

[^1]: もちろん `rm -rf /` なんて `root` でログインしなければほぼ無力な上にナウい `rm` コマンドだと `root` でも止まってくれますが、 `rm -rf ./` にすれば有効な打撃となるシーンは格段に増えるでしょう。
[^2]: フリではない。

#### ■ VAR=～ bash

この部分はリモートサーバ側で解釈されます。
これは `bash` を実行する際に、 `VAR` という環境変数に `～` の値を代入して実行するという書き方です。本稿の例だと、 `～` の部分は `HOGE` または `STEINS\;GATE\ 0` となりますので、下記のように解釈されます。

```shell-session
$ VAR=HOGE bash
$ VAR=STEINS\;GATE\ 0 bash
```

これは、 `export` を利用して

```shell-session
$ export VAR=HOGE
$ bash
```
等としたときと似たように振るいまいますが、 `export` の場合は `export` を実行した後から `unset` 等するまでに実行されるすべてのコマンドに `VAR=HOGE` が設定されるのに対し、 `VAR=HOGE コマンド名` だとその時に実行したコマンドにしか影響を及ぼしません。

# 利用例

## ローカルサーバのプロンプトをリモートサーバで再現

オレサマ謹製のプロンプトをリモートサーバでの作業にも適用したいのだけれども、アカウントが共有(`ec2-user` とか)だったり運用上の制約で `.bash_profile` や `.bashrc` を触れないときにローカルサーバのオレサマ謹製プロンプトをコピーする方法です。

```shell-session
[08:59:34][ktooi@local ~]$ ssh remote.example.com
[ktooi@remote ~]$ exit  # 普通にログインするとプロンプトが RHEL 系のデフォルト。
Connection to remote.example.com closed.
[09:00:00][ktooi@local ~]$ ssh -t remote.example.com "PS1=$(printf %q "$PS1") bash"
[09:00:10][ktooi@remote ~]$  # ローカルサーバのプロンプトのフォーマットがコピーされた。
```

そもそものプロンプトの変更については記事を書かれている方がいらっしゃるので、試しに変更してみたい方は下記辺りを参照してみてください。

*   [Linuxのプロンプトの表示を変更する](https://qiita.com/katsukii/items/da37d1fdf974bd0e4c2f)
*   [Bashプロンプトの変更](https://qiita.com/zaburo/items/9194cd9eb841dea897a0)

## パスワードをエクスポート

※ 後述の [注意](#注意) を参照の事。

[Linux 作業手順書からべた書きパスワードをなくすシンプルなアイディア](https://qiita.com/ktooi/items/ba156bb1848c31f04949) で記載した方法でパスワードを環境変数に入れ、それをリモートサーバに渡してリモートサーバ上でのパスワード入力を省く方法です。

```shell-session
[ktooi@local ~]$ read -sp "Please input your password: " __pass
Please input your password:   # ← パスワードを入力
[ktooi@local ~]$ ssh -t remote.example.com "__pass=$(printf %q "$__pass") bash"
[ktooi@remote ~]$ curl -u "user:${__pass}" http://example.com
```

## AWS CLI のクレデンシャル情報をエクスポート

※ 後述の [注意](#注意) を参照の事。

まぁ、普通はおとなしく AWS CLI を実行するサーバの `~/.aws/credentials` に「AWS アクセスキー ID」と「AWS シークレットアクセスキー」を保存していると思うので、下記はあくまで例ですが。

```shell-session
[ktooi@local ~]$ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
[ktooi@local ~]$ AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[ktooi@local ~]$ ssh -t remote.example.com "AWS_ACCESS_KEY_ID=$(printf %q "$AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY=$(printf %q "$AWS_SECRET_ACCESS_KEY") bash"
[ktooi@remote ~]$ aws ec2 describe-instances --region us-east-1"
```

本稿の趣旨からは外れますが、そもそもリモートサーバで `aws` コマンドを実行したいだけだったら下記のようにやると手間が減ります。

```shell-session
$ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
$ AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ ssh -t remote.example.com "AWS_ACCESS_KEY_ID=$(printf %q "$AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY=$(printf %q "$AWS_SECRET_ACCESS_KEY") aws ec2 describe-instances --region us-east-1"
```

## `.bashrc` あたりに関数を登録してラクラク環境変数コピーライフを送る

自分の `~/.bashrc` に下記の関数を登録しておくと、 `sshe` と `essh` というコマンドを打てるようになります。

```~/.bashrc
function sshe { local ssh_opts=(); for opt in "$@"; do [ "$opt" == "--" ] && shift && break; ssh_opts=("${ssh_opts[@]}" $1); shift; done; local envs=(); for env in "$@"; do grep -q '^[_a-zA-Z][_a-zA-Z0-9]*$' <<< "$env" || { echo "invalid environment variable name \"$env\""; return 1; }; envs=("${envs[@]}" "$env=$(printf %q "$(eval echo \"\$$env\")")"); done; ssh "${ssh_opts[@]}" -t "${envs[*]} bash"; }
function essh { local envs=(); for env in "$@"; do [ "$env" == "--" ] && shift && break; grep -q '^[_a-zA-Z][_a-zA-Z0-9]*$' <<< "$env" || { echo "invalid environment variable name \"$env\""; return 1; }; envs=("${envs[@]}" "$env=$(printf %q "$(eval echo \"\$$env\")")"); shift; done; local ssh_opts=(); for opt in "$@"; do ssh_opts=("${ssh_opts[@]}" $1); done; ssh "${ssh_opts[@]}" -t "${envs[*]} bash"; }
```
(関数の解説も書こうかと一瞬思ったけど、 **うん、だめだ、やめておこう。。。**)

`sshe` も `essh` もやれることは一緒で、 `ssh` の引数と環境変数名を `--` で区切って渡すとリモートサーバにログインする時に指定した環境変数の内容をコピーしてくれます。
但し、 `sshe` と `essh` は引数の順番が異なっており、それぞれ

```
sshe [ssh のオプション...] -- [環境変数名...]
essh [環境変数名...] -- [ssh のオプション...]
```
のようになります。なお、 `[ssh のオプション...]` に `-t` を入れる必要はありません。関数のなかにハードコードしてあります。
また、 `[ssh のオプション...]` には `-o ConnectTimeout=10` といったオプションを指定することも可能です。

実際の使い方はそれぞれ下記のようになります。

```shell-session
$ VAR1=HOGE
$ VAR2=FUGA
$ sshe remote.example.com -- VAR1 VAR2 PS1
または
$ essh VAR1 VAR2 PS1 -- remote.example.com
```

どちらの場合も、下記のコマンドを実行したときと同じような動作になります。

```shell-session
$ ssh -t remote.example.com "VAR1=$(printf %q "$VAR1") VAR2=$(printf %q "$VAR2") PS1=$(printf %q "$PS1") bash"
```

### 応用編

特定の環境変数を必ずリモートサーバにコピーしたい場合は、 `~/.bashrc` にさらに新たな関数を定義すれば可能です。
例えば `$PS1` を必ずコピーしたい場合は、次のような関数を `~/.bashrc` に定義します。

```~/.bashrc
function psssh { essh PS1 -- "$@"; }
function psessh { essh PS1 "$@"; }
function pssshe { sshe "$@" PS1; }
```

この場合は、下記のように実行することができます。

```shell-session
$ psssh remote.example.com  # コピーする環境変数を指定できないが、シンプルに実行可能。
$ VAR=HOGE
$ psessh VAR -- remote.example.com  # コピーする環境変数を追加指定できるが、コマンドが長くなる。
$ pssshe remote.example.com -- VAR  # コピーする環境変数を追加指定できるが、コマンドが長くなる。
```

# 注意

この方法は便利なのですが、リモートサーバで `root` 権限を持っている第三者がいた場合、渡した環境変数の名前と値を簡単に見ることができてしまうので注意が必要です。
クレデンシャルな情報を渡すときは気を付けましょう。[^3]

[^3]: そもそも、 `root` 権限を行使した盗聴を意識しなければならないサーバで作業なんかしたくはありませんけれども。

また、クレデンシャル情報を引数にしてコマンドに渡すのもセキュアではありません。コマンド実行中に第三者に `ps` コマンドを叩かれたりすると引数を含むプロセスのリストを取得されてしまいます。 `curl` 等一部のコマンドはプロセス起動後に `/proc/<PID>/cmdline` からクレデンシャル情報を削除してくれるので緩和はしていますが、それでもセキュリティ的にはあんまりよろしくないです。

# まとめ

*   `$ ssh -t remote.example.com "VAR=$(printf %q "$VAR") bash"` といった感じに `ssh` するとお手軽に環境変数をコピーできる。
*   [.bashrc あたりに関数を登録してラクラク環境変数コピーライフを送る](#bashrc-あたりに関数を登録してラクラク環境変数コピーライフを送る) を参考にしてもらうともっとお手軽に環境変数をコピーできる。

おしまい。

# FYI

## 参考にしたページ

*   [Bashで文字列をエスケープをする](https://qiita.com/kawaz/items/f8d68f11d31aa3ea3d1c)

## 類似するページ

*   [ログイン前にbashの環境変数を設定するsshワンライナー](https://qiita.com/noexpect/items/e0e68610592874633653)