Merge pull request #200 from theta42/feature/openbao-secrets

v1.13.0: load secrets from OpenBao at boot
This commit is contained in:
2026-08-01 12:34:55 -04:00
committed by GitHub
5 changed files with 127 additions and 66 deletions
+17
View File
@@ -6,6 +6,23 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
## [Unreleased] ## [Unreleased]
## [1.13.0] - 2026-08-01
### Changed
- **Secrets now load from OpenBao at boot** via
[@simpleworkjs/bao-conf](https://simpleworkjs.github.io/bao-conf/), which
deep-merges `secret/proxy/conf` over the file-loaded config. The proxy
authenticates to OpenBao with a scoped `VAULT_TOKEN` (policy `proxy`
read-only on its own path), never the root token. Because the OIDC
`clientSecret` is captured at require time inside `createOidcClient` (during
`require('../models')`, which `require('../app')` triggers transitively),
`bin/www` now defers `require('../app')` until after `bao-conf.init()`
resolves. Fail-soft: if OpenBao is unreachable, boot continues from
`CONF_SECRETS`. The `config/proxy-secrets.js` file is now an operator-edit
seed artifact (gitignored); OpenBao is authoritative. See theta-env's
[Secrets docs](https://theta42.github.io/theta-env/secrets/).
- Bumped package version to track the release tag.
## [1.12.1] - 2026-08-01 ## [1.12.1] - 2026-08-01
### Changed ### Changed
+17
View File
@@ -162,6 +162,23 @@ docker compose exec proxy tail -f /var/log/nginx/error.log
docker compose logs --tail=200 --since=10m proxy docker compose logs --tail=200 --since=10m proxy
``` ```
## Secrets
Secrets are loaded from **OpenBao** at boot via
[@simpleworkjs/bao-conf](https://simpleworkjs.github.io/bao-conf/), which
deep-merges `secret/proxy/conf` over the file-loaded config. The proxy's OIDC
`clientSecret` is captured at require time (inside `createOidcClient` during
`require('../models')`), so `bin/www` runs `bao-conf.init()` **before**
`require('../app')` (which transitively loads models). Fail-soft: if OpenBao is
unreachable, boot continues from `CONF_SECRETS`. The proxy authenticates to
OpenBao with the scoped `VAULT_TOKEN` (env, policy `proxy` — read only
`secret/proxy/conf`), never the root token.
The `config/proxy-secrets.js` file is an operator-edit seed artifact
(gitignored); the bootstrap writes the generated OAuth client creds into
OpenBao, which is authoritative. For the full architecture see theta-env's
**[Secrets docs](https://theta42.github.io/theta-env/secrets/)**.
## Manual Installation ## Manual Installation
For manual installation or other distributions, see the detailed steps below. For manual installation or other distributions, see the detailed steps below.
+78 -65
View File
@@ -4,34 +4,91 @@
* Module dependencies. * Module dependencies.
*/ */
var app = require('../app');
var debug = require('debug')('proxy-api:server');
var http = require('http');
const conf = require('@simpleworkjs/conf'); const conf = require('@simpleworkjs/conf');
const debug = require('debug')('proxy-api:server');
const http = require('http');
/** // @simpleworkjs/conf loads ./config/proxy-secrets.js synchronously, then
* Get port from environment and store in Express. // @simpleworkjs/bao-conf deep-merges secret/proxy/conf from OpenBao over it.
*/ // The OIDC clientSecret is captured at require time inside models (via
// createOidcClient), and require('../app') transitively loads models, so the
// OpenBao fetch MUST resolve before require('../app'). Fail-soft: if OpenBao
// is unreachable, init() leaves conf as the file-loaded fallback and boot
// continues from ./config/proxy-secrets.js.
require('@simpleworkjs/bao-conf').init({ path: 'proxy', conf }).then(() => {
var app = require('../app'); // models + createOidcClient now see merged conf
var port = normalizePort(process.env.NODE_PORT || conf.port || '3000'); /**
app.set('port', port); * Get port from environment and store in Express.
*/
/** var port = normalizePort(process.env.NODE_PORT || conf.port || '3000');
* Create HTTP server. app.set('port', port);
*/
var server = http.createServer(app); /**
* Create HTTP server.
*/
var io = require('socket.io')(server); var server = http.createServer(app);
app.io = io;
/** var io = require('socket.io')(server);
* Listen on provided port, on all network interfaces. app.io = io;
*/
server.listen(port); /**
server.on('error', onError); * Listen on provided port, on all network interfaces.
server.on('listening', onListening); */
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
console.log('Listening on ' + bind);
for(let listener of app.onListen){
listener()
}
}
}).catch(err => {
console.error('boot failed:', err);
process.exit(1);
});
/** /**
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false.
@@ -51,48 +108,4 @@ function normalizePort(val) {
} }
return false; return false;
} }
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
console.log('Listening on ' + bind);
for(let listener of app.onListen){
listener()
}
}
+13
View File
@@ -12,6 +12,7 @@
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.0",
"@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/frontend": "^0.2.7", "@simpleworkjs/frontend": "^0.2.7",
"@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/ldap": "^1.0.0",
@@ -297,6 +298,18 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@simpleworkjs/bao-conf": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/bao-conf/-/bao-conf-1.0.0.tgz",
"integrity": "sha512-HxB2ohFuDKbwTfNh5dXCot0dd6qoP+3Ebz1xKH0eOhKNVNMjHU1p7XZv2VTe+VnHn+DV22Eh8lAgWxnX/pkXUw==",
"license": "MIT",
"dependencies": {
"extend": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@simpleworkjs/conf": { "node_modules/@simpleworkjs/conf": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxy-api", "name": "proxy-api",
"version": "1.9.0", "version": "1.13.0",
"author": [ "author": [
{ {
"name": "William Mantly", "name": "William Mantly",
@@ -22,6 +22,7 @@
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/bao-conf": "^1.0.0",
"@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/frontend": "^0.2.7", "@simpleworkjs/frontend": "^0.2.7",
"@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/ldap": "^1.0.0",