diff --git a/README.md b/README.md index 9e21826..2bd28a7 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ Setting | Description `DOORMAN_ENROLL_SECRET` | A list of valid enrollment keys to use. See osquery TLS [remoting settings](https://osquery.readthedocs.io/en/stable/deployment/remote/) for more information. By default, this list is empty. `DOORMAN_EXPECTS_UNIQUE_HOST_ID` | If osquery is deployed on endpoints to start with the `--host_identifier=uuid` cli flag, set this value to `True`. Default is `True`. `DOORMAN_CHECKIN_INTERVAL` | Time (in seconds) nodes are expected to check-in for configurations or call the distributed read endpoint. Nodes that fail to check-in within this time will be highlighted in red on the main nodes page. -`DOORMAN_ENROLL_DEFAULT_TAGS` | A default set of tags to apply to newly enrolled nodes. +`DOORMAN_ENROLL_DEFAULT_TAGS` | A default set of tags to apply to newly enrolled nodes. See also `DOORMAN_ENROLL_SECRET_TAG_DELIMITER`. +`DOORMAN_ENROLL_SECRET_TAG_DELIMITER` | A delimiter to separate the enroll secret from tag values (up to maximum of 10 tags) the node should inherit upon enrollment. Default is `None`, i.e., a node will not inherit any tags when first enrolled. This provides a little more flexibility than `DOORMAN_ENROLL_DEFAULT_TAGS`, allowing individual nodes to inherit different tags based on environment, asset class, etc. In the osquery configuration, you would supply an enroll secret in the format: `--enroll-secret=secret:tag1:tag2:tag3`, assuming `:` is your tag delimiter. `DOORMAN_CAPTURE_NODE_INFO` | A list of tuples, containing a pair of osquery result column and label used to determine what information is captured about a node and presented on a node's information page. In order for this information to be captured, a node must execute a query which returns a result containing these columns. By default, the following information is captured: (i.e., `select * from system_info;`) `DOORMAN_EXTRA_SCHEMA` | Doorman will validate queries against the expected set of tables from osquery. If you use any custom extensions, you'll need to add the corresponding schema here so you can use them in queries. `DOORMAN_MINIMUM_OSQUERY_LOG_LEVEL` | The minimum osquery status log level to retain. Default is `0`, (all logs). diff --git a/doorman/api.py b/doorman/api.py index 8750ec2..ab8bab5 100644 --- a/doorman/api.py +++ b/doorman/api.py @@ -109,6 +109,13 @@ def enroll(): # If we pre-populate node table with a per-node enroll_secret, # let's query it now. + if current_app.config.get('DOORMAN_ENROLL_SECRET_TAG_DELIMITER'): + delimiter = current_app.config.get('DOORMAN_ENROLL_SECRET_TAG_DELIMITER') + enroll_secret, _, enroll_tags = enroll_secret.partition(delimiter) + enroll_tags = set([tag.strip() for tag in enroll_tags.split(delimiter)[:10]]) + else: + enroll_secret, enroll_tags = enroll_secret, set() + node = Node.query.filter(Node.enroll_secret == enroll_secret).first() if not node and enroll_secret not in current_app.config['DOORMAN_ENROLL_SECRET']: @@ -176,7 +183,9 @@ def enroll(): enrolled_on=now, last_ip=request.remote_addr) - for value in current_app.config.get('DOORMAN_ENROLL_DEFAULT_TAGS', []): + enroll_tags.update(current_app.config.get('DOORMAN_ENROLL_DEFAULT_TAGS', [])) + + for value in sorted((t.strip() for t in enroll_tags if t)): tag = Tag.query.filter_by(value=value).first() if tag and tag not in node.tags: node.tags.append(tag) diff --git a/doorman/settings.py b/doorman/settings.py index 91f50e6..d061088 100644 --- a/doorman/settings.py +++ b/doorman/settings.py @@ -39,6 +39,7 @@ class Config(object): DOORMAN_PACK_DELIMITER = '/' DOORMAN_MINIMUM_OSQUERY_LOG_LEVEL = 0 + DOORMAN_ENROLL_SECRET_TAG_DELIMITER = None DOORMAN_ENROLL_DEFAULT_TAGS = [ ] @@ -272,7 +273,7 @@ class TestConfig(Config): DOORMAN_ENROLL_SECRET = [ 'secret', ] - DOORMAN_UNIQUE_HOST_ID = False + DOORMAN_EXPECTS_UNIQUE_HOST_ID = False DOORMAN_AUTH_METHOD = None diff --git a/tests/test_functional.py b/tests/test_functional.py index 3662c54..4fad47e 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -218,6 +218,90 @@ def test_reenrolling_node_does_not_get_new_tags(self, db, node, testapp): assert node.is_active assert node.last_ip == '127.0.0.1' + def test_enroll_secret_tags(self, db, node, testapp): + testapp.app.config['DOORMAN_ENROLL_SECRET_TAG_DELIMITER'] = ':' + testapp.app.config['DOORMAN_EXPECTS_UNIQUE_HOST_ID'] = True + enroll_secret = testapp.app.config['DOORMAN_ENROLL_SECRET'][0] + resp = testapp.post_json(url_for('api.enroll'), { + 'enroll_secret': enroll_secret, + 'host_identifier': 'foobaz'}, + extra_environ=dict(REMOTE_ADDR='127.0.0.2') + ) + + assert resp.json['node_invalid'] is False + assert resp.json['node_key'] != node.node_key + + n = Node.query.filter_by(node_key=resp.json['node_key']).one() + assert n.is_active + assert n.last_ip == '127.0.0.2' + assert not n.tags + + resp = testapp.post_json(url_for('api.enroll'), { + 'enroll_secret': ':'.join([enroll_secret, 'foo', 'bar']), + 'host_identifier': 'barbaz'}, + extra_environ=dict(REMOTE_ADDR='127.0.0.2') + ) + + assert resp.json['node_invalid'] is False + assert resp.json['node_key'] != node.node_key + + n = Node.query.filter_by(node_key=resp.json['node_key']).one() + assert n.is_active + assert n.last_ip == '127.0.0.2' + assert len(n.tags) == 2 + assert 'foo' in (t.value for t in n.tags) + assert 'bar' in (t.value for t in n.tags) + + resp = testapp.post_json(url_for('api.enroll'), { + 'enroll_secret': ':'.join([enroll_secret, 'foo', 'bar', 'baz']), + 'host_identifier': 'barbaz'}, + extra_environ=dict(REMOTE_ADDR='127.0.0.2') + ) + assert resp.json['node_key'] != node.node_key + assert resp.json['node_key'] == n.node_key + + n = Node.query.filter_by(node_key=resp.json['node_key']).one() + assert n.is_active + assert n.last_ip == '127.0.0.2' + assert len(n.tags) == 2 + assert 'foo' in (t.value for t in n.tags) + assert 'bar' in (t.value for t in n.tags) + + testapp.app.config['DOORMAN_ENROLL_SECRET'].append(':'.join(enroll_secret)) + testapp.app.config['DOORMAN_ENROLL_SECRET_TAG_DELIMITER'] = ',' + resp = testapp.post_json(url_for('api.enroll'), { + 'enroll_secret': ':'.join(enroll_secret), + 'host_identifier': 'bartab'}, + extra_environ=dict(REMOTE_ADDR='127.0.0.2') + ) + + assert resp.json['node_invalid'] is False + assert resp.json['node_key'] != node.node_key + + n = Node.query.filter_by(node_key=resp.json['node_key']).one() + assert n.is_active + assert n.last_ip == '127.0.0.2' + assert not n.tags + + def test_enroll_max_secret_tags(self, db, node, testapp): + testapp.app.config['DOORMAN_ENROLL_SECRET_TAG_DELIMITER'] = ':' + testapp.app.config['DOORMAN_EXPECTS_UNIQUE_HOST_ID'] = True + enroll_secret = testapp.app.config['DOORMAN_ENROLL_SECRET'][0] + enroll_secret = ':'.join([enroll_secret] + list('abcdef1234567890')) + resp = testapp.post_json(url_for('api.enroll'), { + 'enroll_secret': ':'.join([enroll_secret, 'foo', ]), + 'host_identifier': 'barbaz'}, + extra_environ=dict(REMOTE_ADDR='127.0.0.2') + ) + + assert resp.json['node_invalid'] is False + assert resp.json['node_key'] != node.node_key + + n = Node.query.filter_by(node_key=resp.json['node_key']).one() + assert n.is_active + assert n.last_ip == '127.0.0.2' + assert len(n.tags) == 10 # max 10 tags when passing tags w/enroll secret + class TestConfiguration: