v1.16.0: OpenBao as central secrets store + vault broker + UI rework

- Boot: bao-conf.init('sso-manager') replaces conf_manager; deep-merges
  secret/sso-manager/conf over file config (fail-soft). Scoped VAULT_TOKEN
  (policy sso-broker), never root.
- /api/vault reworked: middleware.auth -> scopeGuard -> token-injecting
  proxy. vault_broker.js mints Redis-cached per-user (user-<uid>) /
  per-admin (sso-admin) tokens via the sso-broker role; scopeGuard enforces
  path prefix on top of the OpenBao policy. Client auth-token stripped.
- vault UI renamed (vaultwarden.ejs -> vault.ejs), /vault route auth-gated,
  role-scoped: users see only secret/users/<uid>/, admins get free-form +
  Apps mint tab (secret/apps/<name>/*, token shown once).
- api_conf.js writes via bao-conf.set('sso-manager', ...).
- Remediation: config/*-secrets.js untracked+gitignored, test_plugins.js
  deleted, proxy-secrets.js.example placeholder added. Secrets remain in
  git history; provider-side rotation is the real fix.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-01 12:36:08 -04:00
parent 74746e409b
commit 59d4b65195
17 changed files with 718 additions and 380 deletions
+22 -4
View File
@@ -1,5 +1,5 @@
const router = require('express').Router();
const confManager = require('../utils/conf_manager');
const baoConf = require('@simpleworkjs/bao-conf');
const permission = require('../utils/permission');
const conf = require('@simpleworkjs/conf');
@@ -21,9 +21,23 @@ router.get('/', async (req, res) => {
res.json(editable);
});
// Shallow-per-key merge of `src` into the live conf object (matches the old
// conf_manager.applyConf behaviour: nested objects are spread, not deep-merged,
// so call-time conf readers see saved values without a restart).
function applyToLiveConf(src) {
if (!src) return;
for (const key of Object.keys(src)) {
if (typeof src[key] === 'object' && src[key] !== null && !Array.isArray(src[key])) {
conf[key] = { ...(conf[key] || {}), ...src[key] };
} else {
conf[key] = src[key];
}
}
}
router.post('/', async (req, res, next) => {
try {
const existing = await confManager.getVaultConf() || {};
const existing = await baoConf.get('sso-manager/conf') || {};
// Deep merge req.body into existing
for (const key of Object.keys(req.body)) {
if (typeof req.body[key] === 'object' && req.body[key] !== null && !Array.isArray(req.body[key])) {
@@ -32,11 +46,15 @@ router.post('/', async (req, res, next) => {
existing[key] = req.body[key];
}
}
await confManager.setVaultConf(existing);
await baoConf.set('sso-manager/conf', existing);
// Reflect the saved values in the live conf immediately (the next boot's
// bao-conf.init() would pick them up too, but this keeps running readers
// current without a restart, as the old conf_manager did).
applyToLiveConf(existing);
res.json({ success: true });
} catch(err) {
next(err);
}
});
module.exports = router;
module.exports = router;
+19 -3
View File
@@ -11,6 +11,8 @@ const {Tos} = require('../models/tos');
const conf = require('@simpleworkjs/conf');
const buildInfo = require('../utils/build_info');
const { mountStaticModules } = require('@simpleworkjs/app-stack');
const middleware = require('../middleware/auth');
const permission = require('../utils/permission');
const values ={
title: conf.environment !== 'production' ? `dev` : '',
@@ -65,7 +67,6 @@ router.get('/dashboard', (req, res) => res.redirect(301, '/overview'));
router.get('/executive', (req, res) => res.redirect(301, '/overview'));
router.get('/conf', async function(req, res, next) {
const permission = require('../utils/permission');
try {
await permission.byGroup(req.user, ['app_sso_admin']);
res.render('conf', {...values});
@@ -86,8 +87,23 @@ router.get('/plugins', function(req, res, next) {
res.redirect('/directory');
});
router.get('/vault', function(req, res, next) {
res.render('vaultwarden', {...values});
router.get('/vault', middleware.auth, async function(req, res, next) {
// Personal per-user secrets (secret/users/<uid>/*) for everyone; admins get
// free-form access across all of secret/ plus an Apps tab to mint scoped
// tokens for external apps. The /api/vault proxy enforces the same scoping
// server-side (scopeGuard + the token's own OpenBao policy).
let isAdmin = false;
try {
await permission.byGroup(req.user, ['app_sso_admin']);
isAdmin = true;
} catch (e) { /* non-admin: personal namespace only */ }
res.render('vault', {
...values,
vaultUid: req.user.uid,
vaultIsAdmin: isAdmin,
vaultBase: isAdmin ? '' : `users/${req.user.uid}/`,
vaultAddr: process.env.VAULT_ADDR || 'http://openbao:8200',
});
});
// Linkable deep-link to a single resource's modal, e.g. from the resource