Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[THREESCALE-11019] - FAPI advance profile #1466

Merged
merged 2 commits into from
Jul 23, 2024

Conversation

tkan145
Copy link
Contributor

@tkan145 tkan145 commented Jun 7, 2024

What

This PR support https://issues.redhat.com/browse/THREESCALE-11019.

Verification steps:

  • Build new runtime-image
make runtime-image IMAGE_NAME=apicast-test
  • Move into keycloak dev-environment
cd dev-environments/keycloak-env

Prepare certificates

mkdir certs && cd certs

Generate CA certificate

openssl genrsa -out rootCA.key 2048
openssl req -batch -new -x509 -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem
  • Generate a private key for the HTTPS keystore. Provide changeit as keystore password
keytool -genkeypair -keyalg RSA -keysize 2048 -dname "CN=keycloak" -alias jboss -keystore keystore.jks -storepass changeit -keypass changeit

Generate a certificate signing request (CSR) for the HTTPS keystore.

keytool -certreq -keyalg rsa -alias jboss -keystore keystore.jks -file sso.csr -storepass changeit
  1. Sign the CSR with the CA certificate..
openssl x509 -req -extfile  <(printf "subjectAltName=DNS:keycloak") -CA rootCA.pem -CAkey rootCA.key -in sso.csr -out sso.crt -days 365 -CAcreateserial
  1. Import the CA certificate into the HTTPS keystore. Reply yes to Trust this certificate? [no]: question
keytool -import -file rootCA.pem -alias rootCA.ca -keystore keystore.jks -storepass changeit -noprompt
  1. Import the signed CSR into the HTTPS keystore.
keytool -import -file sso.crt -alias jboss -keystore keystore.jks -storepass changeit

We can verify the certificates are imported with below command.

keytool -v -list -storepass changeit \  
  -alias jboss -keystore keystore.jks
  1. Import CA certificate into the truststore
keytool -import -file rootCA.pem -alias rootCA.ca -keystore truststore.jks -storepass changeit -noprompt

Generate client certificate

Generate APIcast certificates

$ openssl req -subj '/CN=apicast'  -newkey rsa:4096 -nodes \
      -sha256 \
      -days 3650 \
      -keyout apicast.key \
      -out apicast.csr
$ chmod +r apicast.key
$ openssl x509 -req -in apicast.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out apicast.crt -days 500 -sha256
$ cat apicast.key apicast.crt >apicast.pem

Repeat the step above and generate the client certificate

$ openssl req -subj '/CN=client'  -newkey rsa:4096 -nodes \
      -sha256 \
      -days 3650 \
      -keyout client.key \
      -out client.csr
$ chmod +r client.key
$ openssl x509 -req -in client.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out client.crt -days 500 -sha256
$ cat client.key client.crt >client.pem

Once the certificate are generated, we are ready to deploy Keycloak

Deploy

  • Update apicast-config.json
diff --git a/dev-environments/keycloak-env/apicast-config.json b/dev-environments/keycloak-env/apicast-config.json
index 071296cd..bab21204 100644
--- a/dev-environments/keycloak-env/apicast-config.json
+++ b/dev-environments/keycloak-env/apicast-config.json
@@ -61,7 +61,7 @@
         "api_test_path": "/",
         "api_test_success": null,
         "apicast_configuration_driven": true,
-        "oidc_issuer_endpoint": "http://oidc-issuer-for-3scale:oidc-issuer-for-3scale-secret@keycloak:8080/realms/basic",
+        "oidc_issuer_endpoint": "https://oidc-issuer-for-3scale:oidc-issuer-for-3scale-secret@keycloak:8443/realms/basic",
         "lock_version": 4,
         "authentication_method": "oidc",
         "oidc_issuer_type": "keycloak",
