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 AntiEtcd = require('antietcd');
const vitastor_auth_filter = require('./vitastor_auth_filter.js');
const vitastor_persist_filter = require('./vitastor_persist_filter.js'); const vitastor_persist_filter = require('./vitastor_persist_filter.js');
const { b64, local_ips } = require('./utils.js'); const { b64, local_ips } = require('./utils.js');
@@ -40,7 +41,7 @@ class AntiEtcdAdapter
port: selected[0][2], port: selected[0][2],
cert: config.antietcd_cert, cert: config.antietcd_cert,
key: config.antietcd_key, 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'), 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' }), 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 node_id: selected[0][1].substr(2)+':'+selected[0][2], // node_id = ip:port
@@ -49,15 +50,38 @@ class AntiEtcdAdapter
stale_read: 1, stale_read: 1,
log_level: 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) if (config.use_auth)
{ {
antietcd_config.client_cert_auth = true; antietcd_config.client_cert_auth = true;
antietcd_config.auth_filter = require('./vitastor_auth_filter.js'); antietcd_config.auth_filter = vitastor_auth_filter;
antietcd_config.peer_ca = config.antietcd_server_ca; antietcd_config.ca = config.client_ca;
if (!config.antietcd_server_ca || config.antietcd_server_ca == config.etcd_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)'); antietcd_config.peer_ca = config.antietcd_server_ca;
process.exit(1); 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) for (const key in config)
@@ -184,7 +208,7 @@ class AntiEtcdAdapter
await new Promise(ok => setTimeout(ok, timeout-(Date.now()-prev))); await new Promise(ok => setTimeout(ok, timeout-(Date.now()-prev)));
} }
prev = Date.now(); 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) if (res.error)
{ {
console.error('Failed to query antietcd '+path+' (retry '+retry+'/'+retries+'): '+res.error); console.error('Failed to query antietcd '+path+' (retry '+retry+'/'+retries+'): '+res.error);
+87 -31
View File
@@ -6,16 +6,19 @@
// 1. Users. // 1. Users.
// Stored in /vitastor/config/user/<username>. // Stored in /vitastor/config/user/<username>.
// Has 2 properties: // Has 2 properties:
// - type, one of: osd, mon, admin, client. // - type, one of: admin, client.
// osd, mon types should be used by OSDs/monitors. // admin has full access to all images and also to cluster config.
// admin should be used for administrative access from vitastor-cli. // client has r/w access to owned images and r/o access to images with reader_group.
// client should be used for regular clients.
// - groups, a list of group names the user is included in. // - groups, a list of group names the user is included in.
// 2. Images. // 2. Images.
// Stored in /vitastor/config/inode/<pool>/<inode>. Has the following properties: // Stored in /vitastor/config/inode/<pool>/<inode>. Has the following properties:
// - owner (user name) // - owner (user name)
// - owner_group (group name) // - owner_group (group name)
// - reader_group // - reader_group
// 3. Certificates.
// - osd, mon use their own trusted certificates.
const { X509Certificate } = require('node:crypto');
const static_perms = { const static_perms = {
invalid: { invalid: {
@@ -24,7 +27,7 @@ const static_perms = {
}, },
osd: { osd: {
keys: { '/pg/config': false }, 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: { mon: {
keys: { '/pg/config': true, '/stats': true, '/history/last_clean_pgs': true }, keys: { '/pg/config': true, '/stats': true, '/history/last_clean_pgs': true },
@@ -42,15 +45,15 @@ const static_perms = {
}, },
client: { client: {
keys: { '/config/global': false, '/config/node_placement': false, '/config/pools': false, '/pg/config': false }, 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 = { const api_perms = {
osd: { 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 }, mon: { lease_grant: true, lease_revoke: true, lease_keepalive: true, maintenance_status: true },
admin: { maintenance_status: true }, admin: { maintenance_status: true },
client: {}, client: { maintenance_status: true },
}; };
class VitastorAuthFilter class VitastorAuthFilter
@@ -63,6 +66,43 @@ class VitastorAuthFilter
this.prefix_parts = this.prefix.split('/'); 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) _get(path, decode)
{ {
let cur = this.antietcd.etctree.state; let cur = this.antietcd.etctree.state;
@@ -349,19 +389,31 @@ class VitastorAuthFilter
return true; 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) if (!userInfo)
{ {
userInfo = { type: 'client' }; userInfo = { type: 'client' };
} }
else if (userInfo.type !== 'client' && userInfo.type !== 'admin')
{
userInfo.type = 'client';
}
userInfo.perms = static_perms[userInfo.type] || static_perms['invalid']; userInfo.perms = static_perms[userInfo.type] || static_perms['invalid'];
userInfo.name = username; userInfo.name = context.username;
if (userInfo.groups instanceof Array) if (userInfo.groups instanceof Array)
{ {
userInfo.groups = userInfo.groups.reduce((a, c) => { a[c] = true; return a; }, {}); userInfo.groups = userInfo.groups.reduce((a, c) => { a[c] = true; return a; }, {});
@@ -373,23 +425,27 @@ class VitastorAuthFilter
return userInfo; 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); else if (context.username)
return userInfo && api_perms[userInfo.type] && api_perms[userInfo.type][api]; {
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') const userInfo = this._get_user(context);
{
return true;
}
const userInfo = this._get_user(username);
if (!userInfo) if (!userInfo)
{ {
return null; return null;
@@ -425,13 +481,13 @@ class VitastorAuthFilter
return txn; return txn;
} }
filter_txn_response(username, txn, res) filter_txn_response(context, txn, res)
{ {
if (!res.responses || username === 'root') if (!res.responses)
{ {
return; return;
} }
const userInfo = this._get_user(username); const userInfo = this._get_user(context);
if (!userInfo) if (!userInfo)
{ {
for (const resp of res.responses) 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; return;
} }
const userInfo = this._get_user(username); const userInfo = this._get_user(context);
if (!userInfo) if (!userInfo)
{ {
msg.result.events = []; msg.result.events = [];