ルモーリン
ホーム 更新 Perl Sample ランドナー サービス 雑談 コースガイド 鉄ゲタ 自転車 Linux リンク 連絡先

管理者PCのIPアドレスだけポートを開く

2019-03-01

前置き

おそらく、ブラウザがクライアント証明書でサーバーにアクセスすれば済む事です。 わざわざ手間をかけているだけですから笑って見てください。

背景

サーバー管理者の私だけがsshやftpやメールの送受信を行いたいので、自宅のグローバルIPアドレスに限り、それ用のポートを開きたい。 固定IPではないからプロバイダが所有するIPアドレスのどれかに変更されてしまう。 ひと昔前であれば、大きなCIDRをいくつか所有する程度でしたからiptablesでCIDRを全部指定しておけば カバーできました。 けれどIPv4枯渇の都合でプロバイダ間の譲り合いが多発、小さなCIDRを多量に所有する状態のようです(推測)。 特に私が契約している零細プロバイダは上位プロバイダから回線を借りていて、そこが小さいCIDRを持っています。 CIDRからハミ出た(=別のCIDRに変わった)ことが1ヵ月に4度も起き、 その度にVPSを修復モードで再起動してシングルモードでログイン、iptablesを指定し直しました。 VPSは通常通り稼働していますから、Webサーバーに管理者PCのグローバルIPアドレスを伝えてiptablesを指定し直せば良い訳です。

概要

  1. 管理者PCからサーバーにグローバルIPアドレスを問い合わせ
  2. IPアドレスを入れたJSONファイルを作成
  3. 秘密鍵で署名
  4. 公開鍵は予めVPSに転送
  5. 署名したJSONファイルをサーバーへアップロード
  6. サーバーが署名を検証
  7. リモートホストアドレスとの一致を検証
  8. 管理者PCのIPアドレスと確認できたらファイル出力
  9. スーパーユーザーがファイルを監視、出力されていればiptablesへ反映、ファイルを削除

鍵ペア作成と公開鍵のVPS転送

PCでWindows用のKleopatraを使って作成し、公開鍵をVPSへ転送してgpgに登録、信頼度を最高に指定しました。

登録プログラム

サーバーへの問い合わせ、JSONファイル出力、gpgで署名、サーバーへアップロードを行います。 コードはこんな感じ。 「???...???」はランダム英数字32文字でURLの末尾になります。
#!/usr/bin/env perl -w

use utf8;
use strict;
use warnings;

use Data::Dumper;
use Encode::Argv;
use Encode::Locale;
use JSON;
use LWP::UserAgent;

use feature "say";
use open IO => ":utf8";

binmode STDIN, ":encoding(console_in)";
binmode STDOUT, ":encoding(console_out)";

$| = 1;

say "管理PCのリモートアドレスを取得";
my $ua = LWP::UserAgent->new;
my $url = "https://www.lemorin.jp/api/";
my $admin = "????????????????????????????????";
my $res = $ua->get("$url$admin");
my $ip_current;
if ($res->is_success) {
	$ip_current = $res->content;
	say "リモートアドレス:$ip_current";
} else {
	say "失敗";
	exit;
}

my $ip_json = encode_json {admin_pc => $ip_current};
say "JSON:$ip_json";

say "アドレスファイル生成";
my $ip_txt = "current_ip.txt";
if (open my $ip_fh, ">", $ip_txt) {
	print $ip_fh $ip_json;
	close $ip_fh;
}

say "アドレスファイルに署名";
my $ip_sign = "current_ip.txt.sign";
unlink $ip_sign;
system "gpg --output $ip_sign --clearsign $ip_txt";
if (open my $sign_fh, "<", $ip_sign) {
	while (<$sign_fh>) {
		chomp;
		say;
	}
	close $sign_fh;
}

say "管理PCを登録";
$res = $ua->post("$url$admin",
	Content_Type => "form-data",
	Content => [
		current_ip => [$ip_sign],
	],
);

say "結果";
if ($res->is_success) {
	say $res->content;
} else {
	say $res->status_line;
}
実行結果はこんな感じです。 gpgを呼び出すと秘密鍵のパスフレーズを聞いてきますので自分で入力します。
管理PCのリモートアドレスを取得
リモートアドレス:36.53.202.47
JSON:{"admin_pc":"36.53.202.47"}
アドレスファイル生成
アドレスファイルに署名
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