@@ -84,10 +84,10 @@
         },
         "policy_chain": [
           {
-            "name": "token_introspection",
+            "name": "fapi",
             "version": "builtin",
             "configuration": {
-              "auth_type": "use_3scale_oidc_issuer_endpoint"
+              "validate_oauth2_certificate_bound_access_token": true
             }
           },
           {
  • Update docker-compose.yaml with the following
diff --git a/dev-environments/keycloak-env/docker-compose.yml b/dev-environments/keycloak-env/docker-compose.yml
index 3fdbd011..a0904272 100644
--- a/dev-environments/keycloak-env/docker-compose.yml
+++ b/dev-environments/keycloak-env/docker-compose.yml
@@ -8,6 +8,9 @@ services:
     - two.upstream
     - keycloak
     environment:
+      APICAST_HTTPS_PORT: 8443
+      APICAST_HTTPS_CERTIFICATE: /var/run/secrets/apicast/apicast.crt
+      APICAST_HTTPS_CERTIFICATE_KEY: /var/run/secrets/apicast/apicast.key
       THREESCALE_CONFIG_FILE: /tmp/config.json
       THREESCALE_DEPLOYMENT_ENV: staging
       APICAST_CONFIGURATION_LOADER: lazy
@@ -22,6 +25,7 @@ services:
       - "8090:8090"
+      - "8443:8443" 
     volumes:
       - ./apicast-config.json:/tmp/config.json
+      - ./certs:/var/run/secrets/apicast
   example.com:
     image: alpine/socat:1.7.4.4
     container_name: example.com
@@ -39,9 +43,20 @@ services:
     command: "start-dev"
     expose:
       - "8080"
+      - "8443"
     ports:
       - "9090:8080"
+      - "9443:8443"
+    volumes:
+      - ./certs/keystore.jks:/etc/x509/https/keystore.jks
+      - ./certs/truststore.jks:/etc/x509/https/truststore.jks
     restart: unless-stopped
     environment:
       KEYCLOAK_ADMIN: admin
       KEYCLOAK_ADMIN_PASSWORD: adminpass
+      KC_HTTPS_CLIENT_AUTH: request
+      KC_HTTPS_KEY_STORE_FILE: /etc/x509/https/keystore.jks
+      KC_HTTPS_KEY_STORE_PASSWORD: changeit
+      KC_HTTPS_TRUST_STORE_FILE: /etc/x509/https/truststore.jks
+      KC_HTTPS_TRUST_STORE_PASSWORD: changeit
(END)
  • Start APIcast
make gateway IMAGE_NAME=apicast-test
  • Bootstrap keycloak
make keycloak-data
  • We should be able to access keycloak UI via https://localhost:9443
  • Login as admin user admin:adminpass
  • Switch to basic realm
  • Create new client Clients -> Create client
Client type: OpenID Connect
Client ID: mtls_client_demo
  • Click Next
Client authentication: On
Authorization: On
  • Click Next -> then Save
  • Once the new client is created, go to Credentials tab and select X509 Certificate as Client Authenticator
  • Next put in your Subject DN, in this test we use (.*?)(?:$). Then click Save
  • Go to Advanced tab, and then scroll down to Advance settings and enable OAuth 2.0 Mutual TLS Certificate Bound Access Token -> click Save

Request token

Now we have everything set up, we can use curl to authenticate with the client certificate to get the access token.

ACCESS_TOKEN=$(docker compose -p keycloak-env exec gateway curl -k -v -H "Content-Type: application/x-www-form-urlencoded" \
   -d 'grant_type=client_credentials' \
   -d 'client_id=mtls_client_demo' \
   --cert /var/run/secrets/apicast/client.crt \
   --key /var/run/secrets/apicast/client.key \
   --cacert /var/run/secrets/apicast/rootCA.pem \
   "https://keycloak:8443/realms/basic/protocol/openid-connect/token" | jq -r '.access_token')
  • Validate that token has cnf claim.
    If we decode this token using jwt.io, we can see the payload has the cnf claim and its value is certificate sha256 thumbprint. The resource server (APIcast) will use this thumbprint to validate if the same user is making the request. For example:
"cnf": {
    "x5t#S256": "3hhTJwX93ZWWyuuKOzm1k4qo-MH0dfhDC7jgg8ZyR6U"
  },

Now we are ready to test

  • Send request
curl -v --resolve stg.example.com:8080:127.0.0.1 -H "Authorization: Bearer ${ACCESS_TOKEN}" "http://stg.example.com:8080"

Request should failed with {"error": "invalid_token"}

  • Send request again with client certificates
curl -v -k --resolve stg.example.com:8080:127.0.0.1  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
   --cert certs/client.crt \
   --key certs/client.key \
  "http://stg.example.com:8443"

Request should return 200

< HTTP/2 200
< server: openresty
< date: Fri, 14 Jun 2024 06:50:11 GMT
< content-type: application/json
< content-length: 1756
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-fapi-transaction-id: 83688fb6-0ca9-44d3-9e9f-3b31bcdefd8a
<

@tkan145 tkan145 requested a review from a team as a code owner June 7, 2024 12:21
@tkan145 tkan145 force-pushed the THREESCALE-11019-fapi-advance-profile branch 2 times, most recently from 4ae0eef to ce1e153 Compare June 14, 2024 03:39
@tkan145 tkan145 changed the title WIP [THREESCALE 11019] - FAPI advance profile [THREESCALE 11019] - FAPI advance profile Jun 14, 2024
@tkan145 tkan145 requested a review from eguzki June 20, 2024 01:33
Copy link
Member

@eguzki eguzki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job 🎖️

Before approval, let me do some questions to clarify:

  • APIcast does not validate the client (downstream) certificate as regular validation, it only does validation specified in OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. Am I right? If so, shouldn't APIcast validate the certificate as well (it would require cacerts that signed the client cert)?
  • In the verification steps, there is an unknown "keycloak_mtls.json". Maybe leftovers of past tries?
  • In the verification steps, make gateway command is missing. (which somewhat important :) )
  • The verification steps tell that the created client should have enabled the "OAuth 2.0 Mutual TLS Certificate Bound Access Token". Is this really necessary? Maybe it is the setting that adds the cnf claim to the JWT?
  • There is no user generated in keycloak. That means that there is no need for users in keycloak and the client cert is enough as user ID? If so, any user with a certificate signed with the same cacert defined for keycloak can get the token?
  • The verification steps miss the client configuration "allow regexp pattern"
  • I wonder if this "OAuth 2.0 Mutual TLS Certificate Bound Access Token" use case deserves its own dev env. Mainly because if I want to reproduce this, I would need to come back to this PR and re-do the verification steps. If not dev-env, maybe developer oriented readme?? what do you think?
  • No need to get the APICast IP, just need to update the docker-compose.yml and add 8443 port to the gateway service:
     ports:
       - "8080:8080"
       - "8090:8090"
+      - "8443:8443"

@tkan145
Copy link
Contributor Author

tkan145 commented Jul 22, 2024

APIcast does not validate the client (downstream) certificate as regular validation, it only does validation specified in OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens. Am I right? If so, shouldn't APIcast validate the certificate as well (it would require cacerts that signed the client cert)?

According to the spec, we only need to verify the thumbprint. And I think user can use TLS Client Certificate Validation if they wish to validate the certificate.

In the verification steps, there is an unknown "keycloak_mtls.json". Maybe leftovers of past tries?
Good catch! I tried to automate dev-env and provide necessary information to make testing easier. I will submit another PR to update dev-env once this one is merged.

In the verification steps, make gateway command is missing. (which somewhat important :) )
Hehe fixed.

