diff --git a/README.md b/README.md index 422f0ef..77de34f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ To stop and clean up: | [basic-multitenancy](examples/basic-multitenancy) | Multi-tenancy: isolated Kibana spaces and index access per user | | [kibana-reverse-proxy](examples/kibana-reverse-proxy) | Two Kibana nodes behind an Apache HTTPS reverse proxy with sticky-session load balancing, SSL termination, and a configurable base-path rewriting strategy | | [fleet](examples/fleet) | Full Elastic Fleet stack: Fleet Server, Elastic Agent with APM, and an instrumented Node.js service, all secured with ReadonlyREST | +| [pki-auth](examples/pki-auth) | Services authenticating with a TLS client certificate instead of a password, with the username and groups read from the certificate | ## Project structure diff --git a/examples/pki-auth/.env b/examples/pki-auth/.env new file mode 100644 index 0000000..fc72c92 --- /dev/null +++ b/examples/pki-auth/.env @@ -0,0 +1,25 @@ +# PKI authentication is not in a released ReadonlyREST yet, so the plugin ships with this repository +# rather than being downloaded. The zip lives in runner/plugins/ because the Docker build context is +# runner/ - ROR_ES_FILE is a COPY source and has to be relative to it. +# +# Rebuild it from the elasticsearch-readonlyrest-plugin repository with: +# ./gradlew clean buildRorPlugin '-PesVersion=9.5.0' +ROR_MIN_LICENSE_EDITION=FREE + +# 9.5.0 is the version this build was verified against by the PKI integration suites. +# Only the es94x module implements PKI so far; it covers ES 9.4.x and 9.5.0. +ES_VERSION=9.5.0 +ROR_ES_PLUGIN_SOURCE=LOCAL_FILE +ROR_ES_FILE=plugins/readonlyrest-1.71.0-pre7_es9.5.0.zip + +# Kibana is here so a human can log in with a password while the services authenticate by certificate +# on the same port. PKI itself is demonstrated with curl - certificates are for machine-to-machine +# traffic, and Kibana never presents one. +# +# NOTE: the Elasticsearch plugin is the local pre-release above while the Kibana plugin is downloaded, +# so the two ReadonlyREST versions differ. If Kibana cannot talk to Elasticsearch, align them - either +# point ROR_KBN_VERSION at a matching release or switch to LOCAL_FILE with ROR_KBN_FILE. +KBN_INSTANCES=1 +KBN_VERSION=9.5.0 +ROR_KBN_PLUGIN_SOURCE=API +ROR_KBN_VERSION=1.70.3 diff --git a/examples/pki-auth/README.md b/examples/pki-auth/README.md new file mode 100644 index 0000000..e3168ea --- /dev/null +++ b/examples/pki-auth/README.md @@ -0,0 +1,120 @@ +# PKI Auth Example + +Demonstrates authenticating services by their TLS client certificate: ReadonlyREST derives the username and groups from the certificate, while password-based users share the same port. + +## Users + +| Identity | Credential | Group | Kibana access | Access to `logs-*` | +|-----------------|--------------------------------------------------|----------|---------------|--------------------| +| `svc-logstash` | Certificate `CN=svc-logstash,OU=ingest,OU=Services` | `ingest` | None | Write | +| `svc-dashboard` | Certificate `CN=svc-dashboard,OU=query,OU=Services` | `query` | None | Read | +| `jsmith` | Certificate `CN=jsmith,OU=ingest,OU=People` | — | None | Refused | +| `analyst` | Password `analyst` | — | Read-only | Read | + +The three certificates come from the same CA. None of the services holds a password. + +`jsmith` is refused even though the node trusts that certificate and it carries the same `ingest` role, because the certificate is issued into the People branch and the PKI provider declares `subject_dn_base: "OU=Services,DC=corp,DC=example,DC=com"`. One corporate CA usually issues to more than one population, and without that constraint a `CN` extractor would authenticate humans as services. + +## How to run + +```bash +curl -sL https://raw.githubusercontent.com/beshu-tech/readonlyrest-examples/master/quickstart.sh | bash -s pki-auth +``` + +From a local clone it is just `./run.sh pki-auth`. + +Access points after startup: + +| Entry point | URL | +|---------------|--------------------------| +| Elasticsearch | https://localhost:19200 | +| Kibana | https://localhost:15601 | + +## What to explore + +Run these from the example directory. No credential is passed other than the certificate. + +- Write as `svc-logstash`, authenticated by certificate alone: + + ```bash + curl -sk --cert certs/svc-logstash.crt --key certs/svc-logstash.key \ + -XPOST https://localhost:19200/logs-2026/_doc \ + -H 'Content-Type: application/json' -d '{"msg":"hello"}' + ``` + +- Read with the same certificate — forbidden, because the `ingest` group only grants writes: + + ```bash + curl -sk --cert certs/svc-logstash.crt --key certs/svc-logstash.key https://localhost:19200/logs-2026/_search + ``` + +- Read as `svc-dashboard` — a different certificate, a different group, reads allowed: + + ```bash + curl -sk --cert certs/svc-dashboard.crt --key certs/svc-dashboard.key https://localhost:19200/logs-2026/_search + ``` + +- Try `jsmith` — trusted by the same CA, but refused for being outside `subject_dn_base`: + + ```bash + curl -sk --cert certs/jsmith.crt --key certs/jsmith.key https://localhost:19200/logs-2026/_search + ``` + +- Send no certificate at all — the request falls through to the password block on the very same port: + + ```bash + curl -sk -u analyst:analyst https://localhost:19200/logs-2026/_search + ``` + +- Watch a real client do the same thing. A Logstash container ships to `logs-2026` using the `svc-logstash` certificate and no password at all — its config holds no credential other than the certificate ([`confs/logstash.conf`](confs/logstash.conf)). It reports every event it sends: + + ```bash + docker logs -f $(docker ps -qf name=logstash) + ``` + +- Watch the data arrive, reading with a *different* certificate. Run this twice a few seconds apart — the count goes up: + + ```bash + curl -sk --cert certs/svc-dashboard.crt --key certs/svc-dashboard.key \ + 'https://localhost:19200/logs-2026/_count' + ``` + + That is the whole point in one line: `svc-logstash` wrote it and cannot read it back, `svc-dashboard` reads it and cannot write, and neither of them holds a password. + +- Log in to Kibana as `analyst:analyst`. A browser never presents a client certificate, so Kibana authenticates with a password on the same port the services use certificates on. + +## How it is configured + +The node asks for a certificate and verifies it ([`confs/elasticsearch.yml`](confs/elasticsearch.yml)): + +```yaml +xpack.security.http.ssl.client_authentication: optional +xpack.security.http.ssl.verification_mode: certificate +xpack.security.http.ssl.certificate_authorities: [ "ca.crt", "pki-ca.crt" ] +``` + +`optional` rather than `required`, so a caller without a certificate still reaches the ACL and can fall through to the password block. `required` would reject it during the handshake instead, and `analyst` would never get in. + +ReadonlyREST turns the certificate into a user ([`confs/readonlyrest.yml`](confs/readonlyrest.yml)): + +```yaml +pkis: + - name: corporate_pki + subject_dn_base: "OU=Services,DC=corp,DC=example,DC=com" + issuer_dn: "CN=Corp Issuing CA,DC=corp,DC=example,DC=com" + users: + user_id_attribute: "CN" + groups: + group_id_attribute: "OU" +``` + +The groups it reads are *external* groups, mapped to local ones in the `users` section. Every certificate here carries two OUs — `OU=ingest` names a role, `OU=Services` merely places it in the corporate tree — and only the role is mapped. The other is discarded. + +The certificates are generated by [`certs/generate.sh`](certs/generate.sh), which you can rerun. The distinguished names are part of the configuration: change them and `confs/readonlyrest.yml` has to change with them. + +## Things to check in your own cluster + +- **TLS must terminate at Elasticsearch.** If a load balancer, ingress or service mesh terminates it upstream, no certificate ever reaches the node and PKI rules never match. This is the most common reason PKI appears not to work. +- **Kibana cannot use PKI.** A browser presents no client certificate, so anything reaching Elasticsearch through Kibana authenticates as Kibana's own service account. Keep a password or SSO path for people. +- **Never set `verification_mode: none`.** The node would still ask for a certificate and then validate nothing, so anyone able to run a CA could issue one saying `CN=svc-logstash` and be authenticated as that service. ReadonlyREST cannot detect this, and `issuer_dn` is no defence: the issuer name is read off the certificate that was presented, so a self-signed one can simply claim `CN=Corp Issuing CA` as well. `issuer_dn` narrows which of several *trusted* CAs an identity may come from; it is not a substitute for validating the chain. +- **Order your blocks.** If a request carries both a certificate and an `Authorization` header, the first matching block decides the identity. Put password blocks for known service accounts above the PKI blocks. diff --git a/examples/pki-auth/certs/generate.sh b/examples/pki-auth/certs/generate.sh new file mode 100755 index 0000000..5ee0308 --- /dev/null +++ b/examples/pki-auth/certs/generate.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# Regenerates the CA and the client certificates this example authenticates with. +# +# The distinguished names are the whole point: ReadonlyREST reads the username out of CN and the groups +# out of OU, so these names and confs/readonlyrest.yml have to stay in step. +# +# CN=svc-logstash, OU=ingest, OU=Services -> user svc-logstash, external group 'ingest' +# CN=svc-dashboard,OU=query, OU=Services -> user svc-dashboard, external group 'query' +# CN=jsmith, OU=ingest, OU=People -> rejected: outside the provider's subject_dn_base +# +set -euo pipefail + +cd "$(dirname "$0")" + +DAYS=3650 +BASE_DN="/DC=com/DC=example/DC=corp" + +rm -f ./*.crt ./*.key ./*.csr ./*.srl + +echo "==> certificate authority" +openssl req -x509 -newkey rsa:2048 -nodes -days "$DAYS" \ + -keyout pki-ca.key -out pki-ca.crt \ + -subj "${BASE_DN}/CN=Corp Issuing CA" 2>/dev/null + +new_client() { + local name="$1" subject="$2" + openssl req -newkey rsa:2048 -nodes \ + -keyout "${name}.key" -out "${name}.csr" \ + -subj "${subject}" 2>/dev/null + openssl x509 -req -in "${name}.csr" -days "$DAYS" \ + -CA pki-ca.crt -CAkey pki-ca.key -CAcreateserial \ + -out "${name}.crt" 2>/dev/null + rm -f "${name}.csr" + echo " ${name}: $(openssl x509 -in "${name}.crt" -noout -subject | sed 's/^subject=//')" +} + +echo "==> client certificates" +# openssl appends the RDNs in the order given and a DN prints right to left, so OU=Services - written +# here *before* OU=ingest - is the one that ends up rightmost, next to the DC components. That suffix +# is what subject_dn_base matches against, so swapping the two OUs would silently break authentication. +new_client "svc-logstash" "${BASE_DN}/OU=Services/OU=ingest/CN=svc-logstash" +new_client "svc-dashboard" "${BASE_DN}/OU=Services/OU=query/CN=svc-dashboard" +new_client "jsmith" "${BASE_DN}/OU=People/OU=ingest/CN=jsmith" + +rm -f ./*.srl +echo +echo "==> done. Keep pki-ca.crt next to elasticsearch.yml so the node trusts these certificates." diff --git a/examples/pki-auth/certs/jsmith.crt b/examples/pki-auth/certs/jsmith.crt new file mode 100644 index 0000000..8778c87 --- /dev/null +++ b/examples/pki-auth/certs/jsmith.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUTCCAjkCCQDK6IB8UlwONjANBgkqhkiG9w0BAQsFADBeMRMwEQYKCZImiZPy +LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEUMBIGCgmSJomT8ixk +ARkWBGNvcnAxGDAWBgNVBAMMD0NvcnAgSXNzdWluZyBDQTAeFw0yNjA4MDYxNzM5 +MTVaFw0zNjA4MDMxNzM5MTVaMHcxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRQwEgYKCZImiZPyLGQBGRYEY29ycDEPMA0GA1UE +CwwGUGVvcGxlMQ8wDQYDVQQLDAZpbmdlc3QxDzANBgNVBAMMBmpzbWl0aDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxnAcCT8KpTHoRwUyHoD/5toWHo +hDXe3q/jLvKfmG7VmgorP3bhlqEeT/Pbr2g+rJ3sWDuMcXuAkp7XggwtPJWlUvsL +zG55Ugw2Mwgm8pEJwerL7qyYhtCA/Ce5cJNOE1L75lI2phN8BPFqnmGLBAHlStYL +4q4/dUUMgAxD9RVSpyJVgISKuxVR15BQsFh/KIZrc4PvdFUQSY9W4cid4HmY/yMb +rk9UBCMNm8aTvprrM7o6dUmuMr0e/aYrytrHVI2DRb82y4+e8EfZOT7Uu81Kx112 +2lt0Q/EkZO2hV5wsYyy5X3NqkxKdGDWT22/IpvqMRWnSFaUG/JHIdChVcUkCAwEA +ATANBgkqhkiG9w0BAQsFAAOCAQEAnX01YsjlM0lrZ2MkfUuQdUXfqFePVFTCWVSG +pCcU9500BXoCAdXJuOg8ZDhrve3ZJxy36wUH1XdLqjdLaVtSq0gwXs8zOqLQwbot +6FeRuP89ojMHGnYEiSPfzoyIxhkAxo361FsIhmC34co1bh30sbnyIODrfiIWZtUV +44kRZtQJWuZs5EmmYQ8c88BidEuNUeiSYuyH30bT7ErEjouxE/7yo75Vhjzkm9rP +Ixc/SOAHLO211yv3nNpY3ha9jy8XyiazKwnqDrb66faCrkm9V9EradEXcygVjcDm +oIPyKhru1oEk++f1vOwXezVaqLNqB62q4mPukqSPWib7mFof0A== +-----END CERTIFICATE----- diff --git a/examples/pki-auth/certs/jsmith.key b/examples/pki-auth/certs/jsmith.key new file mode 100644 index 0000000..5115c21 --- /dev/null +++ b/examples/pki-auth/certs/jsmith.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMZwHAk/CqUx6E +cFMh6A/+baFh6IQ13t6v4y7yn5hu1ZoKKz924ZahHk/z269oPqyd7Fg7jHF7gJKe +14IMLTyVpVL7C8xueVIMNjMIJvKRCcHqy+6smIbQgPwnuXCTThNS++ZSNqYTfATx +ap5hiwQB5UrWC+KuP3VFDIAMQ/UVUqciVYCEirsVUdeQULBYfyiGa3OD73RVEEmP +VuHIneB5mP8jG65PVAQjDZvGk76a6zO6OnVJrjK9Hv2mK8rax1SNg0W/NsuPnvBH +2Tk+1LvNSsdddtpbdEPxJGTtoVecLGMsuV9zapMSnRg1k9tvyKb6jEVp0hWlBvyR +yHQoVXFJAgMBAAECggEARsh5A953XhRQoh5fu4MoXrxKQaSKfDlEtnYe4OhBmkgg +Att9K8btKbhciZ8O/DOQBVQ+Lcjx14nrvFP29g6IR24r8UHhtnQO5Km0PGN4Zp+R +ZqTebyiWrwerynneMTS9XmRbGydPlbnB4HRCipbgeWOU6dDN2/efvZyZjyXNbmdF +ZCyjon/B5JjKKJE5OFvAU5XdJQlCTPPLiqm2VxV96JrY0cK7eehGiKa2BJ9BjxJJ +0xAxGtepu3GB7CrN8UPgqk1UXZ8PRpoJkTySf4tvf/oo0gjNX/UqNWLemS8x/GOk +TORlm7SSBOrs7AyezDJbgNRR8Hjv+fykHbV9DDsCHQKBgQDus1ojr67u5dtuwXfV +vqwiPsc7Kw5PAL+92wlTP/7R1vS8vBE9VjqemuQYQ9QUp0aDolPHgLrSJ6hsol6z +v9QzG/pJAQHlkQQVXwxK2Lm8o+n2t4p7bGudni4qnvU2BqE4OLmLq6nXao4ca5aH +mmZ2beVsyHS036Nj0+Dvchq2kwKBgQDbN1CNLRM+6RQsq5H2HhgYe2Rd8dIYL41O +O/zH0N8pXiy4YndV/sxk7HBSdiJw4ebNHOXgRiCuwM8+7w9tQuINsK5CnKqVgRDo +7Wa0FHBaJD/m+Bdat7upZbVVgLliHy/E8SBXh5zX1vkF7hc9SW49doLU6+pw23If +yECqyPnmMwKBgQCwdHc7EEFSKytmmbB7muNmwV/IVpDSSCx2LfibySAXgT8UUjaw +UBEvdDMP2PxrdCjFYHdscLYqatSv0ewOFs3IJnOECjGbwfLx1Xyhy8qqL8Xh81Z0 +3PZE400fhriggpAlnpFTNchtDUEWs1Xo0nPSMnU6UIktuHDxeAy9FW237wKBgBiR +hXydDagiVTmAbRZpXwf3ZFNHE+XUBoE7JeR7G1e2j0qG9przuJKjER+cz9VY7BYW +5AiTs3wCbfe+sTrsoQspvw4GIvQoh+2jFbyfAcyIfYT0TyHCr8yXcpBHof9GQYNQ +BvDoaQKjvDsW6RHcVfebfaRBYIAPiCXZuq2pYqCTAoGBAK248EhDBP8/n+CRPimh +ddHyvhLdv41hl9nmqfj6lkFxdY36BOyv8kxMV9bTEm1CULuoQzCcoRRTxm+n/v7v +wAT2QWsNDHg0cOdUp1JhU0pIVmFduNsNKkDOsceBmGDb55Rq1Zx2gijiuCzJbGgx +xD3vbwWa2ePI4hQT3pBWTKcn +-----END PRIVATE KEY----- diff --git a/examples/pki-auth/certs/pki-ca.crt b/examples/pki-auth/certs/pki-ca.crt new file mode 100644 index 0000000..96efff9 --- /dev/null +++ b/examples/pki-auth/certs/pki-ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDODCCAiACCQCW5hO8JQvyRzANBgkqhkiG9w0BAQsFADBeMRMwEQYKCZImiZPy +LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEUMBIGCgmSJomT8ixk +ARkWBGNvcnAxGDAWBgNVBAMMD0NvcnAgSXNzdWluZyBDQTAeFw0yNjA4MDYxNzM5 +MTVaFw0zNjA4MDMxNzM5MTVaMF4xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRQwEgYKCZImiZPyLGQBGRYEY29ycDEYMBYGA1UE +AwwPQ29ycCBJc3N1aW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAwhielJvlb/BVlseFdDHGtwNwzdEiCQxcSAtzkGsVr8sH0TikoIaCTJmABIu1 +Mc66IHhvPV56wmqCWRGNU6I2n2a3g3RkMfwPGMu12U5Du7x+MX81Zd0G4rmEylYX +DO39Jh71wBX+lDUjuXYpL+11vvayy0uwqletsyrjtwBj85HL3EpCzUyQ3MRBiYoq +1ux9TFqf0e7Pj4xdgXT2Z8jJ3t6SnTOOcQYnWS9te1ooCZZ0QnHG8oWs0dnqw8OF +I0d27oo94WFdtvEgKoqlDaRlTj3SWyc3dTRn5yc/JZmN5DR4z6J6WGu/Hm4fzJe5 +/1Za2HxnilKPMlUj3RUFlTkU+wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBmcoBC +bkvTUmFHIg1NzYx8ZsSu/T3ZZApfoaXFA2WISKbckhvDiW5dSX8haKOthb095yeA +aPfxKYmursaFF4ZRoGeeeIVHCWanaZF9U/OpcmcahFnTG6Bg0qw7KNxPNhEcOdRP +H91tJrbnm8S6TlYBH8RxpqoyXq+2dN3+OK++zzWoUldf7a77U9Jw7W6YFdcAq2g5 +6+RmO9tJtGOpyUTgJ2kGJ5SoLktReWU4cw3qtUgvVkvxTchVsURzvopSIfHXAHbB +JQmY+FozFQfD4O/Bo74QYYJwURFkbWXk6BHoR6orOwueDgBomT+U7iUInBFwnSxj +ZxdgaSLCZLrrmmw3 +-----END CERTIFICATE----- diff --git a/examples/pki-auth/certs/pki-ca.key b/examples/pki-auth/certs/pki-ca.key new file mode 100644 index 0000000..6154c67 --- /dev/null +++ b/examples/pki-auth/certs/pki-ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCGJ6Um+Vv8FWW +x4V0Mca3A3DN0SIJDFxIC3OQaxWvywfROKSghoJMmYAEi7UxzrogeG89XnrCaoJZ +EY1TojafZreDdGQx/A8Yy7XZTkO7vH4xfzVl3QbiuYTKVhcM7f0mHvXAFf6UNSO5 +dikv7XW+9rLLS7CqV62zKuO3AGPzkcvcSkLNTJDcxEGJiirW7H1MWp/R7s+PjF2B +dPZnyMne3pKdM45xBidZL217WigJlnRCccbyhazR2erDw4UjR3buij3hYV228SAq +iqUNpGVOPdJbJzd1NGfnJz8lmY3kNHjPonpYa78ebh/Ml7n/VlrYfGeKUo8yVSPd +FQWVORT7AgMBAAECggEAX0sXEH85tiuY2d47d0C4/0GBMIts2mRnKjf1FQJ+M0DL +Jb1ZljZz4oe6goDVBQ8p8qcudkLconcIaBJmAJmzl68mijOqvZ+zCcl6DqaOOq3g +hwydetV5e/b5ax25U2/EyeKJZTVnN/ye/X812YIMPSWBwq32nqtNbJmyEzbt27bt +NhOoKtz2GXlr+pN2e3fvDsMm4eDOqFh/qMiha3eOv2yKoPnKJFuVjANwTC6ziamb +p77VNvi5xUnpCvlV9keL6rpZhy2i7yaNB7IgSdaFrfl/jaSAG/BnhTgUbvhkOjbL +djOw/SmIIWoKGzd74JHp0RwYG0wuK7jV9ESiOyZ0AQKBgQD04nTqkAbVJqH+RmLV +cVIHlwuFfjTTsSBTixGsfkTAXrLGpsyrayMaNeTSVFQ1v2cW+I48nq7igUP5lYtB +Zs16Z/sbdd96ZkTkp9E3N67X1oj+w4L02Hu2ELnOsOyA1nXulZM4wf6nbAqXcb6U +So+5oiobikPP0tP7ID9dk/Wu2wKBgQDK6AEmWX39+8VKW/tHh5rin6aDTzW3DdkS +OgugD8L4IeEeePn0/Kz/VDr6P5md0szrWDC1TwScwdpxVSeUs7ZvtsGC5q03fGNy +hKP6qVmdN43D4K3z7/iy8EXFsMlp1aUI6f6mZvylCjVbwhNk6PyFsP3S4RbyW4Ci +9/HdePu8YQKBgDbovDyINPAAbJxeXfTsJu+Kv8ucA+5frhbtfPYHjhTwZvfCGOxq +5oEPCpLa09MFavEspIAVOLOTNpG1JXdxvKswu45pvMVuPw5iTrgp5SuCcE9nuWp7 +TCoD0BX3d+BftivcIBm+7gHOaQWSPKB1o4qFQRnRw+jKpjuN/IdYEOO3AoGBAJWS +1x4lBFqRGUuKaL6++O8sTzwmzOsHG0hzX5R7afcuSlwDRAdTqFeECQHmlmAgQA0Q +8r8E49qGkHfRFR9qJwdNCnNrhq8LU+fcXWDvK+9YP3nwi2ryMDwAIP7tJlyNMF5U +1/JkosMqtlDQzSkrEtOeuE2WviqkRzxbWIExqSihAoGBAMyY9o4Us6h2T14yjSyP +C8fhMK9aESfZgZ2CZql+uvIh1WEyUVVY2rok3TThSb5/PYqylgd3erIdRzUIs03+ +P3gyTaMN1AhpXOA3ekYV6IJZOm+1bhoesh/WgYnpaHjzg/Ui1JsfIRezFC5PJP1X +VfYNegQN9kL6CI4ABFIceD5Y +-----END PRIVATE KEY----- diff --git a/examples/pki-auth/certs/svc-dashboard.crt b/examples/pki-auth/certs/svc-dashboard.crt new file mode 100644 index 0000000..6b07148 --- /dev/null +++ b/examples/pki-auth/certs/svc-dashboard.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAkECCQDK6IB8UlwONTANBgkqhkiG9w0BAQsFADBeMRMwEQYKCZImiZPy +LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEUMBIGCgmSJomT8ixk +ARkWBGNvcnAxGDAWBgNVBAMMD0NvcnAgSXNzdWluZyBDQTAeFw0yNjA4MDYxNzM5 +MTVaFw0zNjA4MDMxNzM5MTVaMH8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRQwEgYKCZImiZPyLGQBGRYEY29ycDERMA8GA1UE +CwwIU2VydmljZXMxDjAMBgNVBAsMBXF1ZXJ5MRYwFAYDVQQDDA1zdmMtZGFzaGJv +YXJkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7jgmyq5qF0UbmCTI +dXa1rsaKrE7CuH4RRFRGaln0da0HQ9wsS7SeJgNP3Tpl1ODPQ6OR+6xiEzwptccd +GH6+0Ej5dCGezGCnD/yRnBHRqmZm8fWxPqq+jm1727YWZBgkwJcq7LRP8IrfpJgi +u9yt1I3AnvWHEs0zwuKXR/xJkWXgm3PCFud2vns9aU5y1qQiNTA4GfJc5twGb95p +ALmdsbSJUY80bf8F4YxhQFwY/4OVrtLUIGNYNp3Fh4n/vWZBBSh6is2QhPw/Evw6 +TplMy2S8EFIl/YZ2q+l1cF8bvmLo3hy216GynssR2KKpZBtmM0Zt+HpqKdmJKctM +7Qp48wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAW6LbZ9/shMROyXUeWereCE4iO +zW5+JzwoqlhNlheGfTmfmA8QTNk1TuxoX6sKe6eAEVuOXaw6aQ0USAT/TYHkUM0x +fwoqeVWKhkq0ziHIFPEcX7Gbf67lelvsQyz6js2mNA5XR79wSLeUGaHAupc9QqWa +qoibqwPKXx/yrEGBgS13T3pcQrouqAfe5/KlVTe6zrT4VdCvvch77BiXOAscCEB4 +v1oKiKSi2234GionjCfsCNlRRVoYzcdI5cqTCqociSHqMUncnHGVyGhviFzjOZ+a +Pao5Zh9oj0h6ggqec+6gDYBeZLsW65mlIIyLlhruAbosvewDAquGz0a4K+jB +-----END CERTIFICATE----- diff --git a/examples/pki-auth/certs/svc-dashboard.key b/examples/pki-auth/certs/svc-dashboard.key new file mode 100644 index 0000000..58c6021 --- /dev/null +++ b/examples/pki-auth/certs/svc-dashboard.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDuOCbKrmoXRRuY +JMh1drWuxoqsTsK4fhFEVEZqWfR1rQdD3CxLtJ4mA0/dOmXU4M9Do5H7rGITPCm1 +xx0Yfr7QSPl0IZ7MYKcP/JGcEdGqZmbx9bE+qr6ObXvbthZkGCTAlyrstE/wit+k +mCK73K3UjcCe9YcSzTPC4pdH/EmRZeCbc8IW53a+ez1pTnLWpCI1MDgZ8lzm3AZv +3mkAuZ2xtIlRjzRt/wXhjGFAXBj/g5Wu0tQgY1g2ncWHif+9ZkEFKHqKzZCE/D8S +/DpOmUzLZLwQUiX9hnar6XVwXxu+YujeHLbXobKeyxHYoqlkG2YzRm34emop2Ykp +y0ztCnjzAgMBAAECggEAfkwy9N5HzRKMsLs/tFc9t+33c6dGQX5FNU7hDl2m6ATW +hzniGjkmZ8z1uLrPZm/SM3AzY5VfYgdRrdhlKql4DQHDj7iLcpwDtswXfwFLgeZM +yheS40CvSt/s/O2rLWJHifwbe+eVQli+fYtTTvqPBtQovZ47ANLekIKF5lpxvq2Y +zMjisl2tz1rNaO2Nw3op3nA5C8gQvVVpIdtQfAAkx6ub0jCFLs5FzPonaS4TJAJ7 +/1yum9VDRZhxfzYonWFLMYAFybix1VSmaWy3Ek0RIPFUOd4KpHNfdGWa2scZoeuX +r8igz4tCg5or64oRARp8xtHe6hRVU1y3Sq2uUE0/kQKBgQD+4jipwZYH/lh0CY7m +IVAI2xiOhU/Ld2lJO655EvS3X4SWCcAiyz6AilCdvVsbVreFNgCQc7B6QYRd0R2g +YTup+we55gSokTayS/Q99OyCdyIvQMitB81+5x8cRo9Zb2WRxwXqTRKBWkddEyBm +GIme9mNbZ3tZRml+WCdd9uuuTQKBgQDvQz71psIsNkuguIICUc4wAr1aL4bJr2n3 +z/e/itwaNMW5zHwgmOXiuab59t963cnteevng9LGp31KAiMMxVle+taYDJuNrqxe +a0PO6cmHXvRemyZ5wH9tJ0ocVRNJ8P9coqWRJCHeiUAluKjZ5fFAIhBIdFsnDYll +sn8/3AbkPwKBgQC79IZkaGUCsAT6TLIb8iTa4vZQ4u3c1MnHP0OB9QCQ2Nck7TvK +bKZCk7yvFZvBpUjf6tqvqyBQh0/c/wAh7JHa30rrQzvcMnlrMaeCqMJf0wpaXiOd +tUtMcZL3fvDmusbjoSgzh3JGARTvBdO2dhHGL1tFZCIJy2qvyMH/AL77YQKBgQDf +0syHCSUELxa4l2InwVddWnLAd01kRxCenpJpQQF+EngVw9EqvV3wpzQpCmUtj2Vj +Hncs8QiwheaS4UTgoY1laMpvGvw5onnlKfsZCWNZm96G0iVAKHAMMIEH2B5fQW/h +vNWd3WtBvqufngt3K8Bv+m25GgBbnDI4TK71kmNwCQKBgQD4A9UxJ+eXd6gifQJY +OWUlwAoftyjNQGckb6AJvAoYrBcKLut4K1VaCj8PKp2dRmNo1R+8X9iwWcXa+nul +9buL29O+fvC+NVublD/47R2kSb5cQyFGDAkYcyVM2rPFbrugusn4FFUbvTRwBCnF +28Krv3jm3Nkag8Q4K+9lbvQZbQ== +-----END PRIVATE KEY----- diff --git a/examples/pki-auth/certs/svc-logstash.crt b/examples/pki-auth/certs/svc-logstash.crt new file mode 100644 index 0000000..65ff326 --- /dev/null +++ b/examples/pki-auth/certs/svc-logstash.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAkECCQDK6IB8UlwONDANBgkqhkiG9w0BAQsFADBeMRMwEQYKCZImiZPy +LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEUMBIGCgmSJomT8ixk +ARkWBGNvcnAxGDAWBgNVBAMMD0NvcnAgSXNzdWluZyBDQTAeFw0yNjA4MDYxNzM5 +MTVaFw0zNjA4MDMxNzM5MTVaMH8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRQwEgYKCZImiZPyLGQBGRYEY29ycDERMA8GA1UE +CwwIU2VydmljZXMxDzANBgNVBAsMBmluZ2VzdDEVMBMGA1UEAwwMc3ZjLWxvZ3N0 +YXNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvg5dzq7Y62pTDcxb +kV3rEPDGtzO1RRui2Nb6kg7qedW8dy6ilbWfHzrc3/DFZJWKpJxBMc3UMUTHRyej +LiSohAaofzezP4L2Ak3vvQMHa/fb+yO5JTdqlEi/nrdJVFZzCPTS4KX2QrkTIvj0 +NL2YU69D/19kzoxujuzk1DWbZ7IX4MZRbFpk+PyIEmWzpy12ZsIniKpdcri8NiAu +H//9dTlS1w804L5APbODGANMlf7MC+CXk64XBx8GguxWyToVhufu/wbztt3MRP/E +dPNUGnaN5RAVGEsAnLp9EcyO+0LFuDCyuHLILwbQcYDdBiAW4UpCcgMkZxm7ineH +EgLnlwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAr9CC3cRrhV2FOAV3s7RXcEka+ +TPXCHF49euiVAyEC0btYqETagvlaOLktTflOUHHedVxnjyT8bIDCWEq29KIkE1H8 +uEgErmcMf1ahkwJWwhleXmx6lu2Mm71tC5lDLr20PtnTXSszIK+AfeLxkIxqAjN+ +21Y9Nhl8B1gq4yhve38DA1iTdAjSCOoRlsywArxX37IYQ/ZfBVInU6zF9P1Q7/8q +LR3CI5Yoiy9mlATGTCMCZD0TsNdLPMY/QvDjkPgSE3ENutoeBOjXD4JVA04o4YwF +gNd15ILkhoqRW08pJ+oGrWmVoBLsBylQpAvn9no2m6sB6oluD5rHGCJFzMCp +-----END CERTIFICATE----- diff --git a/examples/pki-auth/certs/svc-logstash.key b/examples/pki-auth/certs/svc-logstash.key new file mode 100644 index 0000000..0fb48a5 --- /dev/null +++ b/examples/pki-auth/certs/svc-logstash.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+Dl3OrtjralMN +zFuRXesQ8Ma3M7VFG6LY1vqSDup51bx3LqKVtZ8fOtzf8MVklYqknEExzdQxRMdH +J6MuJKiEBqh/N7M/gvYCTe+9Awdr99v7I7klN2qUSL+et0lUVnMI9NLgpfZCuRMi ++PQ0vZhTr0P/X2TOjG6O7OTUNZtnshfgxlFsWmT4/IgSZbOnLXZmwieIql1yuLw2 +IC4f//11OVLXDzTgvkA9s4MYA0yV/swL4JeTrhcHHwaC7FbJOhWG5+7/BvO23cxE +/8R081Qado3lEBUYSwCcun0RzI77QsW4MLK4csgvBtBxgN0GIBbhSkJyAyRnGbuK +d4cSAueXAgMBAAECggEACk27FjZyeijUgd3/ZN0jnlJ/Y3VBMZLy/DCDeyGl4p8M +zaVQmS80hjvpt5ZVDUPyNURnYGzZQSaKazgh56GDJU07G1KIvDu8XJmfEq80Ws0g +NvhT1Emx1IIxKUikiBb7u5OSTy2QMUPZXr8Cdk2FEZxh4llonK4OXF9JdJvHZhKw +l6KJChjdevxiFbNSPK/W4DRUYddo8TkIrqOj4bAQpAGhBtxjUhc/6tEBTbuEgj4l +dkC68VTUOnfcgrk3eYNIjo7o9atiM9e0AR8EpvVD8/dlgKiW5umdeWH8pHkJdKeJ +NDtF4Efl5e5wVB7G1hBXMwwtN4xER7HrB8TUHMImGQKBgQD3Y8VjRlLUQR2tSGVt +L17gEJaAiJ2Os7KPsAnTfnVTuy48JBhkBY8RHXH5qh+2Cc6MdyIuGjWboJk7TdWK +VopeuqPjBTmYy4TVS5AgFRwX9Vtbbjx+mmvbxK2w85pu4GCo3p5cPw7gVx2tNHDp +25Kik48q0fk7Hakfob7JXVUIrQKBgQDEq8GMVCZKMLYReGjBPmBOtsb0J8P6lIPp +QD70JlZB945dWLmz7c38UyFSLzeC0pugdkCGAKf+8hSaGRSfb2OxxAStCrWoyn+q +5nwflatcn6QiP6DSsNjY+TuOn+mAmsaZX0PwDHbXpN+uHTU0Kx3BTI0IOdWdM/RE +dfQMWBLl0wKBgQDQqQF7G3yPdKDAYHjUIAlo+fTHUvN5wo2Qbk3LajUxu4ZiyZsc ++idSq0BP8bMvipQBnBZjRk4DBvFXuO2s8hNhlRDYUbbj7n0KnuJgfQ2mE5fLxW4W +5s6knf5WaxHOBUjiBmsRqUo15KSuS7YVOdMzdzxhRJtvxjO472cJVsR7PQKBgD2N +P7sKjx1DF4hqNebc/clE3QmG9IgVOR+kMs+4u+BTeSPv49bTQ1eIMPKGwonYHkrf +F5yY0fsxio8b8F9hvkLhhVEe+/HCxqgXEJvV/Na1Q+pQHaYzIPrvLduaYrsUNbnE +nf+f9wXQnQziKUeVgu9/ZNhBOYDExNs0S1gdrHRvAoGALz8e3KS4K8plXMhQlI5t +kC4xK3qZQpnPk3DqgL4KtOOEzKMFWOya2m8p/Dqkzy1MaRPDOTs0VBwe0A5Qsx/w +HYbCClTSsgADfvTG6uuaeih/VKc5UulubAeZH4YsVzQklyJJV6cQ7XAV2Ga3hhFs +F4pl7BJQmI94hO9GrBFo42c= +-----END PRIVATE KEY----- diff --git a/examples/pki-auth/confs/elasticsearch.yml b/examples/pki-auth/confs/elasticsearch.yml new file mode 100644 index 0000000..08d7a24 --- /dev/null +++ b/examples/pki-auth/confs/elasticsearch.yml @@ -0,0 +1,31 @@ +network.host: 0.0.0.0 + +path.repo: /tmp/repositories + +cluster.max_shards_per_node: 10000 + +# TLS terminated by X-Pack, which is the recommended arrangement for new deployments and the one +# ReadonlyREST's PKI support targets first. +xpack.security.enabled: true + +xpack.security.http.ssl.enabled: true +xpack.security.http.ssl.key: elasticsearch.key +xpack.security.http.ssl.certificate: elasticsearch.crt + +# Both CAs: the runner's, which issued this node's own certificate, and the one that issued the client +# certificates in ./certs. Without the second, the node will not trust the services. +xpack.security.http.ssl.certificate_authorities: [ "ca.crt", "pki-ca.crt" ] + +# 'optional', not 'required': a caller without a certificate still has to reach the ACL so it can fall +# through to the password block. 'required' would reject it during the handshake instead. +xpack.security.http.ssl.client_authentication: optional + +# ⚠️ 'certificate', never 'none'. With 'none' the node asks for a certificate and validates nothing, so +# anyone able to run a CA could issue one saying CN=svc-logstash and be authenticated as that service. +xpack.security.http.ssl.verification_mode: certificate + +xpack.security.transport.ssl.enabled: true +xpack.security.transport.ssl.key: elasticsearch.key +xpack.security.transport.ssl.certificate: elasticsearch.crt +xpack.security.transport.ssl.certificate_authorities: ca.crt +xpack.security.transport.ssl.verification_mode: certificate diff --git a/examples/pki-auth/confs/kibana.yml b/examples/pki-auth/confs/kibana.yml new file mode 100644 index 0000000..f5fb4c2 --- /dev/null +++ b/examples/pki-auth/confs/kibana.yml @@ -0,0 +1,18 @@ +server.name: ${SERVER_NAME} +server.host: 0.0.0.0 + +elasticsearch.username: kibana +elasticsearch.password: kibana +elasticsearch.ssl.verificationMode: none + +# generated with: +# $ openssl req -x509 -batch -nodes -days 3650 -newkey rsa:2048 -keyout kibana.key -out kibana.crt +server.ssl.enabled: true +server.ssl.certificate: /usr/share/kibana/config/kibana.crt +server.ssl.key: /usr/share/kibana/config/kibana.key +server.ssl.redirectHttpFromPort: 80 + +xpack.encryptedSavedObjects.encryptionKey: "min-32-byte-long-strong-encryption-key" + +readonlyrest_kbn.logLevel: info +readonlyrest_kbn.cookiePass: '12312313123213123213123abcdefghijklm' diff --git a/examples/pki-auth/confs/logstash.conf b/examples/pki-auth/confs/logstash.conf new file mode 100644 index 0000000..213f751 --- /dev/null +++ b/examples/pki-auth/confs/logstash.conf @@ -0,0 +1,45 @@ +# Logstash shipping to Elasticsearch with a client certificate and nothing else. +# +# There is no user, no password and no API key anywhere in this file. ReadonlyREST works out who this +# is from the certificate presented during the TLS handshake: CN=svc-logstash gives the username, and +# OU=ingest gives the group that grants writes on logs-*. + +input { + # One event every 5 seconds - slow enough not to flood, fast enough that a document count visibly + # moves between two curl calls. + heartbeat { + interval => 5 + message => "shipped by svc-logstash, authenticated by certificate alone" + } +} + +filter { + mutate { + add_field => { "service" => "svc-logstash" } + remove_field => [ "@version", "event", "host" ] + } +} + +output { + elasticsearch { + hosts => [ "https://es-ror:9200" ] + index => "logs-2026" + + # The credential. That is the whole configuration. + ssl_enabled => true + ssl_certificate => "/certs/svc-logstash.crt" + ssl_key => "/certs/svc-logstash.key" + ssl_certificate_authorities => [ "/certs/ca.crt" ] + + # ReadonlyREST grants this certificate writes on logs-* and nothing more. Index template and ILM + # management are cluster-level operations, so turn them off rather than widening the ACL - which is + # what you would want for a locked-down ingest account in production too. + manage_template => false + ilm_enabled => false + } + + # Report what was shipped, so `docker logs` shows the certificate did the work. + stdout { + codec => rubydebug + } +} diff --git a/examples/pki-auth/confs/readonlyrest.yml b/examples/pki-auth/confs/readonlyrest.yml new file mode 100644 index 0000000..b14f215 --- /dev/null +++ b/examples/pki-auth/confs/readonlyrest.yml @@ -0,0 +1,66 @@ +readonlyrest: + + access_control_rules: + + # The node health check comes in as this user. + - name: "ADMIN" + type: allow + auth_key: admin:admin + verbosity: error + + # Kibana's own service account, and the cluster initializer that seeds logs-2026 at startup. Both + # authenticate with a password: Kibana is a browser front end and never presents a client + # certificate, which is exactly why mixed mode matters. + - name: "KIBANA" + type: allow + auth_key: kibana:kibana + verbosity: error + + # Services whose certificate carries the 'ingest' role. No password anywhere. + # 'cluster:monitor/main' is what a client library uses to detect the Elasticsearch version when it + # connects - Logstash cannot start without it. Writes and nothing else beyond that: no + # 'indices:admin/*', so this certificate cannot delete or reconfigure the index it ships to. + - name: "Ingest services" + groups: ["ingest"] + actions: ["cluster:monitor/main", "indices:data/write/*"] + indices: ["logs-*"] + + # Services whose certificate carries the 'query' role. Read only. + - name: "Query services" + groups: ["query"] + actions: ["indices:data/read/*", "indices:admin/get", "indices:admin/mappings/get"] + indices: ["logs-*"] + + # A human on the very same port, in a browser rather than a script. Callers with no certificate fall + # through to here, which is what makes mixed mode work. + - name: "Analysts" + auth_key: analyst:analyst + indices: ["logs-*"] + kibana: + access: ro + + users: + - username: "*" + groups: + # Only the role OUs are mapped. Every certificate also carries OU=Services, which places it in + # the corporate tree rather than naming a role - it matches no mapping and is simply discarded. + - local_group: { id: "ingest", name: "ingest" } + external_group_ids: ["ingest"] + - local_group: { id: "query", name: "query" } + external_group_ids: ["query"] + pki_auth: + name: "corporate_pki" + groups_any_of: ["ingest", "query"] + + pkis: + - name: corporate_pki + # The same CA also issues to the People branch; this keeps those certificates out, so a human + # certificate cannot authenticate as a service even though the node trusts it. + subject_dn_base: "OU=Services,DC=corp,DC=example,DC=com" + # Any CA the node trusts could mint a certificate saying CN=svc-logstash. Pinning the issuer is + # what stops one of them impersonating a service issued by another. + issuer_dn: "CN=Corp Issuing CA,DC=corp,DC=example,DC=com" + users: + user_id_attribute: "CN" + groups: + group_id_attribute: "OU" diff --git a/examples/pki-auth/docker-compose.override.yml b/examples/pki-auth/docker-compose.override.yml new file mode 100644 index 0000000..8c7833e --- /dev/null +++ b/examples/pki-auth/docker-compose.override.yml @@ -0,0 +1,34 @@ +services: + + # The CA that issued the client certificates has to sit next to elasticsearch.yml, so the node can + # verify the chains the services present. + es-ror: + volumes: + - ${EXAMPLE_DIR}/certs/pki-ca.crt:/usr/share/elasticsearch/config/pki-ca.crt:ro + + # A real client authenticating with nothing but its certificate. Everything curl demonstrates by hand, + # this does on its own - and its logs show it working. + logstash: + image: docker.elastic.co/logstash/logstash:${ES_VERSION} + hostname: logstash + depends_on: + es-ror: + condition: service_healthy + # The 'ingest' group is not allowed to create indices, only to write into logs-*, so wait for the + # initializer to have created logs-2026 rather than relying on Logstash losing the race. + initializer: + condition: service_healthy + environment: + - XPACK_MONITORING_ENABLED=false + - LS_JAVA_OPTS=-Xms256m -Xmx256m + volumes: + - ${EXAMPLE_DIR}/confs/logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro + - ${EXAMPLE_DIR}/certs/svc-logstash.crt:/certs/svc-logstash.crt:ro + - ${EXAMPLE_DIR}/certs/svc-logstash.key:/certs/svc-logstash.key:ro + # The CA that signed the Elasticsearch server certificate, so Logstash can verify the node it + # connects to - the reverse direction to the client certificate above. That CA belongs to the + # runner rather than to this example, and this path is deliberately relative to the Compose + # project directory (runner/, where docker-compose.yml lives), not to this file. + - ./conf/es/ca.crt:/certs/ca.crt:ro + networks: + - ror-network diff --git a/examples/pki-auth/scripts/init.sh b/examples/pki-auth/scripts/init.sh new file mode 100755 index 0000000..8e0bcc4 --- /dev/null +++ b/examples/pki-auth/scripts/init.sh @@ -0,0 +1,7 @@ +#!/bin/bash -ex + +set -o pipefail + +source /usr/local/lib/ror-utils.sh + +createIndex "logs-2026" && generate_log_documents 5 | putDocument "logs-2026" diff --git a/examples/pki-auth/scripts/post-start.sh b/examples/pki-auth/scripts/post-start.sh new file mode 100755 index 0000000..62d294b --- /dev/null +++ b/examples/pki-auth/scripts/post-start.sh @@ -0,0 +1,37 @@ +CERTS="$(cd "$(dirname "${BASH_SOURCE[0]}")/../certs" && pwd)" +ES="https://localhost:19200" + +echo -e "" +echo -e "Kibana (password login, no certificate involved): https://localhost:15601 -> analyst:analyst" +echo -e "" +echo -e "Three identities that hold no password at all:" +echo -e "" +echo -e " svc-logstash CN=svc-logstash,OU=ingest,OU=Services,... -> group 'ingest', may write logs-*" +echo -e " svc-dashboard CN=svc-dashboard,OU=query,OU=Services,... -> group 'query', may read logs-*" +echo -e " jsmith CN=jsmith,OU=ingest,OU=People,... -> refused: outside subject_dn_base" +echo -e "" +echo -e "Try them (see README.md for what each one proves):" +echo -e "" +echo -e " # authenticated by certificate alone, and allowed to write" +echo -e " curl -sk --cert $CERTS/svc-logstash.crt --key $CERTS/svc-logstash.key \\" +echo -e " -XPOST $ES/logs-2026/_doc -H 'Content-Type: application/json' -d '{\"msg\":\"hello\"}'" +echo -e "" +echo -e " # same certificate, forbidden to read - the group only grants writes" +echo -e " curl -sk --cert $CERTS/svc-logstash.crt --key $CERTS/svc-logstash.key $ES/logs-2026/_search" +echo -e "" +echo -e " # a different certificate, a different group, reads allowed" +echo -e " curl -sk --cert $CERTS/svc-dashboard.crt --key $CERTS/svc-dashboard.key $ES/logs-2026/_search" +echo -e "" +echo -e " # trusted by the same CA, but issued into the People branch - refused" +echo -e " curl -sk --cert $CERTS/jsmith.crt --key $CERTS/jsmith.key $ES/logs-2026/_search" +echo -e "" +echo -e " # no certificate at all: falls through to the password block on the very same port" +echo -e " curl -sk -u analyst:analyst $ES/logs-2026/_search" +echo -e "" +echo -e "A Logstash container is already shipping to logs-2026 with the svc-logstash certificate and no" +echo -e "password. Watch what it sends, and watch a different certificate read it back:" +echo -e "" +echo -e " docker logs -f \$(docker ps -qf name=logstash)" +echo -e "" +echo -e " curl -sk --cert $CERTS/svc-dashboard.crt --key $CERTS/svc-dashboard.key $ES/logs-2026/_count" +echo -e "" diff --git a/runner/plugins/readonlyrest-1.71.0-pre7_es9.5.0.zip b/runner/plugins/readonlyrest-1.71.0-pre7_es9.5.0.zip new file mode 100644 index 0000000..3397e0f Binary files /dev/null and b/runner/plugins/readonlyrest-1.71.0-pre7_es9.5.0.zip differ