-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fix] Control access with cookie, rather than bearer token
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
Showing
8 changed files
with
124 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.