Use osd & mon certs instead of usernames

This commit is contained in:
Vitaliy Filippov
2026-05-19 17:19:47 +03:00
parent 3dd0e4daca
commit 9243aed615
2 changed files with 118 additions and 38 deletions
+31 -7
View File
@@ -3,6 +3,7 @@
const AntiEtcd = require('antietcd');
const vitastor_auth_filter = require('./vitastor_auth_filter.js');
const vitastor_persist_filter = require('./vitastor_persist_filter.js');
const { b64, local_ips } = require('./utils.js');
@@ -40,7 +41,7 @@ class AntiEtcdAdapter
port: selected[0][2],
cert: config.antietcd_cert,
key: config.antietcd_key,
ca: config.etcd_ca,
ca: config.antietcd_ca,
data: config.antietcd_data_file || ((config.antietcd_data_dir || '/var/lib/vitastor') + '/mon_'+selected[0][2]+'.json.gz'),
persist_filter: vitastor_persist_filter({ vitastor_prefix: config.etcd_prefix || '/vitastor' }),
node_id: selected[0][1].substr(2)+':'+selected[0][2], // node_id = ip:port
@@ -49,15 +50,38 @@ class AntiEtcdAdapter
stale_read: 1,
log_level: 1,
};
if (config.etcd_proxy)
{
// Monitor may use the builtin etcd_proxy mode
if (!config.etcd_proxy.urls)
{
console.error('etcd_proxy.urls are empty');
process.exit(1);
}
antietcd_config.etcd_proxy = config.etcd_proxy.urls;
antietcd_config.etcd_cert = config.etcd_proxy.cert;
antietcd_config.etcd_key = config.etcd_proxy.key;
antietcd_config.etcd_ca = config.etcd_proxy.ca;
delete antietcd_config.data;
delete antietcd_config.persist_filter;
delete antietcd_config.cluster;
delete antietcd_config.cluster_key;
}
if (config.use_auth)
{
antietcd_config.client_cert_auth = true;
antietcd_config.auth_filter = require('./vitastor_auth_filter.js');
antietcd_config.peer_ca = config.antietcd_server_ca;
if (!config.antietcd_server_ca || config.antietcd_server_ca == config.etcd_ca)
antietcd_config.auth_filter = vitastor_auth_filter;
antietcd_config.ca = config.client_ca;
antietcd_config.osd_ca = config.osd_ca;
antietcd_config.mon_ca = config.mon_ca;
if (!config.etcd_proxy)
{
console.error('Secure setup requires separate antietcd_server_ca (for signing antietcd server certificates) and etcd_ca (for signing client certificates)');
process.exit(1);
antietcd_config.peer_ca = config.antietcd_server_ca;
if (!config.antietcd_server_ca || config.antietcd_server_ca == config.client_ca)
{
console.error('Secure setup requires separate antietcd_server_ca (for signing antietcd server certificates) and client_ca (for signing client certificates)');
process.exit(1);
}
}
}
for (const key in config)
@@ -184,7 +208,7 @@ class AntiEtcdAdapter
await new Promise(ok => setTimeout(ok, timeout-(Date.now()-prev)));
}
prev = Date.now();
const res = await this.antietcd.api(path.replace(/^\/+/, '').replace(/\/+$/, '').replace(/\/+/g, '_'), body, { username: 'root' });
const res = await this.antietcd.api(path.replace(/^\/+/, '').replace(/\/+$/, '').replace(/\/+/g, '_'), body, { user_type: 'mon' });
if (res.error)
{
console.error('Failed to query antietcd '+path+' (retry '+retry+'/'+retries+'): '+res.error);
+87 -31
View File
@@ -6,16 +6,19 @@
// 1. Users.
// Stored in /vitastor/config/user/<username>.
// Has 2 properties:
// - type, one of: osd, mon, admin, client.
// osd, mon types should be used by OSDs/monitors.
// admin should be used for administrative access from vitastor-cli.
// client should be used for regular clients.
// - type, one of: admin, client.
// admin has full access to all images and also to cluster config.
// client has r/w access to owned images and r/o access to images with reader_group.
// - groups, a list of group names the user is included in.
// 2. Images.
// Stored in /vitastor/config/inode/<pool>/<inode>. Has the following properties:
// - owner (user name)
// - owner_group (group name)
// - reader_group
// 3. Certificates.
// - osd, mon use their own trusted certificates.
const { X509Certificate } = require('node:crypto');
const static_perms = {
invalid: {
@@ -24,7 +27,7 @@ const static_perms = {
},
osd: {
keys: { '/pg/config': false },
prefixes: { '/osd/': true, '/pg/state/': true, '/pg/history/': true, '/pgstats/': true },
prefixes: { '/config/': false, '/osd/': true, '/pg/state/': true, '/pg/history/': true, '/pgstats/': true },
},
mon: {
keys: { '/pg/config': true, '/stats': true, '/history/last_clean_pgs': true },
@@ -42,15 +45,15 @@ const static_perms = {
},
client: {
keys: { '/config/global': false, '/config/node_placement': false, '/config/pools': false, '/pg/config': false },
prefixes: { '/osd/stats/': false, '/pg/state/': false, '/index/maxid/': false },
prefixes: { '/osd/state/': false, '/pg/state/': false, '/index/maxid/': false },
},
};
const api_perms = {
osd: { lease_grant: true, lease_revoke: true, lease_keepalive: true },
mon: { lease_grant: true, lease_revoke: true, lease_keepalive: true },
osd: { lease_grant: true, lease_revoke: true, lease_keepalive: true, maintenance_status: true },
mon: { lease_grant: true, lease_revoke: true, lease_keepalive: true, maintenance_status: true },
admin: { maintenance_status: true },
client: {},
client: { maintenance_status: true },
};
class VitastorAuthFilter
@@ -63,6 +66,43 @@ class VitastorAuthFilter
this.prefix_parts = this.prefix.split('/');
}
async init()
{
if (!this.cfg.cert || !this.cfg.key || !this.cfg.osd_ca || !this.cfg.etcd_proxy && !this.cfg.peer_ca || !this.cfg.client_cert_auth)
{
throw new Error('Authenticated Vitastor setups require enabled client_cert_auth, cert, key'+
' and separate ca (client CA), osd_ca'+(this.cfg.etcd_proxy ? '' : ', peer_ca')+' and optionally mon_ca');
}
this.osd_ca = await this.antietcd.readPEM(this.cfg.osd_ca);
this.osd_ca_obj = new X509Certificate(this.osd_ca);
this.antietcd.tls.ca.push(this.osd_ca);
if (this.cfg.mon_ca)
{
this.mon_ca = await this.antietcd.readPEM(this.cfg.mon_ca);
this.mon_ca_obj = new X509Certificate(this.mon_ca_obj);
this.antietcd.tls.ca.push(this.mon_ca);
}
}
init_context(context, clientCert)
{
let cert = clientCert;
while (cert)
{
if (cert.fingerprint256 == this.osd_ca_obj.fingerprint256)
{
context.user_type = 'osd';
break;
}
if (this.mon_ca_obj && cert.fingerprint256 == this.mon_ca_obj.fingerprint256)
{
context.user_type = 'mon';
break;
}
cert = cert.issuerCertificate;
}
}
_get(path, decode)
{
let cur = this.antietcd.etctree.state;
@@ -349,19 +389,31 @@ class VitastorAuthFilter
return true;
}
_get_user(username)
_get_user(context)
{
if (!username)
if (context.user_type === 'osd' || context.user_type === 'mon')
{
return null;
return {
name: context.user_type,
type: context.user_type,
perms: static_perms[context.user_type],
};
}
let userInfo = this._get([ ...this.prefix_parts, 'config', 'user', username ], true);
if (!context.username)
{
return {};
}
let userInfo = this._get([ ...this.prefix_parts, 'config', 'user', context.username ], true);
if (!userInfo)
{
userInfo = { type: 'client' };
}
else if (userInfo.type !== 'client' && userInfo.type !== 'admin')
{
userInfo.type = 'client';
}
userInfo.perms = static_perms[userInfo.type] || static_perms['invalid'];
userInfo.name = username;
userInfo.name = context.username;
if (userInfo.groups instanceof Array)
{
userInfo.groups = userInfo.groups.reduce((a, c) => { a[c] = true; return a; }, {});
@@ -373,23 +425,27 @@ class VitastorAuthFilter
return userInfo;
}
filter_api(username, api/*, data*/)
filter_api(context, api/*, data*/)
{
if (username === 'root')
let type = 'client';
if (context.user_type === 'osd' || context.user_type === 'mon')
{
return true;
type = context.user_type;
}
const userInfo = this._get([ ...this.prefix_parts, 'config', 'user', username ], true);
return userInfo && api_perms[userInfo.type] && api_perms[userInfo.type][api];
else if (context.username)
{
const userInfo = this._get([ ...this.prefix_parts, 'config', 'user', context.username ], true);
if (userInfo && userInfo.type === 'admin')
{
type = 'admin';
}
}
return api_perms[type] && api_perms[type][api];
}
filter_txn(username, txn)
filter_txn(context, txn)
{
if (username === 'root')
{
return true;
}
const userInfo = this._get_user(username);
const userInfo = this._get_user(context);
if (!userInfo)
{
return null;
@@ -425,13 +481,13 @@ class VitastorAuthFilter
return txn;
}
filter_txn_response(username, txn, res)
filter_txn_response(context, txn, res)
{
if (!res.responses || username === 'root')
if (!res.responses)
{
return;
}
const userInfo = this._get_user(username);
const userInfo = this._get_user(context);
if (!userInfo)
{
for (const resp of res.responses)
@@ -452,13 +508,13 @@ class VitastorAuthFilter
}
}
filter_watch_message(username, msg)
filter_watch_message(context, msg)
{
if (!msg.result || !msg.result.events || username === 'root')
if (!msg.result || !msg.result.events)
{
return;
}
const userInfo = this._get_user(username);
const userInfo = this._get_user(context);
if (!userInfo)
{
msg.result.events = [];