Monday, November 9, 2026

Spark authorization using KeyCloak

Spark is great but how do you stop users seeing data they should not if you are building your own on-prem solution?

KeyCloak

KeyCloak is an Identity and Access Management (IAM) solution. It implements both OAuth2 (authorization - that is, what you can do) and OIDC (authentication - who is doing it). OIDC extends OAuth2 by, for example, adding standardized endpoints.

Because of this, a lot of the terminology in these standards applies to KeyCloak. But, in addition, KeyCloak has some of its own - Realms for example. (Incidentally, KeyCloak can serve its own login page for your protected application. This would live under /realms/YOUR_REALM)

Clients are machines and processes, users are humans. Clients have secrets, users have passwords. They are totally orthoganol concepts. A client and a user that have the same name are not related.

The grant_type describes the flow - that is, what's happening. This could be a user logging in via MFA (authorization_code issued by the IdP when the browser redirects to it) or back end servers talking to each other using service accounts (client_credentials) or an interactive session (password).

One such flow is Direct Grant Flow, where an app fronts the login page that is sent to KeyCloak that then mints the token. It is heavily discouraged as it circumvents MFA.

When Spark is configured to use the OIDC token (that is, with spark.sql.catalog.<catalog_name>.token as Spark configuration), it sends it directly to Polaris with the HTTP header Authorization: Bearer <token>. Polaris then validates this with KeyCloak rather than using a grant_type.

Claims are properties of a token. There are some reserved ones but you can define your own.

Spark

So, when Spark is configured to read an Iceberg table using Polaris, it differs to Iceberg code's OAuth2Manager. It's this that makes the call to Polaris with the relevant grant_type.

Saturday, October 24, 2026

Git cheatsheet

Differences between PR branch and the main Polaris branch:

git fetch --all
git diff upstream/main...origin/$FEATURE_BRANCH --name-only

Note the elipses. 

This is not the same as simply:

git diff upstream/main origin/$FEATURE_BRANCH --name-only

Rebase and merge do the same thing in different ways. Whereas merge splices two branches together, rebase rewrites the history to make it look as if there was no branch at all. This might mean that if you merge main onto your feature branch and then push it to the same feature branch, it may be reject even if you're the only person committing to that branch. This is because your local changes have different hashes to the remote. It's easily resolved with:

git push --force-with-lease origin $FEATURE_BRANCH

To rebase to main, the path should be:

git fetch origin
git switch $FEATURE_BRANCH
git rebase origin/main
git switch main
git pull --ff-only origin main
git merge --ff-only $FEATURE_BRANCH
git push origin main

Note that merge is a misnomer. It will act like a rebase.

The --ff is a fast-forward and will just move the branch pointer if there are no new commits to the branch to which your pulling. If there are, you'll need to merge like normal. On the merge, if there are changes, it will create a new commit.

Miscellaneous Security

Examine your JVM's certificates of your Certificate Authorities with the tool provided by the JDK:

keytool --list -keystore /usr/lib/jvm/jdk-25.0.2+10/lib/security/cacerts

This can tell you if your credentials are broken:

$ openssl s_client -showcerts -connect YOUR_DOMAIN:443
CONNECTED(00000003)
depth=2 C = US, O = Amazon, CN = Amazon Root CA 1
...
Certificate chain
 0 s:CN = emryspolaris.click
   i:C = US, O = Amazon, CN = Amazon RSA 2048 M01
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Nov 21 00:00:00 2025 GMT; NotAfter: Dec 20 23:59:59 2026 GMT
-----BEGIN CERTIFICATE-----
...

OWASP (Open World Application Security Project) is non-profit open source and docs.

SPIFFE and SPIRE

RFC 8693 defines how one OAuth token can be exchanged for another, typically to downscope its rights before performing a more granular action.

While KeyCloak operates at the human level, SPIFFE (workload identity standards) and SPIRE (its runtime) operate lower. 

SPIFFE rests on three concepts
  1. SPIFFE ID - a URL that acts like 'username' for the container or machine.
  2. SPIFFE Verifiable Identity Document (SVID) - a short lived certificate
  3. An API that can give a SVID with no further protocol.
SPIRE specifies a central Server for identity and Agents running on nodes. Both the node and the process must go through a process of attestation.

IP addresses

