diff --git a/mon/antietcd_adapter.js b/mon/antietcd_adapter.js index a3f439e4..184af069 100644 --- a/mon/antietcd_adapter.js +++ b/mon/antietcd_adapter.js @@ -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); diff --git a/mon/vitastor_auth_filter.js b/mon/vitastor_auth_filter.js index 83264c76..2de29a5b 100644 --- a/mon/vitastor_auth_filter.js +++ b/mon/vitastor_auth_filter.js @@ -6,16 +6,19 @@ // 1. Users. // Stored in /vitastor/config/user/. // 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//. 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 = [];