{"admin_pc":"36.53.202.47"}
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEwEitvUyxvuFkiJK9JKNDPIuht94FAlx5SFMACgkQJKNDPIuh
t94+Mg//WqN1IaUAVwWhM8lEI78EPuxLCejPLRd6v6lmYO0f19xpnAECflzf6bbx
pNdsMnkkZlYLosA2VE4xtp0RII4eZxwsfRVxPzPZPV6kNW5flnDmdLUZ584gxHFr
Hau1IuSUqNP60E/STgRGomTHty7q5QdAwuzQzz/9YicupdKip5zESaW/VWbT4bIf
H7mZKYZQr90+wc8OUl7zZ0EMwyUkPkiHeBfbGCxR1YPztvRFuPPIsXdA0P2EdSFO
kivMmHUgxCGSb/EjAk4/sA1/qUJmb2cWROzwXgHE3tqNyxJBFrBWJpQjDyABLTVs
+cLb3rAlI5gHS8+rmCi0M8XWBPshzouv2GtGS5IdWoG92oSbzOofG1PrL9OyTMOV
TMwyI8BvEuDwdPaPm4f7ukl+16DCEMbhzQ8pQBJtalcmYCKnhRtGPDBoEvpVyxEB
Bep3xQTloQxze/yWeUhctyD6HGM5CRIKrJW83GVYvZlCv7HlwA8Mq3XcQIG/BMC7
r8ETiDTsu65w31+clsdkYOTtOUoZQT86gvpSQMPnREAzIjjzl8xamr5TfQ0/hMBh
nshnkVbzlc9mkrtUDdGIJXDETjp69tRqGmI3ZQYW2hU//MNervvMfhbLNNeP3MNL
O0NCT/kl0IIyGPeaqI69o2ixuYQ099bTZqtMkarCxuC4pWDRJ8U=
=TYRA
-----END PGP SIGNATURE-----
管理PCを登録
結果
result is 0
current IP is 36.53.202.47
update IP

受付サーバー(ルーター)

VPSのWebサーバーはMojoliciousです。 アプリケーションのstartupでルーターを設定します。 getメソッドはグローバルIPアドレスを返し、postメソッドは署名付きJSONファイルを受け取ります。 「???...???」は登録プログラムと同じものです。
	# 管理PC登録
	my $admin = "????????????????????????????????";
	$r->get("/api/$admin")->to("ApiAdminPC#index");
	$r->post("/api/$admin")->to("ApiAdminPC#current_ip");

受付サーバー(コントローラー)

コントローラーをApiAdminPC.pmに用意します。 CPANにPGPを扱うモジュールがいくつかあって試しましたけれど上手いこと使えなかったのでgpgを起動します。 コード中の「???...???」はMojoliciousのアプリケーションのパスです。
package Myapp::Controller::ApiAdminPC;
use Mojo::Base 'Mojolicious::Controller';

use Crypt::GPG;
use Encode;
use JSON;

# This action will render a template

sub index {
	my $self = shift;
	$self->render(text => $self->tx->remote_address);
}

sub current_ip {
	my $self = shift;

	my $sign_file = $self->param("current_ip")->asset->to_file;
	my $filename = $sign_file->path;
	my $result = system "gpg --verify '$filename'";
	if (-1 == $result) {
		$self->render(text => "error is $!");
	} else {
		$result = $? >> 8;
		my $line = "result is $result";
		my ($admin_pc) = grep {/admin_pc/} split /\r\n/, $sign_file->slurp;
		my $json = decode_json $admin_pc;
		my $current_ip = $json->{admin_pc};
		if (!defined $current_ip) {
			$line .= "\ncurrent IP not found";
		} elsif ($current_ip ne $self->tx->remote_address) {
			$line .= "\ncurrent IP is not remote host";
		} else {
			$line .= "\ncurrent IP is $current_ip";
			if (!$result) {
				if (open my $ip_fh, ">", "???????????????????????/current_ip.txt") {
					print $ip_fh "$current_ip\n";
					close $ip_fh;
				}
				$line .= "\nupdate IP";
			} else {
				$line .= "\nverify error";
			}
		}
		$self->render(text => $line);
	}
}

1;

iptablesへ反映

スーパーユーザーでcrontabを使って5分毎に実行しています。 新しいIPアドレスのファイルがあれば、それをiptablesへ登録します。 iptablesにカスタムチェインADMIN_PCを追加しておき、管理者だけが使うポートを転送してあります。 ADMIN_PCに自宅のグローバルIPアドレスを追加すれば、それらのポートを自宅から使えるようになります。 コード中の「???...???」はMojoliciousのアプリケーションのパスです。
#!/usr/bin/perl -w

use utf8;
use warnings;
use strict;

use feature "say";

my $current_ip;
my $ip_file = "???????????????????????/current_ip.txt";
if (open my $ip_fh, "<", $ip_file) {
        ($current_ip) = <$ip_fh>;
        chomp $current_ip;
        close $ip_fh;
        unlink $ip_file;
}

if ($current_ip) {
        system "/sbin/iptables --insert ADMIN_PC 1 --source $current_ip/32 --jump ACCEPT";
}

exit;