]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
acl: Add ACL support to http and ari
authorMike Bradeen <mbradeen@sangoma.com>
Fri, 27 Feb 2026 19:35:37 +0000 (12:35 -0700)
committergithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Thu, 5 Mar 2026 12:52:43 +0000 (12:52 +0000)
Add uri prefix based acl support to the built in http server.
This allows an acl to be added per uri prefix (ie '/metrics'
or '/ws') to restrict access.

Add user based acl support for ARI. This adds new acl options
to the user section of ari.conf to restrict access on a per
user basis.

resolves: #1799

UserNote: A new section, type=restriction has been added to http.conf
to allow an uri prefix based acl to be configured. See
http.conf.sample for examples and more information.
The user section of ari.conf can now contain an acl configuration
to restrict users access. See ari.conf.sample for examples and more
information

configs/samples/ari.conf.sample
configs/samples/http.conf.sample
main/http.c
res/ari/ari_doc.xml
res/ari/cli.c
res/ari/config.c
res/ari/internal.h
res/res_ari.c

index e50eb39fe5c54563042d9fe46a13905caaa62a73..04973e10b4c5f93be9f25e51890682a784052aa5 100644 (file)
@@ -35,6 +35,22 @@ enabled = yes       ; When set to no, ARI support is disabled.
 ; When set to plain, the password is in plaintext.
 ;
 ;password_format = plain
+;
+; The following three options (permit, deny, acl) allow for a per-user acl to be
+; configured.  The format follows the rules as documented in acl.conf.sample
+;
+; If no restriction is defined for a given user, no IPs will be blocked by
+; Asterisk (legacy behavior).
+;
+;deny =             ; Deny acces from the subnet for the given user
+;permit =           ; Permit access from the subnet(s) for the given user
+;acl =              ; Optional name for the acl.
+;
+;Example:
+;deny = 0.0.0.0/0
+;permit = 127.0.0.1,10.0.0.0/24
+;acl = localasteriskuser
+;
 
 ; Outbound Websocket Connections
 ;
index bd9794c5a99d73ff0ade23afb3c3f5170f51619b..c1c38b21ca7153c98d4265e6d1a0829cf0ccb97d 100644 (file)
@@ -130,3 +130,41 @@ bindaddr=127.0.0.1
 ; POST URL: /asterisk/uploads will put files in /var/lib/asterisk/uploads/.
 ;uploads = /var/lib/asterisk/uploads/
 ;
+
+;[uripath]
+;
+;type = restriction ; Specifies acl configuration
+;
+; The following options (permit, deny, acl) allow for an acl to be configured
+; on a per uri prefix basis. The first character should be an '/'
+;
+; The format follows the rules as documented in acl.conf.sample
+;
+; If no restriction is defined for a given prefix, legacy behavior will apply.
+;
+; If multiple restrictions apply, any restriction that denies will supersede
+; any another restrictions that permit.  For example if an /ari restriction
+; results in a deny, but an /ari/channels restriction would permit, the
+; attempt would still be denied.
+;
+;deny =             ; Deny the subnet access for the given user
+;permit =           ; Permit the subnet(s) access for the given user
+;acl =              ; Optional name for the acl.
+;
+;Examples:
+;
+; Only allow ari connections from localhost:
+;
+;[/ari]
+;type = restriction
+;deny = 0.0.0.0/0
+;permit = 127.0.0.1
+;acl = localarionly
+;
+; Only allow metrics to be gathered by 10.0.0.23
+;
+;[/metrics]
+;type = restriction
+;deny = 0.0.0.0/0
+;permit = 10.0.0.23
+;
\ No newline at end of file
index 9d7ae3d6aae6bb101a65ba90e8ea377b52648fe8..37d4d08a7eda19e0c29f264f87922b7ff9d6e744 100644 (file)
@@ -51,6 +51,7 @@
 #include <fcntl.h>
 
 #include "asterisk/paths.h"    /* use ast_config_AST_DATA_DIR */
+#include "asterisk/acl.h"
 #include "asterisk/cli.h"
 #include "asterisk/tcptls.h"
 #include "asterisk/http.h"
