Skip to content

Commit

Permalink
[fix] Control access with cookie, rather than bearer token
Browse files Browse the repository at this point in the history
As it turns out, the `openshift-session-token` cookie is the authentication vehicle for the OpenShift console; wielding it outside of its expected scope is tough (in particular, because it is an [`HttpOnly` cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#httponly)).

- Replace `kube-rbac-proxy` with 115 lines of JavaScript that do pretty much the same thing, except from said cookie and with more CORS
- Make the JS proxy visible under `console-openshift-console.$CLUSTERNAME/api/hubble`, so that the OpenShift console will send its cookie to it without messing with anything
  • Loading branch information
Dominique Quatravaux committed May 8, 2024
1 parent 1a5d070 commit 785d64c
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 0 deletions.
8 changes: 8 additions & 0 deletions api-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:22

COPY package.json package-lock.json .
RUN npm install

COPY index.js .

CMD ['node', 'index.js']
116 changes: 116 additions & 0 deletions api-proxy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const NodeCache = require('node-cache');
const fs = require('fs/promises');

const app = express();
app.use(cookieParser());
app.use(cors({ credentials: true }));

const tokenResolver = new TokenResolver();
app.use(createProxyMiddleware({
target: process.env.TEST_PROXY_TARGET || "http://hubble-api-epfl:81/",
changeOrigin: true,
async pathRewrite (path, req) {
const token = req.cookies["openshift-session-token"];
if (! await tokenResolver.validate(token)) {
throw new Error("Permission denied");
}
},
on: {
error (err, req, res) {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end('Error.');
}
}
}));

app.listen(3000, () => {
console.log('Reverse proxy server listening on port 3000');
});


function TokenResolver (opts) {
opts = {
positiveTTL: 30,
negativeTTL: 5,
apiServerUrl: "https://kubernetes.default.svc",
...(opts || {}) };

const cache = new NodeCache();

let ownTokenPromise;
async function getOwnToken () {
if (! ownTokenPromise) {
ownTokenPromise = fs.readFile("/var/run/secrets/kubernetes.io/serviceaccount/token");
}

return await ownTokenPromise;
}

async function k8sAPICall (kind, spec) {
const apiPathsByKind = {
TokenReview: "authentication.k8s.io/v1/tokenreviews",
SubjectAccessReview: "authorization.k8s.io/v1/subjectaccessreviews"
}

const apiPath = apiPathsByKind[kind];
if (! apiPath) {
throw new Error(`Unknown Kind: ${kind}`);
}

const apiVersion = apiPath.split(',').slice(0, -1).join(',');

const res = await fetch(`${opts.apiServerUrl}/${apiPath}`,
{
method: "POST",
headers: [ ['Content-Type', 'application/json'],
['Authorization', `Bearer ${await getOwnToken()}`] ],
body: JSON.stringify({
apiVersion,
kind,
spec
})
});

return await res.json();
}

async function hasAccess (token) {
const tokenStruct = await k8sAPICall("TokenReview", {
spec: { token }
});
if (! (tokenStruct && tokenStruct.status && tokenStruct.status.authenticated)) {
return false;
}

const accessStruct = await k8sAPICall("SubjectAccessReview", {
spec: {
user: tokenStatus.user.username,
groups: tokenStatus.user.groups,
nonResourceAttributes: {
// Dovetails ../charts/okd-epfl-hubble-ui/templates/hubble/api-access-clusterrole.yaml
verb: "GET",
path: "/api/hubble-ui"
}
}
});
return accessStruct.status && accessStruct.status.allowed;
}

return {
async validate (token) {
if (! cache.get(token)) {
validated = await hasAccess(token);
cache.set(token, { validated }, validated ? opts.positiveTTL : opts.negativeTTL);
}

return cache.get(token).validated;
}
}
}

0 comments on commit 785d64c

Please sign in to comment.