Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

Zabbix API との通信を暗号化する

概要

前回記事
Javaアプリから Zabbix API を使う - Status Code 303 - See Other

java アプリケ−ションから通信はできたが、HTTP 通信のため平文で通信している。
外部にこれらを公開することを考慮して、今回は通信を暗号化する。

また、Zabbix サーバとの SSL 通信に用いる証明書はいわゆる「オレオレ証明書」を用いる。
これらは信頼できない証明にあたるため、Java はデフォルトで通信を許可しないが、これらを無理矢理設定して通信する。

Raspberrypi 設定

Raspberrypi の SSL 対応

作業手順は、下記記事を参考に行った。
Qaplaの覚書・メモ・備忘録・独言 Raspberry Piを公開サーバ化(HTTPS対応)

Linux 系 OS の SSL 導入記事は、Apache2 の conf ファイル名が違ったり、
apt-get から取得したアプリケーションのフォルダ階層が違ったりしていたため、
手順だけ知りたい人は、Raspberrypi での導入記事を選択した方が良さそう。

なお、この証明書作成の過程で何をやっているかについて興味があるなら、この記事を参考に。
オレオレ証明書をopensslで作る(詳細版) - ろば電子が詰まっている

Java アプリケーション

前回と同じく Jersey を用いて実装を行う。まず、何もしないでアクセス先を http → https に変更すると、
下記のようなエラーログが出る。

Exception in thread "main" javax.ws.rs.ProcessingException: Already connected
	at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:264)
	at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:684)
	at org.glassfish.jersey.client.JerseyInvocation$1.call(JerseyInvocation.java:681)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
	at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:444)
	at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:681)
	at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:437)
	at org.glassfish.jersey.client.JerseyInvocation$Builder.post(JerseyInvocation.java:343)
	at api.zabbix.ZabbixAPIAccess.getResponse(ZabbixAPIAccess.java:76)
	at api.zabbix.ZabbixAPIAccess.getToken(ZabbixAPIAccess.java:82)
	at api.zabbix.ZabbixAPIAccess.request(ZabbixAPIAccess.java:41)
	at api.zabbix.ZabbixAPIAccess.request(ZabbixAPIAccess.java:51)
	at api.zabbix.Main.main(Main.java:24)
Caused by: java.lang.IllegalStateException: Already connected
	at sun.net.www.protocol.http.HttpURLConnection.setRequestProperty(HttpURLConnection.java:3014)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.setRequestProperty(HttpsURLConnectionImpl.java:316)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.setOutboundHeaders(HttpUrlConnector.java:421)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.access$100(HttpUrlConnector.java:96)
	at org.glassfish.jersey.client.internal.HttpUrlConnector$4.getOutputStream(HttpUrlConnector.java:384)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(CommittingOutputStream.java:200)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(CommittingOutputStream.java:194)
	at org.glassfish.jersey.message.internal.CommittingOutputStream.commit(CommittingOutputStream.java:262)
	at org.glassfish.jersey.message.internal.OutboundMessageContext.commitStream(OutboundMessageContext.java:816)
	at org.glassfish.jersey.client.ClientRequest.writeEntity(ClientRequest.java:545)
	at org.glassfish.jersey.client.internal.HttpUrlConnector._apply(HttpUrlConnector.java:388)
	at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:285)
	at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:255)
	... 14 more

これは、どうやら https に変更すると、自動的に SSL で通信を行ってくれるのだが、
信頼できない証明書を利用した SSL 通信はデフォルトで、例外を発生させるみたいだ。

事実、信頼できる証明書を使用するサイトでは、前回実装でも SSL でアクセスできる(GoogleYahoo! Japan! で確認)。
では、信頼できない証明書を使っている場合はどうすれば良いか。

答えは、デフォルト設定を無理矢理変更して、アクセスする。
一般的に SSL 導入利点として挙げられるのは、

  1. 通信の暗号化(機密性)
  2. 通信先の相手が正しいことを保証(真正性)
  3. 改竄を検知できることを保証 (完全性)

信頼できない証明書を受け入れるということは、少なくとも「通信先の相手が正しいことを保証」はできなくなる。
もし、 名前解決の部分を悪意をもった誰かが操作して別のサイトに導いた場合、攻撃者はプログラムの通信に関与できてしまう。

このようなセキュリティ上のリスクがあることを認識すること。・・とはいえ、SSL 証明書は導入コストがかかるので、
開発中に「外部に公開しない・重要な処理や情報を扱わない」なら、例外を設けて通信をすることも必要になる。

前置きが長くなったが、実装内容。前回の ZabbixAPIAccess クラスに対して、次の static メソッドを追加。

	private static Client getSSLClient() throws NoSuchAlgorithmException, KeyManagementException {
		SSLContext sc = SSLContext.getInstance("TLSv1");
		System.setProperty("https.protocols", "TLSv1");
		// 全ての証明書を信用するトラストマネージャの作成。詳しくは、JSSE を検索すること。
		TrustManager[] trustAllCerts = { new X509TrustManager() {

			@Override
			public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
			}

			@Override
			public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
			}

			@Override
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}
		} };
		sc.init(null, trustAllCerts, new java.security.SecureRandom());

		// 全ホストを妥当だとする。詳しくは、JSSE を検索。
		HostnameVerifier allHostsValid = new HostnameVerifier() {

			@Override
			public boolean verify(String hostname, SSLSession session) {
				return true;
			}
		};
		// Client を Builder から作成するときに、上記設定を適用する。
		return ClientBuilder.newBuilder().hostnameVerifier(allHostsValid).sslContext(sc).build();
	}

そして、ZabbixAPIAccess クラスのgetResponse メソッドを次のように変更する。

	private Response getResponse(Map<String, Object> params) throws KeyManagementException, NoSuchAlgorithmException {
		return getSSLClient().target(property.getString("zabbix.api.url")).request()
				.post(Entity.entity(params, MediaType.APPLICATION_JSON));
	}

これに伴い、様々な所でキャッチ例外(KeyManagementException, NoSuchAlgorithmException)をどうにかしろと言われるので、throws 宣言を追加。
以上で完了。

ただし、さきほども記述した通り。セキュリティ上あまり良くはないので、
本番時には、ルート証明書を取得するか、例外設定処理を記述した方が良い。
そのときは、getSSLClient メソッドのクライアント取得手順を変更する。

その設定内容としては、以下が参考になる。
JavaのSSLSocketでSSLクライアントとSSLサーバーを実装する:CodeZine(コードジン)
Java SE 6 用 JSSE リファレンスガイド

そういえば、暗号化といえば。
2016年2月から正式リリースされた、Zabbix 3.0 の拡張機能で、 Zabbix-server と Zabbix-agent 間の暗号化をサポートするようになったとか。
Zabbix-Agentとの通信を暗号化してみる. (3.0の機能) - フロッピーディスクの残骸

これも時間があるときに試してみよう。