[Android + ELK] java.security.cert.CertPathValidatorException: Trust anchor for certification path

接觸 ELK 一段時間了,本篇的背景是想要利用 ELK 建立一個接收錯誤訊息的環境,因為有一些手機端發生的錯誤並沒有辦法被偵測到,所以想要利用現有資源來支援手機程式的開發。本篇分成幾個部分做一個紀錄,首先要先在一台 CentOS 的 server 上面建立 ElasticSearch 與 Kibana 的服務,然後必須要在這兩的服務上面啟動 TLS (SSL) ,接著在 Android 程式端在建置好 ElasticSearchLogger 就可以將錯誤訊息記錄到 ELK 裡面,這次卡比較久的地方是在 java.security.cert.CertPathValidatorException: Trust anchor for certification path 這個錯誤,所以本篇以此為標題。

安裝 ElasticSearch 7.10.2 + Kibana 7.10.2

下載並且解壓縮 ElasticSearch + Kibana

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.2-linux-x86_64.tar.gz
tar -xzf elasticsearch-7.10.2-linux-x86_64.tar.gz
cd elasticsearch-7.10.2/ 

curl -O https://artifacts.elastic.co/downloads/kibana/kibana-7.12.0-linux-x86_64.tar.gz
tar -xzf kibana-7.10.2-linux-x86_64.tar.gz
cd kibana-7.10.2-linux-x86_64/

理論上要能夠利用以下的指令啟動:

cd elasticsearch-7.10.2
bin/elasticsearch

cd kibnana-7.10.2-linux-x86_64
bin/kibana

啟動的時候遇到以下的錯誤訊息:

ERROR: [3] bootstrap checks failed
[1]: max number of threads [999] for user [user] is too low, increase to at least [4096]
[2]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]
[3]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

這是因為負責啟動 ElasticSearch 的 Linux 使用者在 Linux 系統上的權限不足,轉換到 root 權限輸入以下指令查詢:

[elasticsearch@vmi327567 bin]$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 31202
max locked memory       (kbytes, -l) 262144
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65536
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

參考解決方法是在 /etc/security/limits.conf 裡面增加以下的設定,但是使用 ulimit -u 則無法成功。

<domain>      <type>  <item>         <value>
* hard nproc 4096
* soft nproc 4096
* hard nofile 65536
* soft nofile 65536

關於 vm.max_map_count 的設定,我們則是使用以下指令解決:

sysctl -w vm.max_map_count=262144

備註:預設啟動的位置,ElasticSearch 會在 9200 這個 port,Kibana 會在 port 5601。

安裝 SSL 到 ElasticSearch 與 Kibana 上

本節參考兩個系列的文章第一個系列是 Configuring SSL, TLS, and HTTPS to secure Elasticsearch, Kibana, Beats, and Logstash,另外一個系列是 ElasticSearch 官網的 Encrypting Communications in ElasticSearch 與他的中文翻譯,最後採用的是第一個方法,簡單來說它分成以下幾個步驟:

  1. 建立 instance.yml 關係檔案:
    vim ~/tmp/cert_blog/instance.yml 
    instances: 
        - name: 'node1' 
          dns: [ 'your-url' ] 
        - name: 'my-kibana' 
          dns: [ 'your-url' ]
  2. 建立 certificate 並且解壓縮:
    cd elasticsearch-7.10.2
    bin/elasticsearch-certutil cert --keep-ca-key --pem --in ~/tmp/cert_blog/instance.yml --out ~/tmp/cert_blog/certs.zip
    cd ~/tmp/cert_blog
    unzip certs.zip -d ./certs
    cp ~/tmp/cert_blog/certs/ca/ca* ~/tmp/cert_blog/certs/node1/* certs
  3. 更新 elasticsearch-7.10.2/config/elasticsearch.yml 檔案
    // ======================== Elasticsearch Configuration =========================
    //
    // NOTE: Elasticsearch comes with reasonable defaults for most settings.
    //       Before you set out to tweak and tune the configuration, make sure you
    //       understand what are you trying to accomplish and the consequences.
    //
    // The primary way of configuring a node is via this file. This template lists
    // the most important settings you may want to configure for a production cluster.
    //
    // Please consult the documentation for further information on configuration options:
    // https://www.elastic.co/guide/en/elasticsearch/reference/index.html
    //
    // ---------------------------------- Cluster -----------------------------------
    //
    // Use a descriptive name for your cluster:
    //
    cluster.name: your-cluster-name
    //
    // ------------------------------------ Node ------------------------------------
    //
    // Use a descriptive name for the node:
    //
    node.name: node-1
    //
    //
    // Add custom attributes to the node:
    //
    node.attr.rack: r1
    //
    // ----------------------------------- Paths ------------------------------------
    //
    // Path to directory where to store the data (separate multiple locations by comma):
    //
    path.data: /path/to/data
    //
    // Path to log files:
    //
    path.logs: /path/to/logs
    //
    // ----------------------------------- Memory -----------------------------------
    //
    // Lock the memory on startup:
    //
    bootstrap.memory_lock: false
    //
    // Make sure that the heap size is set to about half the memory available
    // on the system and that the owner of the process is allowed to use this
    // limit.
    //
    // Elasticsearch performs poorly when the system is swapping the memory.
    //
    // ---------------------------------- Network -----------------------------------
    //
    // Set the bind address to a specific IP (IPv4 or IPv6):
    //
    network.host: url-you-filled-in-instance.yml
    //
    // Set a custom port for HTTP:
    //
    http.port: 9200
    //
    // For more information, consult the network module documentation.
    //
    // --------------------------------- Discovery ----------------------------------
    //
    // Pass an initial list of hosts to perform discovery when this node is started:
    // The default list of hosts is ["127.0.0.1", "[::1]"]
    //
    discovery.seed_hosts: ["url-you-filled-in-instance.yml"]
    ////
    // Bootstrap the cluster using an initial set of master-eligible nodes:
    //
    cluster.initial_master_nodes: ["node-1"]
    //
    // For more information, consult the discovery and cluster formation module documentation.
    //
    // ---------------------------------- Gateway -----------------------------------
    //
    // Block initial recovery after a full cluster restart until N nodes are started:
    //
    gateway.recover_after_nodes: 1
    //
    // For more information, consult the gateway module documentation.
    //
    // ---------------------------------- Various -----------------------------------
    //
    // Require explicit names when deleting indices:
    //
    action.destructive_requires_name: true
    
    xpack.security.enabled: true
    xpack.security.http.ssl.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.http.ssl.key: certs/node1.key
    xpack.security.http.ssl.certificate: certs/node1.crt
    xpack.security.http.ssl.certificate_authorities: certs/ca.crt
    xpack.security.http.ssl.verification_mode: certificate
    xpack.security.transport.ssl.key: certs/node1.key
    xpack.security.transport.ssl.certificate: certs/node1.crt
    xpack.security.transport.ssl.certificate_authorities: certs/ca.crt
  4. 後續可以參考提供的網頁設定好 kibana 的 SSL。
創業 Client 往 ElasticSearch 推送 Event

關於推送 ElasticSearch 的 Event 可以參考檸檬爸之前整理過的常用 curl 指令,這邊我們是在 Android 上面實作的時候遇到以下的錯誤訊息:

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

我們參考網頁實作以下的 HttpsTrustManager 並且在產生 HttpConnection 的之前呼叫,HttpsTrustManager.allowAllSSL(),

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class HttpsTrustManager implements X509TrustManager {

    private static TrustManager[] trustManagers;
    private static final X509Certificate[] _AcceptedIssuers = new X509Certificate[]{};

    @Override
    public void checkClientTrusted(
            java.security.cert.X509Certificate[] x509Certificates, String s)
            throws java.security.cert.CertificateException {
    }

    @Override
    public void checkServerTrusted(
            java.security.cert.X509Certificate[] x509Certificates, String s)
            throws java.security.cert.CertificateException {
    }

    public boolean isClientTrusted(X509Certificate[] chain) {
        return true;
    }

    public boolean isServerTrusted(X509Certificate[] chain) {
        return true;
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return _AcceptedIssuers;
    }

    public static void allowAllSSL() {
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {

            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }

        });

        SSLContext context = null;
        if (trustManagers == null) {
            trustManagers = new TrustManager[]{new HttpsTrustManager()};
        }

        try {
            context = SSLContext.getInstance("TLS");
            context.init(null, trustManagers, new SecureRandom());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }

        HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
    }
}

之後我們就可以正常得到成功發送的訊息:

{
  "_index": "test",
  "_type": "info",
  "_id": "d3CMd3gB3gpQ5aELYqQa",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 4
}