Note that big corporations often use a CDN so when you ping them, you're probably hitting servers owned by, for example, Fastly. To see the true addresses, run somthing like:

getent hosts www.bbc.co.uk

Server Name Indication 

SNI is a field in the TLS handshake carrying the hostname the client wants to reach.

It exists because of a chicken-and-egg problem. One IP address can serve thousands of sites, each with its own certificate. The server has to choose which certificate to present, but that choice happens before there's an encrypted channel to ask over. So the client puts the hostname in the very first packet, the ClientHello, in plaintext.

The client writes its own ClientHello. Nothing stops it sending SNI good-bucket.storage.googleapis.com while the encrypted Host header says bad-bucket.storage.googleapis.com — and the server routes on Host, which the policy can't see. That's domain fronting, and it's the one hole SNI filtering can't close by itself. 

"Domain Fronting: This is the process of using multiple different domain names in the various layers of a web connection, which helps an attacker evade detection from monitoring while they quietly connect to a target." - Aligning Security Operations with the MITRE ATT&CK Framework

Zero Trust Architectures

NetBird is an open source Go tool that instead of going through a front door, transparently talks to services directly. It sits on the open source C tool, WireGuard and uses an IdP to identify the user.

Pomerium is another open source Go app that sits in front of, say, a web app and allows access based on, say, the device that's trying to connect. Other policies can be implemented like geolocation.

Key Ceremony

This is a physical event between humans where keys are exchanged under tight security.

Friday, September 18, 2026

Configuring Polaris

There's a bit of a mismatch between Spark and Polaris terminology as illustrated below:

SparkPolaris
spark.sql.catalog.[CATALOG_NAME].warehousecatalog
namespaceno equivalent
no equivalentnamespace

Polaris CLI

The main areas in Polaris (at least as far as the CLI is concerned) are:

A catalog has a catalog-role.
A principle has a principle-role.
The two roles are associated.
The catalog/catalog-role pairing has a number of CatalogPrivileges associated with it and these dictate what can be done (table creation, read, write etc)

Clouds

Azure

Create an App and assign it to the resource group your container ("bucket") lives in. 

Give this App's login details to Polaris as environment variables and the AppName to the Polaris catalog.

GCP

Create a service account and grant yourself the permission to use it to create tokens. Create an Application Default Credentials (ADC) JSON file for your user but this allows the app to mint access tokens as the service account.

Give Polaris the path to this ADC file as an environment variable.

(Note, I had to up the version of my Polaris to avoid this bug).

AWS

Create a role whose role policy allows users to assume it. Attach the policy allowing access to S3 to that role. 

Give your own AWS credentials to Polaris as environment variables.

Tuesday, July 7, 2026

KeyCloak Security

KeyCloak/OAuth

The nature of OAuth in web usage includes a frontend client like a web browser and a backend client which is the application (for example, Apache Polaris). 

Realms are isolated namespaces of security. They can be federated but are not by default.

The JWT (JSON Web Token) is associated with a realm that is passed to the backend client. It contains an ISSued field that indicates who issued it. The KeyCloak issuing it signs it with its private key.

The ISS is a URL that the embeds both the realm and the domain of the ISS. The backend client can use this URL to find a public key of KeyCloak with which it can verify the provenance of the JWT. 

This endpoint is called the Realm JWKS (JSON Web Key Set).

You can see the contents of the token with a simple Linux line like this:

echo $POLARIS_TOKEN | cut -d. -f2 | tr '_-' '/+' | base64 -d | jq

If a claim in the JWT is azp (authorized party), it identifies to whom the token was issued - typically the front end app. If it's audience, it identifies for whom the token was issued - typically the backend.

So, an example would be that the user logs in and their web browser is the azp. They ask KeyCloak for a token on behalf of the back end, the aud. KeyCloak signs both so the backend knows the token is genuinely for it and who requested it. If the azp is not in its whitelist, the backend can reject it. 

KeyCloak Configuration

To relax this constraint (say, for integration tests), Polaris can set the environment variable quarkus.oidc.token.issuer=any.

Interestingly, OAuth2 servers have a handy endpoint at .well-known/openid-configuration listing salient details. So, for KeyCloak, run:

curl -s https://YOUR_DOMAIN/realms/YOUR_REALM/.well-known/openid-configuration | jq