The verification steps tell that the created client should have enabled the "OAuth 2.0 Mutual TLS Certificate Bound Access Token". Is this really necessary? Maybe it is the setting that adds the cnf claim to the JWT?

Yes it is the settings that will calculate the certificate hash and adds cnf claim to the JWT.

There is no user generated in keycloak. That means that there is no need for users in keycloak and the client cert is enough as user ID? If so, any user with a certificate signed with the same cacert defined for keycloak can get the token?

I believe so, FAPI spec also pointed it out as below:

The use of [MTLS](https://tools.ietf.org/html/rfc8705) for client authentication and sender constraining
access tokens brings significant security benefits over the use of shared secrets. However in some 
deployments the certificates used for [MTLS](https://tools.ietf.org/html/rfc8705) are issued by a Certificate
Authority at an organization level rather than a client level. In such situations it may be common for an
organization with multiple clients to use the same certificates (or certificates with the same DN) across
clients. Implementers should be aware that such sharing means that a compromise of any one client, would
result in a compromise of all clients sharing the same key.

The verification steps miss the client configuration "allow regexp pattern"

Fixed

I wonder if this "OAuth 2.0 Mutual TLS Certificate Bound Access Token" use case deserves its own dev env. Mainly because if I want to reproduce this, I would need to come back to this PR and re-do the verification steps. If not dev-env, maybe developer oriented readme?? what do you think?

Agree

No need to get the APICast IP, just need to update the docker-compose.yml and add 8443 port to the gateway service:

👍

@tkan145 tkan145 force-pushed the THREESCALE-11019-fapi-advance-profile branch from 20dce47 to 97401f1 Compare July 23, 2024 01:51
@tkan145 tkan145 changed the title [THREESCALE 11019] - FAPI advance profile [THREESCALE-11019] - FAPI advance profile Jul 23, 2024
@tkan145 tkan145 merged commit 34a4b0a into 3scale:master Jul 23, 2024
16 checks passed
@tkan145 tkan145 deleted the THREESCALE-11019-fapi-advance-profile branch July 23, 2024 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants