Fully support client CAs with access-lists

This commit changes access-list IP directives to be implemented using
the nginx "geo" directive.

This allows IP-based blocks to return 444 (drop connection) on
authorization failure when the "Drop Unauthorized" is enabled.

It also allows the implementation of "Satisfy Any" with the new
client CA certificate support - i.e. Satisfy Any can allow clients
from the local network to skip client certificate challenge, or drop
down to requesting basic authentication.

It should be noted that including basic authentication requirements
in Satisfy Any mode does prevent a 444 response from being sent, as
the basic auth challenge requires the server to respond.
This commit is contained in:
Will Rouesnel
2023-05-31 01:37:07 +10:00
parent f3c740954b
commit 4d491b2d76
6 changed files with 115 additions and 23 deletions

View File

@@ -11,6 +11,7 @@ const accessListClientCAsModel = require('../models/access_list_clientcas');
const proxyHostModel = require('../models/proxy_host'); const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log'); const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx'); const internalNginx = require('./nginx');
const config = require('../lib/config');
function omissions () { function omissions () {
return ['is_deleted']; return ['is_deleted'];
@@ -392,6 +393,26 @@ const internalAccessList = {
// do nothing // do nothing
} }
}) })
.then(() => {
// delete the client CA file
let clientca_file = internalAccessList.getClientCAFilename(row);
try {
fs.unlinkSync(clientca_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// delete the client geo file file
let client_file = internalAccessList.getClientFilename(row);
try {
fs.unlinkSync(client_file);
} catch (err) {
// do nothing
}
})
.then(() => { .then(() => {
// 4. audit log // 4. audit log
return internalAuditLog.add(access, { return internalAuditLog.add(access, {
@@ -541,6 +562,15 @@ const internalAccessList = {
return '/data/clientca/' + list.id; return '/data/clientca/' + list.id;
}, },
/**
* @param {Object} list
* @param {Integer} list.id
* @returns {String}
*/
getClientFilename: (list) => {
return '/data/nginx/client/' + list.id + '.conf';
},
/** /**
* @param {Object} list * @param {Object} list
* @param {Integer} list.id * @param {Integer} list.id
@@ -550,6 +580,7 @@ const internalAccessList = {
* @returns {Promise} * @returns {Promise}
*/ */
build: (list) => { build: (list) => {
const renderEngine = utils.getRenderEngine();
const htPasswdBuild = new Promise((resolve, reject) => { const htPasswdBuild = new Promise((resolve, reject) => {
logger.info('Building Access file #' + list.id + ' for: ' + list.name); logger.info('Building Access file #' + list.id + ' for: ' + list.name);
@@ -630,8 +661,45 @@ const internalAccessList = {
} }
}); });
const clientBuild = new Promise((resolve, reject) => {
logger.info('Building Access client file #' + list.id + ' for: ' + list.name);
let template = null;
const client_file = internalAccessList.getClientFilename(list);
const data = {
access_list: list
};
try {
template = fs.readFileSync(__dirname + '/../templates/access.conf', {encoding: 'utf8'});
} catch (err) {
reject(new error.ConfigurationError(err.message));
return;
}
return renderEngine
.parseAndRender(template, data)
.then((config_text) => {
fs.writeFileSync(client_file, config_text, {encoding: 'utf8'});
if (config.debug()) {
logger.success('Wrote config:', client_file, config_text);
}
resolve(true);
})
.catch((err) => {
if (config.debug()) {
logger.warn('Could not write ' + client_file + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
});
});
// Execute both promises concurrently // Execute both promises concurrently
return Promise.all([htPasswdBuild, caCertificateBuild]); return Promise.all([htPasswdBuild, caCertificateBuild, clientBuild]);
} }
}; };

View File

@@ -1,31 +1,37 @@
{% if access_list_id > 0 %} {% if access_list_id > 0 %}
{% if access_list.clientcas.size > 0 %} set $auth_basic "Authorization required";
# TLS Client Certificate Authorization {% if access_list.satisfy_any == 1 %}
if ($ssl_client_verify != "SUCCESS") { # Satisfy Any - any check can succeed - so look for success
if ( $access_list_{{ access_list_id }} = 1) {
set $auth_basic off;
}
if ( $ssl_client_verify = "SUCCESS" ) {
set $auth_basic off;
}
{% else %}
# Satisfy All - all checks must succeed (so handle fails)
if ( $access_list_{{ access_list_id }} = 0) {
return {% if drop_unauthorized == 1 %}444{% else %}403{% endif %}; return {% if drop_unauthorized == 1 %}444{% else %}403{% endif %};
} }
{% endif %} if ( $ssl_client_verify != "SUCCESS" ) {
return {% if drop_unauthorized == 1 %}444{% else %}403{% endif %};
}
{% endif %}
{% if access_list.items.length > 0 %} {% if access_list.items.length > 0 %}
# Basic Auth is enabled
# Authorization # Authorization
auth_basic "Authorization required"; auth_basic $auth_basic;
auth_basic_user_file /data/access/{{ access_list_id }}; auth_basic_user_file /data/access/{{ access_list_id }};
{% if access_list.pass_auth == 0 %}
{% if access_list.pass_auth == 0 %}
proxy_set_header Authorization ""; proxy_set_header Authorization "";
{% endif %} {% endif %}
{% endif %}
# Access Rules: {{ access_list.clients | size }} total
{% for client in access_list.clients %}
{{client | nginxAccessRule}}
{% endfor %}
deny all;
# Access checks must...
{% if access_list.satisfy_any == 1 %}
satisfy any;
{% else %} {% else %}
satisfy all; {% if access_list.satisfy_any == 1 %}
# Satisfy Any without Basic Auth
if ( $auth_basic != "off" ) {
return {% if drop_unauthorized == 1 %}444{% else %}403{% endif %};
}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -0,0 +1,16 @@
# Access List Clients for {{ access_list.id }} - {{ access_list.name }}
geo $realip_remote_addr $access_list_{{ access_list.id }} {
{% if access_list.client.size == 0 %}
default 1;
{% else %}
default 0;
{% endif %}
{% for client in access_list.clients %}
{% if client.directive == "allow" %}
{{client.address}} 1;
{% endif %}
{% if client.directive == "deny" %}
{{client.address}} 0;
{% endif %}
{% endfor %}
}

View File

@@ -73,6 +73,7 @@ http {
# Files generated by NPM # Files generated by NPM
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /data/nginx/client/*.conf;
include /data/nginx/default_host/*.conf; include /data/nginx/default_host/*.conf;
include /data/nginx/proxy_host/*.conf; include /data/nginx/proxy_host/*.conf;
include /data/nginx/redirection_host/*.conf; include /data/nginx/redirection_host/*.conf;

View File

@@ -21,6 +21,7 @@ mkdir -p \
/data/logs \ /data/logs \
/data/access \ /data/access \
/data/clientca \ /data/clientca \
/data/nginx/client \
/data/nginx/default_host \ /data/nginx/default_host \
/data/nginx/default_www \ /data/nginx/default_www \
/data/nginx/proxy_host \ /data/nginx/proxy_host \

View File

@@ -121,7 +121,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-muted">Note that the <code>allow</code> and <code>deny</code> directives will be applied in the order they are defined.</div> <div class="text-muted">Note that the most specific directive is what will be applied to the connection. Order does not matter.</div>
<div class="btn-list justify-content-end"> <div class="btn-list justify-content-end">
<button type="button" class="btn btn-teal access_add"><%- i18n('access-lists', 'access-add') %></button> <button type="button" class="btn btn-teal access_add"><%- i18n('access-lists', 'access-add') %></button>
</div> </div>