Pada latihan-k8s, saya menangani authentication di aplikasi Spring Boot dengan menggunakan Spring Security. Saya kemudian membuat service baru, sebuah aplikasi Python, di artikel sebelumnya. Salah satu masalah keamanan pada service baru tersebut adalah endpoint-nya tidak melakukan validasi JWT sehingga bisa diakses oleh siapa saja. Saya bisa saja menggunakan library seperty PyJWT untuk menambahkan validasi JWT. Namun, daripada setiap kali membuat service baru harus menangani JWT, akan lebih elegan bila validasi JWT dapat langsung dilakukan dari Ingress Controller. Kelebihannya adalah saya dapat melakukan konfigurasi di satu tempat yang sama tanpa harus mengubah kode program aplikasi (misalnya untuk mematikan authentication, cukup mengubah Ingress controller).

Di artikel “Memakai Ingress Controller”, saya sudah menggunakan ingress-nginx sebagai Ingress Controller. Ia sangat mudah dipakai di minikube karena tersedia sebagai addon. Namun sayangnya, ingress-nginx masih belum mendukung validasi JWT saat artikel ini ditulis. Sebagai perbandingan, salah satu pesaing NGINX, Envoy sudah mendukung validasi JWT. Terdapat lumayan banyak Ingress Controller berbasis Envoy seperti Istio, Ambassador dan sebagainya. Namun, pada artikel ini, saya akan memakai Kong Ingress Controller yang juga mendukung validasi JWT. Kong Ingress Controller pada dasarnya akan memakai Kong API Gateway yang berbasis NGINX.

Apa beda Ingress Controller dan API Gateway? Bila dilihat dari perannya, mereka sangat berbeda dan tidak dapat dibandingkan secara langsung. Ingress adalah sebuah fasilitas untuk mendeklarasikan gateway dalam bentuk resource yang dikelola oleh Kubernetes. Salah satu tujuan utamanya adalah pengguna tidak perlu melakukan perubahan file konfigurasi secara langsung pada NGINX atau Envoy yang dipakai oleh Ingress Controller. Sebagai contoh, saya dapat mengaktifkan HTTPS cukup dengan menambahkan tls.hosts dan tls.secretName di resource Ingress. Bila melakukan perubahan ini secara langsung di NGINX, saya perlu meng-edit file nginx.conf dan menambahkan pengaturan seperti ssl_certificate, ssl_certificate_key, dan sebagainya. Bukan hanya itu, bila beralih ke Envoy, struktur fike konfigurasinya tentu berbeda lagi.

Kubernetes sendiri tidak mengatur apa yang harus Ingress Controller lakukan selain melakukan routing! Implementasi Ingress Controller juga bebas asalkan ia dapat mengerjakan apa yang tertera di Ingress. Pada umumnya, implementasi Ingress Controller berupa reverse proxy dan load-balancer seperti pada ingress-nginx dan Istio Kubernetes Ingress. Ada juga implementasi Ingress Controller yang menawarkan fitur API Gateway seperti Kong Ingress Controller, Ambassador Ingress Controller, dan sebagainya. Selain itu, ada juga implementasi Ingress Controller yang mendukung service mesh seperti Kuma dan Service Mesh Hub.

Istilah API gateway sendiri lebih merupakan istilah pemasaran, bukan sebuah spesifikasi baku yang diatur dan diawasi oleh badan tertentu (bandingkan dengan spesifikasi jaringan yang dikelola di RFC IETF atau spesifikasi HTML5 yang selalu sama di browser manapun). Dengan demikian, setiap produk API Gateway bisa menawarkan fasilitas yang bervariasi. Secara umum, API gateway menawarkan fitur seperti routing, rate limiting, validasi JWT, dokumentasi OpenAPI, dan sebagainya. ingress-nginx juga mendukung rate limiting dengan annotation seperti nginx.ingress.kubernetes.io/limit-rate, namun karena lebih bekerja untuk mendukung jaringan (bukan condong untuk mendukung aplikasi), ia tidak disebut API Gateway.

Untuk melakukan instalasi Kong Ingress Controller, saya dapat memberikan perintah berikut ini:

$ kubectl apply -f https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/main/deploy/single/all-in-one-dbless.yaml

Langkah berikutnya, saya perlu mengubah ingressClassName di setiap manifest yang sebelumnya memiliki nginx menjadi kong seperti:

...
spec:
  ingressClassName: kong
...

Nilai metadata.annotations yang diawali oleh nginx.ingress.kubernetes.io yang saya berikan untuk pengaturan CORS kini sudah tidak valid lagi karena tidak akan dimengerti oleh Kong Ingress Controller. Sebagai alternatif-nya, Kong menggunakan CDR KongPlugin yang mewakili plugin Kong API Gateway. Agar bisa mendapatkan fasilitas CORS kembali, saya akan memakai plugin CORS. Untuk itu, saya membuat sebuah file manifest baru dengan nama kong/cors-plugin.yaml yang isinya terlihat seperti berikut ini:

apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: app-cors-plugin
config:
  origins:
    - 'https://web.latihan.jocki.me' # kpt-set: https://web.${domain}
plugin: cors

Setelah itu, saya akan mengganti annotation nginx.ingress.kubernetes.io pada setiap Ingress yang perlu mendukung CORS menjadi seperti berikut ini:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-api
  annotations:
    konghq.com/plugins: kong-cors-plugin
...

Nilai kong-cors-plugin pada annotation konghq.com/plugins akan meng-asosiasi-kan plugin CORS dengan Ingress tersebut. Setelah mengaplikasikan file yang berubah dengan kubectl apply -f, saya bisa memastikan resource KongPlugin sudah dibuat dengan memberikan perintah:

$ kubectl get kongplugins

Sekarang saatnya mengaktifkan dukungan validasi JWT. Kong juga menyediakan fitur ini dalam bentuk plugin, JWT. Saya segera membuat file baru dengan nama kong/jwt-plugin.yaml dengan isi seperti berikut ini:

apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: app-jwt-plugin
plugin: jwt
config:
  secret_is_base64: false

Spring Security mendukung discovery dari OpenID Connect (OIDC) dimana ia akan mendapatkan informasi yang dibutuhkan dengan mengakses URL seperti https://auth.latihan.jocki.me/auth/realms/latihan/.well-known/openid-configuration. Dengan demikian, saya hampir tidak perlu menyediakan informasi seperti algoritma signing dan isi public key sama sekali. Sayangnya, plugin JWT dari Kong tidak mendukung OIDC, sehingga saya perlu mengisi informasi yang dibutuhkan secara manual. Sebagai contoh, saya membuat sebuah Secret baru dengan perintah seperti berikut ini:

$ kubectl create secret generic app-jwt-secret \
--from-literal=kongCredType=jwt \
--from-literal=key="https://auth.latihan.jocki.me/auth/realms/latihan" \
--from-literal=algorithm=RS256 \
--from-literal=rsa_public_key="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjEFj5A1c0N8j+wiY/sqJ1Lj4x/VXQ88DWI89WBMkFoat0e5TNg+YL1rYOdD3BsWV+REq4SzUxaY5w7zppvMF9c1ZmPyNk2T6mSPPeA8M5OPkGj4IfftNqCr13TQGUUT5O7CFkQoYsWhy1mB1WtefQbIiZQf6gWbqtv2o214Pxm3S8NQbHoamsoGBcTor1yqgBdrrhAxfl+aDmc2i/HwSvFzFSHTo+4HHnWcJgqVlmmxEiofoq4vFtxXOoWMuT/Oq+/ez24clRBDPfg5HRzX0ApsD4fT3G2IKXXennpW9mtE72b4ENpIf6Q+GS/RvYSVhE86kHZxIKAiD4h9AbI8sjQIDAQAB
-----END PUBLIC KEY-----"

Untuk mendapatkan nilai rsa_public_key, saya bisa membuka URL untuk realm yang dipakai di https://auth.latihan.jocki.me/auth/realms/latihan dan men-copy nilai public_key dari JSON yang ditampilkan di halaman tersebut. Saya perlu memastikan untuk menambahkan -----BEGIN PUBLIC KEY----- dan -----END PUBLIC KEY----- karena plugin JWT dari Kong akan menolak public key tanpa baris tersebut.

Berikutnya, saya membuat sebuah KongConsumer dengan nama web-consumer.yaml yang isinya seperti berikut ini:

apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
  name: web
  annotations:
    kubernetes.io/ingress.class: kong
username: web
credentials:
  - app-jwt-secret

