Use osd & mon certs instead of usernames
This commit is contained in:
+31
-7
@@ -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
@@ -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 = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user