@@ -177,6 +178,19 @@ struct http_uri_redirect {
 
 static AST_RWLIST_HEAD_STATIC(uri_redirects, http_uri_redirect);
 
+/*! \brief Per-path ACL restriction */
+struct http_restriction {
+       AST_LIST_ENTRY(http_restriction) entry;
+       struct ast_acl_list *acl;
+       char path[];
+};
+
+AST_LIST_HEAD_NOLOCK(http_restriction_list, http_restriction);
+
+static AST_RWLIST_HEAD_STATIC(restrictions, http_restriction);
+
+static int check_restriction_acl(struct ast_tcptls_session_instance *ser, const char *uri);
+
 static const struct ast_cfhttp_methods_text {
        enum ast_http_method method;
        const char *text;
@@ -1503,6 +1517,13 @@ static int handle_uri(struct ast_tcptls_session_instance *ser, char *uri,
                }
        }
 
+       /* Check path-based ACL restrictions */
+       if (check_restriction_acl(ser, uri) != 0) {
+               ast_http_request_close_on_completion(ser);
+               ast_http_error(ser, 403, "Forbidden", "Access denied by ACL");
+               goto cleanup;
+       }
+
        AST_RWLIST_RDLOCK(&uri_redirects);
        AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry) {
                if (!strcasecmp(uri, redirect->target)) {
@@ -2127,6 +2148,36 @@ done:
        return NULL;
 }
 
+/*!
+ * \brief Check if a URI path is allowed or denied by acl
+ * \param ser TCP/TLS session instance
+ * \param uri The URI path to check
+ * \return 0 if allowed, -1 if denied
+ */
+static int check_restriction_acl(struct ast_tcptls_session_instance *ser, const char *uri)
+{
+       struct http_restriction *restriction;
+       int denied = 0;
+
+       AST_RWLIST_RDLOCK(&restrictions);
+       AST_RWLIST_TRAVERSE(&restrictions, restriction, entry) {
+               if (ast_begins_with(uri, restriction->path)) {
+                       if (restriction->acl && !ast_acl_list_is_empty(restriction->acl)) {
+                               if (ast_apply_acl(restriction->acl, &ser->remote_address,
+                                   "HTTP Path ACL") == AST_SENSE_DENY) {
+                                       ast_debug(2, "HTTP request for uri '%s' from %s denied by acl by restriction on '%s'\n",
+                                               uri, ast_sockaddr_stringify(&ser->remote_address), restriction->path);
+                                       denied = -1;
+                                       break;
+                               }
+                       }
+               }
+       }
+       AST_RWLIST_UNLOCK(&restrictions);
+
+       return denied;
+}
+
 /*!
  * \brief Add a new URI redirect
  * The entries in the redirect list are sorted by length, just like the list
@@ -2484,10 +2535,14 @@ static int __ast_http_load(int reload)
        char newprefix[MAX_PREFIX] = "";
        char server_name[MAX_SERVER_NAME_LENGTH];
        struct http_uri_redirect *redirect;
+       struct http_restriction *restriction;
+       struct http_restriction_list new_restrictions = AST_LIST_HEAD_NOLOCK_INIT_VALUE;
+       struct http_restriction_list old_restrictions = AST_LIST_HEAD_NOLOCK_INIT_VALUE;
        struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
        uint32_t bindport = DEFAULT_PORT;
        int http_tls_was_enabled = 0;
-       char *bindaddr = NULL;
+       const char *bindaddr = NULL;
+       const char *cat = NULL;
 
        cfg = ast_config_load2("http.conf", "http", config_flags);
        if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
@@ -2602,6 +2657,61 @@ static int __ast_http_load(int reload)
                }
        }
 
+       while ((cat = ast_category_browse(cfg, cat))) {
+               const char *type;
+               struct http_restriction *new_restriction;
+               struct ast_acl_list *acl = NULL;
+               int acl_error = 0;
+               int acl_subscription_flag = 0;
+
+               if (strcasecmp(cat, "general") == 0) {
+                       continue;
+               }
+
+               type = ast_variable_retrieve(cfg, cat, "type");
+               if (!type || strcasecmp(type, "restriction") != 0) {
+                       continue;
+               }
+
+               new_restriction = ast_calloc(1, sizeof(*new_restriction) + strlen(cat) + 1);
+               if (!new_restriction) {
+                       continue;
+               }
+
+               /* Safe */
+               strcpy(new_restriction->path, cat);
+
+               /* Parse ACL options for this restriction */
+               for (v = ast_variable_browse(cfg, cat); v; v = v->next) {
+                       if (!strcasecmp(v->name, "permit") ||
+                               !strcasecmp(v->name, "deny") ||
+                               !strcasecmp(v->name, "acl")) {
+                               ast_append_acl(v->name, v->value, &acl, &acl_error, &acl_subscription_flag);
+                               if (acl_error) {
+                                       ast_log(LOG_ERROR, "Bad ACL '%s' at line '%d' of http.conf for restriction '%s'\n",
+                                               v->value, v->lineno, cat);
+                               }
+                       }
+               }
+
+               new_restriction->acl = acl;
+
+               AST_LIST_INSERT_TAIL(&new_restrictions, new_restriction, entry);
+               ast_debug(2, "HTTP: Added restriction for path '%s'\n", cat);
+       }
+
+       AST_RWLIST_WRLOCK(&restrictions);
+       AST_RWLIST_APPEND_LIST(&old_restrictions, &restrictions, entry);
+       AST_RWLIST_APPEND_LIST(&restrictions, &new_restrictions, entry);
+       AST_RWLIST_UNLOCK(&restrictions);
+
+       while ((restriction = AST_LIST_REMOVE_HEAD(&old_restrictions, entry))) {
+               if (restriction->acl) {
+                       ast_free_acl_list(restriction->acl);
+               }
+               ast_free(restriction);
+       }
+
        ast_config_destroy(cfg);
 
        if (strcmp(prefix, newprefix)) {
@@ -2711,13 +2821,31 @@ static char *handle_show_http(struct ast_cli_entry *e, int cmd, struct ast_cli_a
 
        ast_cli(a->fd, "\nEnabled Redirects:\n");
        AST_RWLIST_RDLOCK(&uri_redirects);
-       AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry)
-               ast_cli(a->fd, "  %s => %s\n", redirect->target, redirect->dest);
        if (AST_RWLIST_EMPTY(&uri_redirects)) {
                ast_cli(a->fd, "  None.\n");
+       } else {
+               AST_RWLIST_TRAVERSE(&uri_redirects, redirect, entry)
+                       ast_cli(a->fd, "  %s => %s\n", redirect->target, redirect->dest);
        }
        AST_RWLIST_UNLOCK(&uri_redirects);
 
+       ast_cli(a->fd, "\nPath Restrictions:\n");
+       AST_RWLIST_RDLOCK(&restrictions);
+       if (AST_RWLIST_EMPTY(&restrictions)) {
+               ast_cli(a->fd, "  None.\n");
+       } else {
+               struct http_restriction *restriction;
+               AST_RWLIST_TRAVERSE(&restrictions, restriction, entry) {
+                       ast_cli(a->fd, "  Path: %s\n", restriction->path);
+                       if (restriction->acl && !ast_acl_list_is_empty(restriction->acl)) {
+                               ast_acl_output(a->fd, restriction->acl, "    ");
+                       } else {
+                               ast_cli(a->fd, "    No ACL configured\n");
+                       }
+               }
+       }
+       AST_RWLIST_UNLOCK(&restrictions);
+
        return CLI_SUCCESS;
 }
 
@@ -2733,6 +2861,7 @@ static struct ast_cli_entry cli_http[] = {
 static int unload_module(void)
 {
        struct http_uri_redirect *redirect;
+       struct http_restriction *restriction;
        ast_cli_unregister_multiple(cli_http, ARRAY_LEN(cli_http));
 
        ao2_cleanup(global_http_server);
@@ -2760,6 +2889,15 @@ static int unload_module(void)
        }
        AST_RWLIST_UNLOCK(&uri_redirects);
 
+       AST_RWLIST_WRLOCK(&restrictions);
+       while ((restriction = AST_RWLIST_REMOVE_HEAD(&restrictions, entry))) {
+               if (restriction->acl) {
+                       ast_free_acl_list(restriction->acl);
+               }
+               ast_free(restriction);
+       }
+       AST_RWLIST_UNLOCK(&restrictions);
+
        return 0;
 }
 
index f897ecb0f34f9312cd989931cb5bd41495b63a47..dd7e54cdfa28e6f010ed7207556fd1ea565a67a6 100644 (file)
                                        </since>
                                        <synopsis>password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext</synopsis>
                                </configOption>
+                               <configOption name="acl">
+                                       <since>
+                                               <version>20.19.0</version>
+                                               <version>22.9.0</version>
+                                               <version>23.3.0</version>
+                                       </since>
+                                       <synopsis>List of IP ACL section names in acl.conf</synopsis>
+                                       <description><para>
+                                               This matches sections configured in <literal>acl.conf</literal>.
+                                       </para></description>
+                               </configOption>
+                               <configOption name="deny">
+                                       <since>
+                                               <version>20.19.0</version>
+                                               <version>22.9.0</version>
+                                               <version>23.3.0</version>
+                                       </since>
+                                       <synopsis>List of IP addresses to deny access from</synopsis>
+                                       <description><para>
+                                               The value is a comma-delimited list of IP addresses. IP addresses may
+                                               have a subnet mask appended. The subnet mask may be written in either
+                                               CIDR or dotted-decimal notation. Separate the IP address and subnet
+                                               mask with a slash ('/')
+                                       </para></description>
+                               </configOption>
+                               <configOption name="permit">
+                                       <since>
+                                               <version>20.19.0</version>
+                                               <version>22.9.0</version>
+                                               <version>23.3.0</version>
+                                       </since>
+                                       <synopsis>List of IP addresses to permit access from</synopsis>
+                                       <description><para>
+                                               The value is a comma-delimited list of IP addresses. IP addresses may
+                                               have a subnet mask appended. The subnet mask may be written in either
+                                               CIDR or dotted-decimal notation. Separate the IP address and subnet
+                                               mask with a slash ('/')
+                                       </para></description>
+                               </configOption>
                        </configObject>
                        <configObject name="outbound_websocket">
                                <since>
index 30c5f45c4af65e702d7bd864e448c4a52924a719..c548e7a52e960cb09a631269879f3829316c32b9 100644 (file)
@@ -78,8 +78,9 @@ static int show_users_cb(void *obj, void *arg, int flags)
        struct ari_conf_user *user = obj;
        struct ast_cli_args *a = arg;
 
-       ast_cli(a->fd, "%-4s  %s\n",
+       ast_cli(a->fd, "%-4s  %-4s  %s\n",
                AST_CLI_YESNO(user->read_only),
+               AST_CLI_YESNO(user->acl && !ast_acl_list_is_empty(user->acl)),
                ast_sorcery_object_get_id(user));
        return 0;
 }
@@ -112,8 +113,8 @@ static char *ari_show_users(struct ast_cli_entry *e, int cmd,
                return CLI_FAILURE;
        }
 
-       ast_cli(a->fd, "r/o?  Username\n");
-       ast_cli(a->fd, "----  --------\n");
+       ast_cli(a->fd, "r/o?  ACL?  Username\n");
+       ast_cli(a->fd, "----  ----  --------\n");
 
        ao2_callback(users, OBJ_NODATA, show_users_cb, a);
 
@@ -173,6 +174,10 @@ static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args
 
        ast_cli(a->fd, "Username: %s\n", ast_sorcery_object_get_id(user));
        ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only));
+       ast_cli(a->fd, "ACL?: %s\n", AST_CLI_YESNO(user->acl && !ast_acl_list_is_empty(user->acl)));
+       if (!ast_acl_list_is_empty(user->acl)) {
+               ast_acl_output(a->fd, user->acl, NULL);
+       }
 
        return CLI_SUCCESS;
 }
index 56fe4fc411623a574078f1b618a6e7f7d869a5aa..6575ef7a66624270fe372918f02f533238c01558 100644 (file)
@@ -505,6 +505,7 @@ static void user_dtor(void *obj)
 {
        struct ari_conf_user *user = obj;
        ast_string_field_free_memory(user);
+       user->acl = ast_free_acl_list(user->acl);
        ast_debug(3, "%s: Disposing of user\n", ast_sorcery_object_get_id(user));
 }
 
@@ -558,6 +559,23 @@ static int user_password_format_from_str(const struct aco_option *opt,
        return 0;
 }
 
+/*! \brief Handler for user ACL options */
+static int user_acl_handler(const struct aco_option *opt,
+       struct ast_variable *var, void *obj)
+{
+       struct ari_conf_user *user = obj;
+       int error = 0;
+       int ignore;
+
+       ast_append_acl(var->name, var->value, &user->acl, &error, &ignore);
+       if (error) {
+               ast_log(LOG_ERROR, "Bad ACL '%s' at line '%d' of ari.conf\n",
+                       var->value, var->lineno);
+       }
+
+       return error;
+}
+
 static int user_password_format_to_str(const void *obj, const intptr_t *args, char **buf)
 {
        const struct ari_conf_user *user = obj;
@@ -729,6 +747,9 @@ static int ari_conf_init(void)
        ast_sorcery_register_sf(user, ari_conf_user, password, password, "");
        ast_sorcery_register_bool(user, ari_conf_user, read_only, read_only, "no");
        ast_sorcery_register_cust(user, password_format, "plain");
+       ast_sorcery_object_field_register_custom(sorcery, "user", "permit", "", user_acl_handler, NULL, NULL, 0, 0);
+       ast_sorcery_object_field_register_custom(sorcery, "user", "deny", "", user_acl_handler, NULL, NULL, 0, 0);
+       ast_sorcery_object_field_register_custom(sorcery, "user", "acl", "", user_acl_handler, NULL, NULL, 0, 0);
 
        ast_sorcery_object_field_register(sorcery, "outbound_websocket", "type", "", OPT_NOOP_T, 0, 0);
        ast_sorcery_register_cust(outbound_websocket, websocket_client_id, "");
index 2a5850f468547a3071fb16223336e1ee85fd8d8d..abc3381cd07a2bc58d3dc6cbb1a9ffe2ea450aa9 100644 (file)
@@ -25,6 +25,7 @@
  * \author David M. Lee, II <dlee@digium.com>
  */
 
+#include "asterisk/acl.h"
 #include "asterisk/http.h"
 #include "asterisk/json.h"
 #include "asterisk/md5.h"
@@ -91,6 +92,8 @@ struct ari_conf_user {
        enum ari_user_password_format password_format;
        /*! If true, user cannot execute change operations */
        int read_only;
+       /*! ACL setting */
+       struct ast_acl_list *acl;
 };
 
 enum ari_conf_owc_fields {
index cb0a7248df7f98b26cab046b0e87538298a6cf40..c38451be6ff384490b43526dd98141cc7d0a5b8d 100644 (file)
@@ -575,6 +575,11 @@ enum ast_ari_invoke_result ast_ari_invoke(struct ast_tcptls_session_instance *se
                        general->auth_realm);
                SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n",
                        response->response_code, response->response_text);
+       } else if (user && user->acl && !ast_acl_list_is_empty(user->acl) &&
+                  ast_apply_acl(user->acl, &ser->remote_address, "ARI User ACL") == AST_SENSE_DENY) {
+               ast_ari_response_error(response, 403, "Forbidden", "Access denied by ACL");
+               SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CONTINUE, "Response: %d : %s\n",
+                       response->response_code, response->response_text);
        } else if (!ast_fully_booted) {
                ast_ari_response_error(response, 503, "Service Unavailable", "Asterisk not booted");
                SCOPE_EXIT_RTN_VALUE(ARI_INVOKE_RESULT_ERROR_CLOSE, "Response: %d : %s\n",