If your backend is in the same private network as KeyCloak, you can set KC_HOSTNAME_STRICT_BACKCHANNEL=false to make KeyCloak dynamically generate backchannel URLs based on the request's Host header - the mandatory HTTP header that defines which virtual host is the endpoint.

Note that an attacker could use Host Header Injection to trick KeyCloak into certifying a malicious domain. To stop this, the load balancer must be correctly configured to ignore inappropriate Host values plus KeyCloak should have KC_HOSTNAME_STRICT=true and KC_HOSTNAME set so even if the hacker gets past the load balancer, KeyCloak says "nice try but I know what public facing name I have". Again, you might like to relax this for testing.

Talking of the load balancer, traffic between it and KeyCloak is unencrypted so we need to tell KeyCloak communication is plain HTTP after TLS termination with KC_HTTP_ENABLED=true and set a KC_PROXY_HEADERS strategy to tell it what the load balancer is doing with the traffic. We use xforwarded so it knows our AWS ALB injects headers in a standard way.

Sunday, June 28, 2026

LLMs in Java - part 1

Interpretability of LLMs is important in many real world situations. For instance, there is an EU law that says if a bank's model refuses a loan then the client has the right to know why. But neural nets are notoriously hard to interpret.

There are a few techniques:

Activation Patching: this is basically A/B testing a neural net where the activations from a clean prompt are transplanted to that of a corrupted prompt and we check if that fixes the corruption.

Logit lens: take the residual stream vector (that is, the vector that represents the working memory) and apply it to the vocabulary logits at each layer. This way, we can see where an output word (probably) starts to form.

Sparse Auto Encoders: the auto encoder is trained on the model's input and output, and deliberately made sparse for low values so prominent features become clear.

The neural net itself

Most of the Java code that iterates over the tokens lives in InferenceEngine.generateTokensGPULlama. In turn, this delegates to the Tornado graphs that are executed in TornadoVMMasterPlan.tornadoVMForwardExecuteLayered. Note that the positionHolder is an IntArray that has a single element.

However, the real heavy lifting is done in LlamaFP16FFNLayers.setupSingleFFNLayer and all the functions in TransformerComputeKernelsLayered it uses. Note, that it doesn't call them. Instead, it builds them up into a graph of lazily evaluated functions that are executed in the code mentioned above.

Note that values whose name ends with Cache are a distillation of all that has come before.

The functions that are called are (in this order):

reductionOneBlockWithLayer
reductionFinalNormalization
fusedQKVMatmulX
ropeRotationWithCacheCopy

processHeadsFlashAttention
processHeadsParallel

matrixVectorGenericWithResidual
reductionOneBlockWithLayer
reductionFinalNormalization
fusedRmsNormFFNGateUp
matrixVectorGenericWithResidual

The italicised functions configure the attention.

Let's go through them:

reductionOneBlockWithLayer calculates the root mean square of the input and takes its inverse. This is calculated per work group.

reductionFinalNormalization reduces these calculations of all the workgroups to a single value.
 
fusedQKVMatmulX multiplies the vector x by the vectors wq, wk, and wv, putting the results in q, k, and v respectively. 

ropeRotationWithCacheCopy here we see a standard, two-dimensional rotation on pairs of elements in q and k. The angle we rotate by is a function of both the word's position and which attention head this is for (which is just the global thread ID mod the head size). Note we're imposing a geometry on vectors q and k that doesn't exist before this point. 

processHeadsFlashAttention this calculates softmax(Q KT) V. It uses tiling [previous post] as this is a massive matrix operation.

This is done for all the FFN layers plus similar calls in LogitsFP16Layer.setupLogitsTaskGraph for the logit layer. Here, the functions are (in this order):

reductionOneBlockWithLayer
reductionFinalNormalization
mapContextWithQuantizeLogits
matrixVectorGeneric

Note that these are all called for each of the layers. There appear to be 16 for Llama.

There are more layers but this is enough for my first post on the subject. More in Part 2.

Rope

Upon loading the model, RoPE.precomputeFreqsCis is called when just before the weights are loaded.

We do a 2-d rotation on pair elements of the vector, imposing a geometry that was not there before. The choice of a 2-d rotation is arbitrary but it's computationally the cheapest dimension to rotate as the number of elements in a rotation matrix is d2 (where d is the number of dimensions we're working in).