Nilai username pada file diatas tidak begitu berpengaruh untuk saat ini. Yang penting adalah saya melakukan referensi ke Secret yang sebelumnya saya buat di credentials dan menambahkan annotation kubernetes.io/ingress.class: kong karena ingin menangani validasi JWT melalui Ingress.

Sebagai langkah terakhir, saya perlu menambahkan plugin app-jwt-plugin yang saya deklarasikan ke file manifest Ingress. Sebagai contoh, pada file ingress-api.yaml, saya menambahkan annotation konghq.com/plugins seperti berikut ini:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-api
  annotations:
    konghq.com/plugins: 'app-cors-plugin, app-jwt-plugin'
    konghq.com/strip-path: 'true'
...

Konfigurasi di atas akan menyebabkan seluruh akses ke path di ingress-api harus menyertakan JWT yang valid. Bila tidak ada JWT yang valid, pemanggil akan mendapatkan pesan kesalahan {"message": "Unauthorized"}. Pesan ini dikembalikan oleh Kong Ingress Controller, bukan oleh aplikasi, sehingga Elasticsearch yang tidak aktif fitur authentication-nya pun ikut terlindungi.

Saya tetap bisa membuat endpoint yang boleh diakses tanpa validasi JWT. Sebagai contoh, bila ingin file yang di-upload dapat dipakai secara bebas (asalkan tahu link-nya), saya cukup tidak menyertakan app-jwt-plugin pada nilai konghq.com/plugins. Sebagai contoh, saya mengubah file ingress-file-read.yaml sehingga terlihat seperti pada contoh berikut ini:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-files-read
  annotations:
    konghq.com/plugins: app-cors-plugin
    konghq.com/strip-path: 'true'  
spec:
  ingressClassName: kong
  tls:
    - hosts:
        - files.latihan.jocki.me # kpt-set: files.${domain}
      secretName: tls-secret
  rules:
    - host: files.latihan.jocki.me # kpt-set: files.${domain}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: file-upload-service
                port:
                  number: 8080

Karena tidak ada app-jwt-plugin di file di atas, path pada Ingress tersebut dapat di-akses secara bebas.

Sementara itu, untuk upload file, karena ingin hanya user yang sudah login saja yang boleh meng-upload file, saya bisa membuat file ingress-file-write.yaml dengan isi seperti:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-files-write
  annotations:
    konghq.com/plugins: 'app-cors-plugin, app-jwt-plugin'  
spec:
  ingressClassName: kong
  tls:
    - hosts:
        - files.latihan.jocki.me # kpt-set: files.${domain}
      secretName: tls-secret
  rules:
    - host: files.latihan.jocki.me # kpt-set: files.${domain}
      http:
        paths:
          - path: /.*/upload
            pathType: Prefix
            backend:
              service:
                name: file-upload-service
                port:
                  number: 8080

Setelah mengaplikasikan seluruh file manifest yang saya ubah di atas, kini validasi JWT akan ditangani oleh Ingress Controller. Saya boleh menghapus Spring Security dari aplikasi Spring Boot karena ia tidak dibutuhkan lagi. Bila JWT tidak valid atau user belum login, request tidak akan pernah sampai di aplikasi.

Kelebihan lainnya adalah saat bekerja di setiap service dan menjalankan service di komputer lokal secara individual, saya tidak perlu mengkhawatirkan masalah authentication lagi. Saya bisa memanggil endpoint selama development secara langsung tanpa perlu melewatkan JWT (selama aplikasi dijalankan di luar Kubernetes atau diakses tanpa melalui API Gateway). Bila ingin mematikan validasi JWT secara global, juga bukanlah hal yang sulit karena bisa dilakukan dengan memodifikasi manifest Ingress dengan menghapus plugin app-jwt-plugin tanpa melakukan perubahan kode program di sisi aplikasi sama sekali.

Bila validasi JWT tidak bekerja sesuai dengan yang diharapkan, saya dapat memberikan perintah seperti berikut ini untuk melihat log pesan kesalahan dari Kong Ingress Controller:

$ kubectl logs $(kubectl get pods -o name -l app=ingress-kong) -c ingress-controller

Pod untuk Kong Ingress Controller terdiri atas dua container, ingress-controller dan proxy. Saya dapat menggunakan -c untuk memilih log dari container mana yang hendak ditampilkan.