Issues with letsencrypt certs from dehydrated: `cURL error 60: SSL certificate problem: unable to get local issuer certificate`

We are having issues setting up RoR for elasticsearch with nextcloud. I followed the docker related instructions provided here and went on with the encryption part here.

  1. I will give some details about my configs
  2. I will describe the problem
  3. Some open/remaining questions

1. Configs

I am getting my certs via dehydrated from letsencrypt, which gives me:

ls -lha /var/lib/dehydrated/certs/mydomain.com/

drwxr-xr-x 2 1000 root 4.0K Dec 5 15:55 .
drwx------ 3 root root 4.0K Nov 25 13:54 ..
-rwxr-xr-x 1 1000 root 554 Nov 25 12:37 cert-1764070647.csr
-rwxr-xr-x 1 1000 root 1.4K Nov 25 12:37 cert-1764070647.pem
lrwxrwxrwx 1 1000 root 19 Nov 25 12:37 cert.csr → cert-1764070647.csr
lrwxrwxrwx 1 1000 root 19 Nov 25 12:37 cert.pem → cert-1764070647.pem
-rwxr-xr-x 1 1000 root 1.6K Nov 25 12:37 chain-1764070647.pem
lrwxrwxrwx 1 1000 root 20 Nov 25 12:37 chain.pem → chain-1764070647.pem
-rwxr-xr-x 1 1000 root 2.9K Nov 25 12:37 fullchain-1764070647.pem
lrwxrwxrwx 1 1000 root 24 Nov 25 12:37 fullchain.pem → fullchain-1764070647.pem
-rw------- 1 1000 root 306 Dec 5 15:46 privkey-1764070647.pem
lrwxrwxrwx 1 1000 root 22 Nov 25 12:37 privkey.pem → privkey-1764070647.pem

I set the ownership by:

sudo chown -R 1000:0 /var/lib/dehydrated/certs/mydomain.com/

I will probably have to changes this later, but for know this is fine.

I mount the certs (and the other 2 configs) in docker-compose.yml, by:

docker-compose.yml
volumes:
  - type: bind
    source: /var/lib/dehydrated/certs/mydomain.com/
    target: /usr/share/elasticsearch/config/certs
  - type: bind
    source: config/readonlyrest.yml
    target: /usr/share/elasticsearch/config/readonlyrest.yml
  - type: bind
    source: config/elasticsearch.yml
    target: /usr/share/elasticsearch/config/elasticsearch.yml
config/readonlyrest.yml

readonlyrest:
access_control_rules:

  • name: “Require HTTP Basic Auth”
    type: allow
    auth_key: user:password

ssl:
enabled: true
server_certificate_file: certs/cert.pem
server_certificate_key_file: certs/privkey.pem

config/elasticsearch.yml

network.host: 0.0.0.0

Enable security features

xpack.security.enabled: false

Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents

#xpack.security.http.ssl:

enabled: false

keystore.path: cert

Enable encryption and mutual authentication between cluster nodes

#xpack.security.transport.ssl:

enabled: false

verification_mode: certificate

keystore.path: certs/transport.p12

truststore.path: certs/transport.p12

http.type: ssl_netty4

When pulling up the container, I get:

$ docker logs root-es-ror-1
"log.level":"ERROR", "message":"fatal exception while booting Elasticsearch", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.bootstrap.Elasticsearch","elasticsearch.node.name":"042c1555b319","elasticsearch.cluster.name":"elasticsearch","error.type":"tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles","error.message":"Unable to initialize Key Manager Factory using provided keystore.","error.stack_trace":"tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles: Unable to initialize Key Manager Factory using provided keystore.\n\tat tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles$.apply(SSLCertHelper.scala:367)\n\tat tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext$$anonfun$1(SSLCertHelper.scala:136)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\n\tat apply @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey$$anonfun$2(SSLCertHelper.scala:280)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\n\tat apply @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:273)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:273)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:273)\n\tat use @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:281)\n\tat flatMap @ tech.beshu.ror.utils.SSLCertHelper$.getPrivateKeyAndCertificateChainFromPemFiles(SSLCertHelper.scala:266)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareSslContextBuilder(SSLCertHelper.scala:346)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:112)\nCaused by: java.lang.IllegalArgumentException: unknown object in getInstance: org.bouncycastle.asn1.ASN1ObjectIdentifier\n\tat org.bouncycastle.asn1.ASN1Sequence.getInstance(Unknown Source)\n\tat org.bouncycastle.asn1.pkcs.PrivateKeyInfo.getInstance(Unknown Source)\n\tat tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey$$anonfun$2$$anonfun$1(SSLCertHelper.scala:277)\n\t... 9 more\n"}
es-ror-1  | ERROR: Elasticsearch did not exit normally - check the logs at /usr/share/elasticsearch/logs/elasticsearch.log
es-ror-1  | 
es-ror-1  | ERROR: Elasticsearch died while starting up, with exit code 1

Asking Google I came up with converting the privkey to pks8, with:

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in certselastic/privkey-1764070647.pem -out certselastic/privkey-pkcs8.pem

Because privkey.pem is a softlink, I converted the real file.

When I now set server_certificate_key_file: to the converted privkey-pks8.pem the container starts up as intended.

I configured the Fulltextsearch App in Nextcloud with: https:// user:password@mydomain .com:9200/

2. Problem

I try to do the first indexing, with sudo -u www-data php8.3 /var/www/nextcloud/occ fulltextsearch:index and get:

cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)

But when I try https://user:password@mydomain.com:9200/ in firefox, or chrome, the certificate gets accepted! They show the letsencrypt certificate and say it’s fine.

I tried the same with openssl s_client -connect ``mydomain.com:9200 and there I also get an Error:

$ openssl s_client -connect mydomain.com:9200
CONNECTED(00000003)
depth=0 CN = mydomain.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = mydomain.com
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 CN = mydomain.com
verify return:1

while it works with all our other domains. I found suggestions to point curl on the nextcloud server to /ets/openssl/ca-certificates.crt, which I did (in all possible php configs, although I now I am using php8.3):

/etc/php/8.3/cli/php.ini:curl.cainfo = /etc/ssl/certs/ca-certificates.crt
/etc/php/8.3/fpm/php.ini:curl.cainfo = /etc/ssl/certs/ca-certificates.crt
/etc/php/8.3/phpdbg/php.ini:curl.cainfo = /etc/ssl/certs/ca-certificates.crt
/etc/php/8.4/cli/php.ini:curl.cainfo = /etc/ssl/certs/ca-certificates.crt
/etc/php/8.4/fpm/php.ini:curl.cainfo = /etc/ssl/certs/ca-certificates.crt

Questions

  • What type of .pem files are supported here? Is the problem, that I converted the original privkey.pem and now the RoR plugin accepts it, but the clients have issues with accepting it?
  • Why can’t RoR just deal with the certs provided by dehydrated?

We would really like to avoid self-signed certificates, any help would be appreciated :slight_smile:

Hello @TU-AStA
Thanks for your message. We’ll look into this further and try to reproduce the problem on our side. We’ll get back to you with more details as soon as we have them.

1 Like

Hello @TU-AStA
Apologies for getting back to you later than expected.

I was able to reproduce your issue. You can find my reproducer here. The problem is that ReadonlyREST is receiving only the server certificate, without the intermediate certificates, which causes curl and openssl to fail with ‘unable to get local issuer certificate’. Browsers work because they automatically fetch missing intermediates, but clients like curl and PHP do not.

The fix should be straightforward: use fullchain.pem instead of cert.pem as the server_certificate_file in your readonlyrest.yml:

readonlyrest:
ssl:
enable: true
server_certificate_file: “fullchain.pem”
server_certificate_key_file: “privkey.pem”

Also, regarding the EC private key format produced by dehydrated (BEGIN EC PRIVATE KEY) — thanks for reporting this. We weren’t aware that this format wasn’t supported. I’ve prepared a fix which should be available in the next release.

Additionally, I am looking into whether we can detect an invalid server certificate file at ReadonlyREST startup and provide a clear error message to help prevent this kind of issue in the future.

Feel free to ask if you have any further questions.

this is fixed in ROR 1.70.0

Thanks for you work here! @Mateusz @coutoPL

So firs, I used the beshultd/elasticsearch-readonlyrest:8.14.3-ror-1.70.0 image, if thats correct? Just to avoid misunderstandings from my side, since docker is not my best friend :see_no_evil_monkey:

As mentioned above I still have issues with the certificate ownerships. Alhough this might be another issue (?) or even a deployment problem on my side, I thought I’d ask you about your thoughts:

When dehydrated pull new certificates, the permissions are set to:

ls -lha /var/lib/dehydrated/certs/mydomain.de/
drwxr-xr-x 2 1000 root 4.0K Jun  8 12:12 .drwx------ 3 root root 4.0K Nov 25  2025 ..-rwxr-xr-x 1 1000 root  554 Nov 25  2025 cert-1764070647.csr-rwxr-xr-x 1 1000 root 1.4K Nov 25  2025 cert-1764070647.pem-rw------- 1 root root  558 Jan 26 03:00 cert-1769392802.csr-rw------- 1 root root 1.4K Jan 26 03:00 cert-1769392802.pem-rw------- 1 root root  558 Mar 30 03:00 cert-1774832404.csr-rw------- 1 root root 1.4K Mar 30 03:00 cert-1774832404.pem-rw------- 1 root root  558 Jun  1 03:00 cert-1780275603.csr-rw------- 1 root root 1.4K Jun  1 03:00 cert-1780275603.pem-rw------- 1 root root  554 Jun  8 12:12 cert-1780913552.csr-rw------- 1 root root 1.4K Jun  8 12:12 cert-1780913552.pemlrwxrwxrwx 1 root root   19 Jun  8 12:12 cert.csr → cert-1780913552.csrlrwxrwxrwx 1 root root   19 Jun  8 12:12 cert.pem → cert-1780913552.pem-rwxr-xr-x 1 1000 root 1.6K Nov 25  2025 chain-1764070647.pem-rw------- 1 root root 1.6K Jan 26 03:00 chain-1769392802.pem-rw------- 1 root root 1.6K Mar 30 03:00 chain-1774832404.pem-rw------- 1 root root 3.5K Jun  1 03:00 chain-1780275603.pem-rw------- 1 root root 3.5K Jun  8 12:12 chain-1780913552.pemlrwxrwxrwx 1 root root   20 Jun  8 12:12 chain.pem → chain-1780913552.pem-rwxr-xr-x 1 1000 root 2.9K Nov 25  2025 fullchain-1764070647.pem-rw------- 1 root root 2.9K Jan 26 03:00 fullchain-1769392802.pem-rw------- 1 root root 2.9K Mar 30 03:00 fullchain-1774832404.pem-rw------- 1 root root 4.8K Jun  1 03:00 fullchain-1780275603.pem-rw------- 1 root root 4.8K Jun  8 12:12 fullchain-1780913552.pemlrwxrwxrwx 1 root root   24 Jun  8 12:12 fullchain.pem → fullchain-1780913552.pem-rw------- 1 1000 root  306 Dec  5  2025 privkey-1764070647.pem-rw------- 1 root root  359 Jan 26 03:00 privkey-1769392802.pem-rw------- 1 root root  359 Mar 30 03:00 privkey-1774832404.pem-rw------- 1 root root  359 Jun  1 03:00 privkey-1780275603.pem-rw------- 1 root root  359 Jun  8 12:12 privkey-1780913552.pemlrwxrwxrwx 1 root root   22 Jun  8 12:12 privkey.pem → privkey-1780913552.pem

Trying to start up the root-es-ror-1 container as root with the following docker-compose.yml:

docker-compose.yml
services:
  es-ror:
    image: beshultd/elasticsearch-readonlyrest:8.14.3-ror-latest
    user: "0:0"
    ports:
      - "9200:9200"
    environment:
      - I_UNDERSTAND_AND_ACCEPT_ES_PATCHING=yes
      - KIBANA_USER_PASS=kibana
      - ADMIN_USER_PASS=admin
      - discovery.type=single-node
        # - r.r.ssl_ca_file=/certs/chain.pem
    volumes:
      - /var/lib/dehydrated/certs/mydomain.de:/usr/share/elasticsearch/config/certs
      - type: bind
        source: /root/config/readonlyrest.yml
        target: /usr/share/elasticsearch/config/readonlyrest.yml
      - type: bind
        source: /root/config/elasticsearch.yml
        target: /usr/share/elasticsearch/config/elasticsearch.yml

The startup will fail with:

{"@timestamp":"2026-06-08T10:42:23.916Z", "log.level": "INFO", "message":"creating NettyAllocator with the following configs: [name=elasticsearch_configured, chunk_size=1mb, suggested_max_allocation_size=1mb, factors={es.unsafe.use_netty_default_chunk_and_page_size=false, g1gc_enabled=true, g1gc_region_size=4mb}]", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.transport.netty4.NettyAllocator","elasticsearch.node.name":"ea6cdb7519fa","elasticsearch.cluster.name":"elasticsearch"}
{"@timestamp":"2026-06-08T10:42:24.030Z", "log.level":"ERROR", "message":"fatal exception while booting Elasticsearch", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.bootstrap.Elasticsearch","elasticsearch.node.name":"ea6cdb7519fa","elasticsearch.cluster.name":"elasticsearch","error.type":"tech.beshu.ror.utils.SSLCertHelper$UnableToLoadDataFromProvidedFilesException","error.message":"Unable to load data from provided files","error.stack_trace":"tech.beshu.ror.utils.SSLCertHelper$UnableToLoadDataFromProvidedFilesException: Unable to load data from provided files\n\tat tech.beshu.ror.utils.SSLCertHelper$UnableToLoadDataFromProvidedFilesException$.apply(SSLCertHelper.scala:371)\n\tat tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext$$anonfun$1(SSLCertHelper.scala:131)\n\tat apply @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat use @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:287)\n\tat flatMap @ tech.beshu.ror.utils.SSLCertHelper$.getPrivateKeyAndCertificateChainFromPemFiles(SSLCertHelper.scala:263)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareSslContextBuilder(SSLCertHelper.scala:352)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\nCaused by: java.io.FileNotFoundException: /usr/share/elasticsearch/config/certs/privkey.pem (Permission denied)\n\tat java.base/java.io.FileInputStream.open0(Native Method)\n\tat java.base/java.io.FileInputStream.open(FileInputStream.java:213)\n\tat java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)\n\tat java.base/java.io.FileReader.<init>(FileReader.java:75)\n\tat tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey$$anonfun$1(SSLCertHelper.scala:270)\n\t... 7 more\n"}
ERROR: Elasticsearch did not exit normally - check the logs at /usr/share/elasticsearch/logs/elasticsearch.log

I tried verifying that the container is working as another user than root, but

~#  docker exec -it root-es-ror-1 id
uid=0(root) gid=0(root) groups=0(root)

I also tried this repeatedly until the startup fails, but until the end it’s always the above.

When I change permissions to:

chmod 640 privkey-1780913552.pem
chgrp 1000 privkey-1780913552.pem

The permission issue seems to be resolved, but than again I get

“Unable to initialize Key Manager Factory using provided keystore.”,“error.stack_trace” :

{"@timestamp":"2026-06-08T11:00:27.066Z", "log.level":"ERROR", "message":"fatal exception while booting Elasticsearch", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"main","log.logger":"org.elasticsearch.bootstrap.Elasticsearch","elasticsearch.node.name":"542b758247b2","elasticsearch.cluster.name":"elasticsearch","error.type":"tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles","error.message":"Unable to initialize Key Manager Factory using provided keystore.","error.stack_trace":"tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles: Unable to initialize Key Manager Factory using provided keystore.\n\tat tech.beshu.ror.utils.SSLCertHelper$UnableToInitializeSslContextBuilderUsingProvidedFiles$.apply(SSLCertHelper.scala:373)\n\tat tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext$$anonfun$1(SSLCertHelper.scala:133)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\n\tat apply @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey$$anonfun$2(SSLCertHelper.scala:286)\n\tat unsafeRunSync @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\n\tat apply @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat fromAutoCloseable @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:270)\n\tat use @ tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey(SSLCertHelper.scala:287)\n\tat flatMap @ tech.beshu.ror.utils.SSLCertHelper$.getPrivateKeyAndCertificateChainFromPemFiles(SSLCertHelper.scala:263)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareSslContextBuilder(SSLCertHelper.scala:352)\n\tat map @ tech.beshu.ror.utils.SSLCertHelper$.prepareServerSSLContext(SSLCertHelper.scala:109)\nCaused by: java.lang.IllegalArgumentException: unknown object in getInstance: org.bouncycastle.asn1.ASN1ObjectIdentifier\n\tat org.bouncycastle.asn1.ASN1Sequence.getInstance(Unknown Source)\n\tat org.bouncycastle.asn1.pkcs.PrivateKeyInfo.getInstance(Unknown Source)\n\tat tech.beshu.ror.utils.SSLCertHelper$.loadPrivateKey$$anonfun$2$$anonfun$1(SSLCertHelper.scala:283)\n\t... 9 more\n"}
ERROR: Elasticsearch did not exit normally - check the logs at /usr/share/elasticsearch/logs/elasticsearch.log

Questions:

  1. Is it correct, that I have to adjust permissions for the user 1000, although the container is ran as root itself?
  2. Did I use the right image to apply the latest fix of yours?
  3. Do you know what to do with the “Unable to initialize Key Manager Factory using provided keystore.” error?

Kind regards

Is it correct, that I have to adjust permissions for the user 1000, although the container is ran as root itself?

Yes, that’s correct. Even though the user shows as “0:0”, the ES entrypoint actually switches the JVM to UID 1000. As a result, the cert files must be readable by UID 1000. Note that docker exec id reflects the user of the spawned shell process, not the ES JVM.

Did I use the right image to apply the latest fix of yours?

I think the ror-latest tag on your machine might point to an outdated version. Tags like latest can sometimes hide older images if you haven’t pulled the latest one recently. You can check the build date with:
docker inspect beshultd/elasticsearch-readonlyrest:8.14.3-ror-latest --format ‘{{.Created}}’

The image should be dated around 2026-01-06T20:36:50Z. The recommended approach is to use an explicit version tag instead — 8.14.3-ror-1.70.0 — which should also fix the “Unable to initialize Key Manager Factory” error by adding native support for the private key format generated by dehydrated.

Thanks for reporting it - you discovered a permissions bug that I missed while testing on macOS.
The updated reproducer can be found here.