Saturday, June 6, 2026

Production ready Polaris

Persisting the metadata

First, create a DB. I created a Postgres RDS database in AWS and allowed it to create the VPC, subnets etc. It took me a while to work out why I could connect from my laptop but not a Polaris running in Azure: the source in the security group AWS automatically generated allowed my IP address but not Microsoft's.

You can check what IP address the greater internet sees you as with:

curl -s https://checkip.amazonaws.com

Bootstrap Polaris with:

docker run --rm -it   --env="polaris.persistence.type=relational-jdbc"   --env="quarkus.datasource.username=$DB_USERNAME"   --env="quarkus.datasource.password=$DB_PASSWORD"   --env="quarkus.datasource.jdbc.url=jdbc:postgresql://$DB_HOST:5432/polaris_db"   apache/polaris-admin-tool:latest bootstrap -r POLARIS -c POLARIS,root,s3cr3t

(You can purge the database by using the above but with arguments purge -r POLARIS)

Then you can see in Postgres:

postgres=> \c polaris_db
polaris_db=> SELECT * FROM pg_catalog.pg_tables;
     schemaname     |           tablename           | tableowner | tablespace | hasindexes | hasrules | hastriggers | rowsecurity 
--------------------+-------------------------------+------------+------------+------------+----------+-------------+-------------
 polaris_schema     | version                       | postgres   |            | t          | f        | f           | f
 polaris_schema     | entities                      | postgres   |            | t          | f        | f           | f
 polaris_schema     | grant_records                 | postgres   |            | t          | f        | f           | f
 polaris_schema     | principal_authentication_data | postgres   |            | t          | f        | f           | f

Create the database with:

CREATE DATABASE polaris_db;
CREATE USER polaris_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE polaris_db TO polaris_user;
\c polaris_db
GRANT ALL ON SCHEMA public TO polaris_user;

If you mess up your Polaris, just run:

kubectl rollout restart deployment polaris-deployment

as now the data is all in the database.

Access Control

For integration tests, you can just use the client_id and client_secret with which you set up Polaris. But if you put it in production, you'll want to create users (Principals).

"At the most basic level, Polaris' persistence layer stores Entities and Grants, where Grants define the access-control-related relationship between entities." [Apache Polaris Catalog Federation Proposal]

To access Polaris, you need a Principal. This will have Principal Roles. They need to be associated with the Catalog Roles that in turn belong to a Catalog.

REST via curl

Effective debugging of Polaris can be done by poking its REST API. There is a command line tool to do this more easily but it's a bit buggy. The latest code for me had trouble deserializing SigV4 objects.

First, you need a token

POLARIS_TOKEN=$(curl -X POST "https://$HOST/api/catalog/v1/oauth/tokens"   -H "Content-Type: application/x-www-form-urlencoded"   -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=PRINCIPAL_ROLE:ALL" | jq -r '.access_token')

View namespaces

curl -X GET "https://$HOST/api/catalog/v1/$CATALOG_NAME/namespaces/$NAMESPACE" -H "Authorization: Bearer ${POLARIS_TOKEN}" | jq
{
  "namespace": [
    "samples"
  ],
  "properties": {
    "owner": "henryp",
    "location": "s3a://emrys-afon-bucket/samples/"
  }
}

View tables

curl -X GET "https://$HOST/api/catalog/v1/$CATALOG_NAME/namespaces/$NAMESPACE/tables"   -H "Authorization: Bearer ${POLARIS_TOKEN}"   -H "Accept: application/json"   -s | jq .

or given a table:

curl -X GET "https://$HOST/api/catalog/v1/$CATALOG_NAME/namespaces/$NAMESPACE/tables/$TABLE" -H "Authorization: Bearer ${POLARIS_TOKEN}" | jq

Clean up

Remove lingering details in the namespace with something like:

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${POLARIS_TOKEN}"  "https://$HOST/api/catalog/v1/aws/namespaces/samples/properties" --data '{"removals": ["owner"] }' | jq

Delete Catalog

curl -X DELETE "https://$HOST/api/management/v1/catalogs/$CATALOG_NAME"   -H "Authorization: Bearer ${POLARIS_TOKEN}"   -H "Content-Type: application/json" -o /dev/null -s -w "%{http_code}\n"