JavaだけでSSHトンネリングを経由してDB接続する

PCアザラシ IT

やりたいこと

・リモートのMySQLにインターネット経由で接続したい。
・安全にSSHトンネル経由で接続したい。

実現方法

JSchというものを使うとうまくいった。

maven

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

java

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

import java.sql.*;

public class TunnelTest {

    public static void main(String[] args) throws ClassNotFoundException, SQLException, JSchException {
        JSch jsch = new JSch();
        jsch.addIdentity("/path/to/.ssh/id_rsa");
        jsch.setKnownHosts("/path/to/.ssh/known_hosts");
        Session session = jsch.getSession("{SSH_USERNAME}", "{SSH_SERVER}", 22);
        session.connect();
        try {
            // 左がトンネル入口で右がSSHサーバから見たDB接続先。"localhost"は省略可。
            session.setPortForwardingL("localhost", 3307, "{MYSQL_SERVER}", 3306);
            Class.forName("com.mysql.cj.jdbc.Driver");
            Connection con = DriverManager.getConnection(
                    // 自ホストのトンネル入口を指定してDBに接続
                    "jdbc:mysql://localhost:3307/",
                    "{DB_USERNAME}",
                    "{DB_PASSWORD}"
            );
            // 適当にクエリを投げてみる
            try (Statement st = con.createStatement()) {
                try (ResultSet resultSet = st.executeQuery("show databases")) {
                    while (resultSet.next()) {
                        System.out.println(resultSet.getString(1));
                    }
                }
            }
        } finally {
            session.disconnect();
        }
    }
}

knownhostsはファイルパスではなくファイルの中身の情報をInputStreamで与えてもよい。

jsch.setKnownHosts(new ByteArrayInputStream("[example.com]:22 ssh-rsa ABC...==".getBytes()));

つまづいたポイント

invalid privatekeyエラー

こんなエラーが出た。

Exception in thread "main" com.jcraft.jsch.JSchException: invalid privatekey: [B@4459eb14
	at com.jcraft.jsch.KeyPair.load(KeyPair.java:664)
	at com.jcraft.jsch.KeyPair.load(KeyPair.java:561)
	at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
	at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
	at com.jcraft.jsch.JSch.addIdentity(JSch.java:366)
	at TunnelTest.main(TunnelTest.java:13)

Process finished with exit code 1

原因は秘密鍵の仕様変更

ググるとstack overflowに情報があった。秘密鍵のフォーマットが新しいものだとだめらしい。

The root cause was discovered to be the ssh private key mismatch. The exception only happened for users with key of newer kind ed25519, which outputs this key header:

-----BEGIN OPENSSH PRIVATE KEY-----

instead of kind RSA:

-----BEGIN RSA PRIVATE KEY-----

regenerating an RSA key (ssh-keygen -t rsa), made the exception go away.

Edit following comments: If you have OpenSSH 7.8 and above you might need to add -m PEM to the generation command: ssh-keygen -t rsa -m PEM
"Invalid privatekey" when using JSch - Stack Overflow

日本語ではこちらで丁寧に解説されていた。

【OpenSSH 7.8】秘密鍵を生成する形式が変更になった件について | DevelopersIO
こんにちは。DI部の春田です。 皆さん、日頃からssh-keygenのコマンドはよく使われるかと思います。 先日、GoのSSHライブラリに関する記事をあげましたが、秘密鍵のヘッダが変更されている件について、もう少し詳しく …

手元の秘密鍵を見てみる。

% head -n 1 ~/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----

まさしくこれが原因だ。

以前の形式で秘密鍵を作って解決

鍵を新しく作ってみる。

% ssh-keygen -t rsa -m PEM -f {新しい秘密鍵のファイルパス}
% head -n 1 {新しい秘密鍵のファイルパス}
-----BEGIN RSA PRIVATE KEY-----

この鍵を使うと、うまく動いた。

以上


参考サイト

Connect to remote MySQL database through SSH using Java - Stack Overflow
JSch - Java Secure Channel
IT
広告
一郎くんどっとこむ