This repository contains an Ansible role for adding user accounts to Debian servers.
Lots of documentation needs to be added to this file, what follows probably
isn't up to date, see the releases
for updates and changes, do not use the master
branch for production, it is
used for development.
For an example of a users YAML dictionary see the users dictionary for the development server that is used for testing this role.
By default this role will update users defined in the users
dictionary for
whom their YAML dictionary has changed (see the defaults),
when their state is either "present" or "absent", if their state is set to
"ignore" then the account will simply be ignored.
The reason for this is since running all the tasks in this role for all the users takes a long time and usually runs a lot of tasks that won't make changes.
In order to keep track of the users state, on the server (so that updates can
be applied from different places), YAML files for each user are written to
sub-directories of /root/users
called, current
, previous
and proposed
.
By setting the users_update_strategy
variable to "all" (rather than the
default of "changed") all, rather than only users with a changed users
dictionary, will be updated.
Alternative update strategies can be specified by setting the
users_update_strategy
variable to a few optional values as explained below.
ansible-playbook wsh.yml --extra-vars "users_update_strategy=all"
ansible-playbook users.yml --extra-vars "users_update_strategy=check"
This is the default:
ansible-playbook users.yml --extra-vars "users_update_strategy=changed"
ansible-playbook users.yml --extra-vars "users_update_strategy=apache"
ansible-playbook users.yml --extra-vars "users_update_strategy=mariadb"
ansible-playbook users.yml --extra-vars "users_update_strategy=matomo"
ansible-playbook users.yml --extra-vars "users_update_strategy=quotas"
ansible-playbook users.yml --extra-vars "users_update_strategy=phpfpm"
ansible-playbook users.yml --extra-vars "users_update_strategy=sshkeys"
Update all users which have users_groups
defined:
ansible-playbook users.yml --extra-vars "users_update_strategy=groups"
To use this role you need to use Ansible Galaxy to install it into another
repository under galaxy/roles/users
by adding a requirements.yml
file in
that repo that contains:
---
- name: users
src: https://git.coop/webarch/users.git
version: master
scm: git
If you want to use any of the quota_
variables then you also need to include
the quota role and make sure that quota_dir
is set to a mount point for a partition, for example have a seperate /home
partition.
---
- name: quota
src: https://git.coop/webarch/quota.git
version: master
scm: git
And a ansible.cfg
that contains:
[defaults]
retry_files_enabled = False
pipelining = True
inventory = hosts.yml
roles_path = galaxy/roles
And a .gitignore
containing:
galaxy/roles
To pull this repo in run:
ansible-galaxy install -r requirements.yml --force
The other repo should also contain a users.yml
file that contains:
---
- name: Add user accounts
become: yes
vars:
users:
foo:
users_name: Foo Bar
users_email: [email protected]
users_home: /var/www/foo
users_skel: /usr/local/etc/skel.d/www
users_group: users
users_home_owner: root
users_home_group: users
# You must quote the mode or the leaving zero is stripped"
users_home_mode: "0750"
users_system: true
users_shell: /usr/sbin/nologin
users_generate_ssh_key: true
users_editor: vim
users_groups:
- users
- ssl-cert
users_group_members:
- www-data
bar:
users_home: /opt/bar
users_shell: /bin/false
users_system: true
users_quota: 1G
baz:
users_groups:
- staff
- users
users_editor: vim
users_quota_block_softlimit: 1908874
users_quota_block_hardlimit: 2097152
users_quota_inode_softlimit: 9532
users_quota_inode_hardlimit: 10484
chris:
users_groups:
- sudo
- operator
users_editor: vim
users_ssh_public_keys:
- https://git.coop/chris.keys
fred:
users_state: absent
hosts:
- users_servers
roles:
- users
And a hosts.yml
file that contains lists of servers, for example:
---
all:
children:
users_servers:
hosts:
host3.example.org:
host4.example.org:
cloud.example.com:
cloud.example.org:
cloud.example.net:
Then it can be run as follows:
ansible-playbook users.yml
If users_update_strategy: check
, for example on the command line using
--extra-vars "users_update_strategy=true"
then no changes will be made other
than to generate /root/users/proposed/*.yml
state files.
If users_domain_check
is set to strict
then if a domain name doesn't
resolve to the servers IP address then the tasks will stop rather than just
warn.
So, for example:
ansible-galaxy install -r requirements.yml --force && \
ansible-playbook users.yml --extra-vars "users_update_strategy=check"
The boolean variable users_notify_passwd
can be used to trigger the setting and sending of a SSH password to the users_email
address, the password will only be sent one and the date it was sent and the email address it was sent to is recorded in a ~/.notify_passwd
file, this is also used to ensure that a email is not sent twice -- if the file exists a new password will not be sent, to trigger the sending of a new password delete the ~/.notify_passwd
file.
If users_cron
is defined and true, at a server level then for users that are
in the chroot
group a daily, hourly and minutely cron job is created to run
bash scripts that are created (empty by default) at ~/bin/cron_daily.sh
,
~/bin/cron_hourly.sh
and ~/bin/cron_minutely.sh
in the chroot, for
non-chrooted users a cron job is created to run the same bash scripts.
Tasks can be added to the Bash scripts using the users_daily_scripts
,
users_hourly_scripts
and users_minutely_scripts
arrays at a user level, or
they can be manually added not using Ansible by users, for example to archive
Matomo stats on an hourly basis:
users_hourly_scripts:
- "cd ~/sites/default && php console --no-ansi -qn core:archive --force-all-websites > ~/private/matomo-archive.log"
- "cd ~/sites/default && php console --no-ansi -qn core:run-scheduled-tasks > ~/private/matomo-archive.log"
To run the Nextcloud cron job every minute:
users_minutely_scripts:
- "php --php-ini ~/.php.ini -f ~/sites/cloud/cron.php"
The number of minutes part the hour that the daily and hourly script run at in
set randomly for each user and saved in ~/.cron_min
to ensure that all the
jobs for different users don't run at the same time.
Also some features of the Ansible cron
module
can used used via a users_cron_jobs
array set at the users level, for
example:
users_cron_jobs:
- name: printenv
job: printenv
minute: 2
- name: echo foo
job: echo foo
state: absent
minute: "*/5"
The users_ssh_public_keys
array should be set to a list of one or more URL's
for public keys (eg from GitHub).
All the files at the URL's will be downloaded to files named:
~/.ssh/authorized_keys.d/authorized_keys_0
~/.ssh/authorized_keys.d/authorized_keys_1
~/.ssh/authorized_keys.d/authorized_keys_2
Then the ~/.ssh/authorized_keys.d/authorized_keys_*
files are assembled to
~/.ssh/authorized_keys
, (inless this file name is overridden from the
default, see the users_ssh_authorized_keys_file_name variable) this means if
you want to add additional keys then you can simply add them to this directory,
with a suitable filename, eg ~/.ssh/authorized_keys.d/authorized_keys_extra
.
When users_apache_virtual_hosts_enabled
is not defined for a user or it is
set to True
the Apache config generate for the user will be anabled using
a2ensite
, when users_apache_virtual_hosts_enabled
is set to False
then
a2dissite
will be run for the user.
By default a DocumentRoot
and Directory
set of directives are generated for
each VirtualHost
based on the YAML dictionaries defined by
users_apache_virtual_hosts
. This DocumentRoot
and Directory
will be
omitted if users_apache_vhost_docroot
is set to False
at a VirtualHost
level (prior to version 3.0.0
of this role the users_apache_vhost_docroot
was not a boolean and could be set to a path, however this wasn't used so it
was re-purposed) . Additional Directory
sections can be added using
users_apache_directories
at a VirtualHost
level.
SSH and PHP-FPM users in the chroot
group are chrooted to a a read-only
chroot at /chroots/USER
which has /home/USER
mounted read-write at
/chroots/USER/home/USER
.
Apache, unlike SSH and PHP-FPM, which can have a seperate chroot per user, has
one chroot for the whole server, not one per user or VirtualHost
, however
when combined with suEXEC
which allows the group and user that runs scripts
via CGID or FastCGI to be set via SuexecUserGroup
for CGI applications it is
possible to use the Apache chroot isolate users running CGI from the
environment in which other services are running.
One restriction that suEXEC
has is to only allow CGI to be run in
sub-directories on /var/www
, so to get around this when Apache is chrooted
users home directories are also mounted under /var/www/users
and under the
same path in the Apache chroot and under the same path in the SSH / PHP-FPM
chroots.
The way this role has been designed to implement this is having a read-write
chroot at /chroot
which is then mounted at /chroots/www-data
read-only and
then on top of that /home/USER
is mounted read-write at
/chroots/www-data/home/USER
and also at
/chroots/www-data/var/www/users/USER
.
In addition some other directories are automatically mounted to get it all to work. ### Options
Server wide settings for VirtualHosts
can be set like this (see the commented
out variables in defaults/main.yml):
users_apache_options:
- -Indexes
- +SymlinksIfOwnerMatch
- -MultiViews
- +IncludesNOEXEC
- -ExecCGI
users_apache_index:
- index.php
- index.html
- index.shtml
- index.htm
users_apache_override:
- AuthConfig
- FileInfo
- Indexes
- Limit
- Options=Indexes,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC
- Nonfatal=Override
If these variables ar not set server-wide then the users_apache_type
variable
can be used per VirtualHost
and if it is set to php
then these directives
are used:
Options -Indexes +SymlinksIfOwnerMatch -MultiViews +IncludesNOEXEC -ExecCGI
DirectoryIndex index.php index.html index.htm index.shtml
AllowOverride AuthConfig FileInfo Indexes Limit Options=Indexes,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC Nonfatal=Override
And if users_apache_type
is set to cgi
then these directives are used:
Options -Indexes +SymlinksIfOwnerMatch -MultiViews +IncludesNOEXEC +ExecCGI
DirectoryIndex index.cgi index.pl index.html index.htm index.shtml
AllowOverride AuthConfig FileInfo Indexes Limit Options=ExecCGI,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC Nonfatal=Override
And if users_apache_type
is omitted (or set to a value such as static
) then
these defaults are used:
Options -Indexes +SymlinksIfOwnerMatch -MultiViews +IncludesNOEXEC -ExecCGI
DirectoryIndex index.html index.htm index.shtml
AllowOverride AuthConfig FileInfo Indexes Limit Options=Indexes,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC Nonfatal=Override
In addition there is support for cgi
and fcgi
together with Apache being
chrooted and running suEXEC to ensure the the processes run as the user
specified and have limited access to the host.
Support for enabling several users_apache_type
's is to be implemended and the
variable might be renamed to a more sensible users_apache_handlers
, but for
now the users_apache_type
options are:
cgi
fcgi
php
php+cgi
php+fcgi
static
The optional Apache configuration arrays users_apache_cgi_extensions
and
users_phpfpm_extensions
can be used at a role, VirtualHost
or Directory
level to change the defaults from cgi
and pl
for CGI / FCGI and php
for
PHP-FPM.
The optional Apache configuration boolean users_apache_cgi_extension_match
can be set to false
to enable all files for a user, VirtualHost
or
Directory
to be processed as CGI / FCGI, this is needed some things like a
Munin Master.
The arrays, users_apache_options
, users_apache_index
and
users_apache_override
can also be set by VirtualHost
aand if they are these
overrule the other settings for the DocumentRoot
directory, see the Apache
template for the details.
Basic authentication can be set on the DocumentRoot
directory, for example
for MediaWiki (the VisualEditor
needs access via the localhost
):
users_apache_auth_name: Private
users_apache_auth_type: Basic
users_apache_require:
- valid-user
- ip "{{ ansible_facts.default_ipv4.address }}"
- ip 127.0.0.1
The same users_apache_htauth_users
array is used for the usernames and
passwords as documented below.
The users_apache_env
array can be used to set and remove environmental
variables, at a server or VirtualHost
level, via the
SetEnv
and
the
UnsetEnv
Apache directives, for example:
users_apache_env:
- env: FOO
value: bar
- env: BAZ
set: false
Will generate:
SetEnv FOO bar
UnsetEnv BAZ
The users_apache_set_env_if
array can be used to set env vars, at a server or
VirtualHost
level, using the
SetEnvIf
and the
SetEnvIfNoCase
Apache directives, for example:
users_apache_set_env_if:
- attribute: Host
regex: "^(.*)$"
env: THE_HOST=$1
case: false
Will generate:
SetEnvIfNoCase Host "^(.*)$" THE_HOST=$1
And if case
is omitted or set to True
:
SetEnvIf Host "^(.*)$" THE_HOST=$1
Will generate:
SetEnvIf Host "^(.*)$" THE_HOST=$1
See the Apache SetEnvIf documentation.
The users_apache_headers
array can be used to set Header and RequestHeader directives, at a VirtualHost
level, for example:
users_apache_headers:
- type: request
action: set
expr: "Content-Security-Policy: default-src 'self'"
- type: request
action: setifempty
arg: X-Forwarded-Proto https
- type: request
action: setifempty
arg: X-Forwarded-Host %{THE_HOST}e
The response
header type accepts an optional condition, con
, an action, action
and an expression, expr
, for example
Header {% if header.con is defined %}{{ header.con }} {% endif %}{{ header.action }} {{ header.expr }}
The request
type accepts an action, action
and an argument, arg
:
RequestHeader {{ header.action }} {{ header.arg }}
The users_apache_redirects
array can be used to generate
Redirect
,
RedirectMatch
,
RedirectPermanent
and
RedirectTemp
directives, for example:
users_apache_redirects:
- path: /service
url: http://foo2.example.com/service
status: "301"
- path: /one
url: http://example.com/two
status: permanent
- regex_path: (.*)\.gif$
url: http://other.example.com$1.jpg
# Use a `regex_path` if you want to use RedirectMatch rather than Redirect so that any URL
# on the domain is redirected to a specific page
- regex_path: (.*)
url: http://new.example.com/about
The users_apache_alias
array can be used to generate
Alias
and
AliasMatch
directives, for example:
users_apache_alias:
- url: /image
path: /ftp/pub/image
- url: /icons/
path: /usr/local/apache/icons/
- url_regex: ^/icons(/|$)(.*)
path: /usr/local/apache/icons$1$2
The users_apache_alias
array can also be used to generate
ScriptAlias
and
ScriptAliasMatch
directives, for example:
users_apache_alias:
- url: /munin/static
path: /var/cache/munin/www/static
- script: /munin
path: /usr/lib/munin/cgi/munin-cgi-html
- script: /munin-cgi/munin-cgi-graph
path: /usr/lib/munin/cgi/munin-cgi-graph
The users_apache_rewrite
array can be used to set RewriteCond
and
RewriteRule
directives, for example:
users_apache_rewrite:
- cond: %{HTTP_USER_AGENT} DavClnt
- rule: ^$ /remote.php/webdav/ [L,R=302]
- rule: .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
- rule: ^\.well-known/carddav /remote.php/dav/ [R=301,L]
- rule: ^\.well-known/caldav /remote.php/dav/ [R=301,L]
- rule: ^remote/(.*) remote.php [QSA,L]
- rule: ^(?:build|tests|config|lib|3rdparty|templates)/.* - [R=404,L]
- rule: ^\.well-known/(?!acme-challenge|pki-validation) /index.php [QSA,L]
- rule: ^(?:\.(?!well-known)|autotest|occ|issue|indie|db_|console).* - [R=404,L]
To generate:
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} DavClnt
RewriteRule ^$ /remote.php/webdav/ [L,R=302]
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L]
RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L]
RewriteRule ^remote/(.*) remote.php [QSA,L]
RewriteRule ^(?:build|tests|config|lib|3rdparty|templates)/.* - [R=404,L]
RewriteRule ^\.well-known/(?!acme-challenge|pki-validation) /index.php [QSA,L]
RewriteRule ^(?:\.(?!well-known)|autotest|occ|issue|indie|db_|console).* - [R=404,L]
The users_apache_locations
array can be used to apply HTTP Authentication to
URL paths, for example:
users_apache_locations:
- authname: WordPress Login
location: /wp-login.php
- authname: WordPress Admin
location: /wp-admin/
- authname: WordPress Admin Ajax
location: /wp-admin/admin-ajax.php
authtype: None
require:
- all granted
- authname: WordPress Config
location: /wp-config.php
authtype: None
require:
- all denied
The users_apache_htauth_users
array can be used to set usernames and
passwords, these are written to ~/.htpasswd/
in one file per
users_apache_server_name
, the optional state
variables can be set to absent
to remove users, for example:
users_apache_htauth_users:
- name: foo
password: bar
- name: baz
state: absent
If the authtype
is set to None
then AuthUserFile
isn't set and then a
require
array can be set to do things like only allow a few IP
addresses,
for example:
users_apache_locations:
- authname: Drupal Login
location: /user/login
authtype: None
require:
- ip 10 172.20 192.168.2
- method GET POST
It is also possible to set Redirect
, see the Apache
Documentation:
users_apache_locations:
- location: old-site/
redirect: https://example.org/
An Alias
can also be used in a Location
:
users_apache_locations:
- location: /static
alias: /home/foo/sites/www/staticfiles
And ProxyPass
and ProxyPassReverse
can be used in a Location
, see the
Apache ProxyPass documentation:
users_apache_locations:
- location: /push/
proxy_pass: http://127.0.0.1:7867/
reverse: true
- location: /push/ws
proxy_pass: ws://127.0.0.1:7867/ws
Results in:
<Location "/push/" >
ProxyPass "http://127.0.0.1:7867/"
ProxyPassReverse "http://127.0.0.1:7867/
</Location>
<Location "/push/ws" >
ProxyPass "ws://127.0.0.1:7867/ws"
</Location>
If match
is set rather than location
then a LocationMatch
directive is generated, for example:
users_apache_locations:
- match: ^/$
redirect: https://example.org/welcome/
Results in:
<LocationMatch "^/$">
Redirect https://example.org/welcome/
</LocationMatch>
The users_apache_directories
variable can be used at a VirtualHost
level to
list dictionaries representing Directory
directives, either relative to the
users_sites_dir
path, when they don't start with a /
or using fill paths.
The variables that can be used are the same as the ones for the DocumentRoot
directory apart from users_apache_type
, this can't be used to set the
Directory
type to php
or static
.
Prior to version 3.0.0
of this role users_apache_directories
was used for
an array of directories and when it was used the default DocumentRoot
/
Directory
was omitted, from version 3.0.0
and up a dictionary rather than
an array is used and the default DocumentRoot
/ Directory
can be ommitted
by setting users_apache_vhost_docroot
to False
.
This is handy when some directories are needed for static
content and Alias
and also a proxy, for example:
users_apache_virtual_hosts:
api:
users_apache_type: static
users_apache_server_name: "api.{{ inventory_hostname }}"
users_apache_alias:
- url: /static
path: /home/api/sites/api/staticfiles
- url: /media
path: /home/api/sites/api/media
users_apache_directories:
api/static:
users_apache_options:
- Indexes
api/media:
users_apache_options:
- Indexes
users_apache_proxy_pass:
- path: /static
url: "!"
- path: /media
url: "!"
- path: /
url: http://127.0.0.1:8000/
reverse: true
And this will generate:
Alias "/static" "/home/api/sites/api/staticfiles"
Alias "/media" "/home/api/sites/api/media"
<Directory "/home/api/sites/api/staticfiles">
Options Indexes
AllowOverride None
Require all granted
</Directory>
<Directory "/home/api/sites/api/media">
Options Indexes
AllowOverride None
Require all granted
</Directory>
ProxyPass "/static" "!"
ProxyPass "/media" "!"
ProxyPass "/" "http://127.0.0.1:8000/"
ProxyPassReverse "/" "http://127.0.0.1:8000/"
Prior to version 4.3.1
of this role users_apache_directories
could only be used for directories relative to the users_sites_dir
path, this is still the case when they doesn't start with a /
, but now a full path can also be used, for example:
users_apache_virtual_hosts:
openproject:
users_apache_server_name: "{{ inventory_hostname }}"
users_apache_vhost_docroot: false
users_apache_expires: medium
users_apache_directories:
"/opt/openproject/public":
users_apache_override:
- "None"
users_apache_options:
- "-Indexes"
users_apache_headers:
- type: request
action: setifempty
arg: X-Forwarded-Proto https
users_apache_alias:
- url: /assets
path: /opt/openproject/public/assets
- url: /uploads
path: /opt/openproject/public/uploads
users_apache_locations:
- location: "/"
proxy_pass: http://127.0.0.1:6000/
reverse: true
- location: "/assets"
proxy_pass: "!"
- match: "^/sys"
authname: Local connections only
authtype: None
require:
- local
Will generate:
<IfModule headers_module>
RequestHeader setifempty X-Forwarded-Proto https
Header setifempty Strict-Transport-Security "max-age=155520225;"
Header setifempty Permissions-Policy "interest-cohort=()"
</IfModule>
<Location "/">
# ProxyPass Location so no AuthUserFile
ProxyPass "http://127.0.0.1:6000/"
ProxyPassReverse "http://127.0.0.1:6000/"
</Location>
<Location "/assets">
# ProxyPass Location so no AuthUserFile
ProxyPass "!"
</Location>
<LocationMatch "^/sys">
# No AuthUserFile for this Location as AuthType is None
AuthName "Local connections only"
AuthType "None"
Require local
</LocationMatch>
<Location "/.well-known/acme-challenge">
AuthType "None"
Require all granted
</Location>
<IfModule alias_module>
Alias "/assets" "/opt/openproject/public/assets"
Alias "/uploads" "/opt/openproject/public/uploads"
</IfModule>
<IfFile "/opt/openproject/public">
<Directory "/opt/openproject/public">
Options -Indexes
DirectoryIndex index.html index.htm
AllowOverride None
AuthType "None"
Require all granted
</Directory>
</IfFile>
If no Directories are required use users_apache_directories: []
.
The following variables can be used to control the content of the Directory
directive:
A list of AddOutputFilter directives, for example:
users_apache_add_output_filters:
- INCLUDES;DEFLATE shtml
Will generate:
AddOutputFilter INCLUDES;DEFLATE shtml
A list of MIME types and extensions for AddType, for example:
users_apache_add_type:
- ext: bar
type: text/plain
Will generate:
AddType text/plain .bar
Note that the dot before the file extensions is added automatically.
A AuthName, for example:
users_apache_auth_name: Authentication Required
AuthName Authentication Required
A AuthType, when set to Basic
a AuthUserFile directive is automatically added to point to the htpasswd for for the VirtualHost, for example:
users_apache_auth_type: Basic
AuthType Basic
AuthUserFile: /home/example/.htpasswd/foo
The other options, Digest
and Form
and None
can be used but they don't add any additional directives.
Set users_apache_expires
to one of these values:
- active
- forever
- medium
- strict
For one of the Apache role templates to be added as a IncludeOptional
for the Directory
:
A list ofFilesMatch and Require directives for example:
users_apache_filesmatch:
- regex: "^(?<sitename>[^/]+)"
require:
- "ldap-group cn=%{env:MATCH_SITENAME},ou=combined,o=Example"
<FilesMatch "^(?<sitename>[^/]+)">
Require ldap-group cn=%{env:MATCH_SITENAME},ou=combined,o=Example
</FilesMatch>
If require
is omitted then it will default to Require all denied
.
A values for the HeaderName, for example:
users_apache_header_name: HEADER.html
HeaderName HEADER.html
A list of file names for the DirectoryIndex directive, for example:
users_apache_index:
- index.htm
- index.html
- index.php
DirectoryIndex index.htm index.html index.php
Text for the IndexHeadInsert directive, for example:
users_apache_head_insert: '<link rel=\"sitemap\" href=\"/sitemap.html\">'
IndexHeadInsert '<link rel=\"sitemap\" href=\"/sitemap.html\">'
A list of options for the IndexOptions, for example:
users_apache_index_options:
- +ScanHTMLTitles
- -IconsAreLinks
- FancyIndexing
IndexOptions +ScanHTMLTitles -IconsAreLinks FancyIndexing
A values for the ReadmeName, for example:
users_apache_readme_name: FOOTER.html
ReadmeName FOOTER.html
A list of requirements for the Apache Require directive, for example:
users_apache_require:
- ip 10 172.20 192.168.2
- method http-method GET HEAD
Require ip 10 172.20 192.168.2
Require method http-method GET HEAD
A boolean, enable the SSILegacyExprParser.
A boolean, enable SSILastModified.
Configure a reverse proxy, for example for a Nextcloud notify_push server like this you can specify:
users_apache_proxy_pass:
- path: /push/ws
url: ws://127.0.0.1:7867/ws
- path: /push/
url: http://127.0.0.1:7867/
reverse: true
And this will generate:
ProxyPass "/push/ws" "ws://127.0.0.1:7867/ws"
ProxyPass "/push/" "http://127.0.0.1:7867/"
ProxyPassReverse "/push/" "http://127.0.0.1:7867/"
For a reverse proxy to a Rocket.Chat server installed using snaps:
users_apache_proxy_pass:
- path: /
url: http://127.0.0.1:3000/
rewrite_conditions:
- '%{HTTP:Upgrade} websocket [NC]'
- '%{HTTP:Connection} upgrade [NC]'
rewrite_rules:
- '^/?(.*) "ws://127.0.0.1:3000/$1" [P,L]'
reverse: true
And this will generate:
ProxyPass "/" "http://127.0.0.1:3000/"
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:3000/$1" [P,L]
ProxyPassReverse "/" "http://127.0.0.1:3000/"
For an ONLYOFFICE server:
users_apache_set_env_if:
- attribute: Host
regex: "^(.*)$"
env: THE_HOST=$1
users_apache_headers:
- type: request
action: setifempty
argument: X-Forwarded-Proto https
- type: request
action: setifempty
argument: X-Forwarded-Host %{THE_HOST}e
users_apache_proxy_pass:
- add_headers: false
path: /.well-known
url: !
- pathmatch: (.*)(\/websocket)$
url: "ws://127.0.0.1:8006/$1$2"
- path: /
url: http://127.0.0.1:8006/
reverse: true
A reverse proxy to an applicaton that doesn't have any user authentication can be configured to use HTTP Authentication, for example for Mailcatcher:
users_apache_proxy_pass:
- path: /
url: http://127.0.0.1:1080/
reverse: true
htauth: true
users_apache_htauth_users:
- name: mailcatcher
password: foo
By default the reverse proxy doesn't proxy error documents served from /wsh
:
ProxyErrorOverride On
ProxyPass "/wsh/" "!"
If an users_apache_filesmatch
array specified at the VirtualHost
level with
a list of regex
like this:
users_apache_filesmatch:
- regex: '^license\.txt$'
- regex: '^readme\.html$'
- regex: '^xmlrpc\.php$'
Then access to these files will be denied, unless one of more require
items
are listed, for example:
users_apache_filesmatch:
- regex: '^xmlrpc\.php$'
require:
- method GET HEAD
- ip 9.9.9.9
The optional users_apache_expires
variable can be used to select the
medium
or
strict
configuration to Include
into the VirtualHost
.
The optional users_apache_robots
variable can be set to deny
to Include
the robots
config
and this will also set an Alias
for the
robots.txt
file.
Some specific PHP variables include the users_apache_nophp_dirs
array, this
can be used to list directories that PHP cannot be used in, for example
directories where users can upload files, for example:
users_apache_nophp_dirs:
- wp-content/uploads
In order to enable PHP settings for the CLI to be configured differently for each user this Bash alias is written to ~/.bash_aliases
:
alias php="php --php-ini ~/.php.ini"
And ~/.php.ini
is created with the following three lines (with example
replaced with the username):
sys_temp_dir = "/home/example/tmp"
memory_limit = -1
apc.enable_cli = 1
Setting memory_limit = -1
overides the default of 128M, this is required for the Nextcloud CLI updater.
By default, for users in the phpfpm
group, the PHP socket is created in the $HOME
directory as ~/php-fpm.sock
, the use of this path by the Apache configuratoon can be overridden when Apache is not chrooted and the users is not chrooted, per VirtualHost
by setting users_apache_php_socket_path
to a path to another socket however this variable is not used by this roles PHP tasks, the socket configuration needs to be done by the PHP role.
The optional users_phpfpm_admin_flags
array can be used to set per-pool configuration, for example Nextcloud uses SabreDav so requires the following in the PHP-FPM pool.d
file:
php_admin_flag[always_populate_raw_post_data] = no
php_admin_flag[magic_quotes_gpc] = no
php_admin_flag[mbstring.func_overload] = no
php_admin_flag[output_buffering] = no
This can be achieved by setting the following at a user level:
users_phpfpm_admin_flags:
- name: always_populate_raw_post_data
value: false
- name: magic_quotes_gpc
value: false
- name: mbstring.func_overload
value: false
- name: output_buffering
value: false
If output_buffering
needs to be set to a value other than on / off then this need to be set at the PHP version level rather than a users level.
If a user has a set of variables like this:
users_apache_virtual_hosts:
default:
users_apache_type: php
users_apache_php_socket_path: /run/php/php8.2-fpm.sock
users_apache_nophp_dirs:
- wp-content/uploads
users_apache_server_name: wordpress.example.org
users_apache_server_aliases:
- www.wordpress.example.org
users_cms: wordpress
wordpress_dbname: wordpress_live
users_daily_scripts:
- "wp-update {{ users_basedir }}/wordpress/{{ users_sites_dir }}/default"
users_apache_htauth_locations:
- name: WordPress Config
location: /wp-config.php
type: None
require:
- all denied
users_apache_nophp_dirs:
- wp-content/uploads
users_apache_expires: medium
dev:
users_apache_type: php
users_apache_nophp_dirs:
- wp-content/uploads
users_apache_robots: deny
users_apache_server_name: dev.wordpress.example.org
users_apache_server_aliases:
- www.dev.wordpress.example.org
users_cms: wordpress
wordpress_dbname: wordpress_dev
users_daily_scripts:
- "wp-update {{ users_basedir }}/wordpress/{{ users_sites_dir }}/default"
users_apache_htauth_users:
- name: foo
password: bar
users_apache_htauth_locations:
- name: WordPress Login
location: /wp-login.php
- name: WordPress Config
location: /wp-config.php
type: None
require:
- all denied
users_apache_expires: medium
It will generate an Apache config like this:
# Ansible managed
# wordpress.example.org
# /home/wordpress/sites/default
<VirtualHost *:80>
ServerName wordpress.example.org
ServerAlias www.wordpress.example.org
RedirectMatch 301 ^(?!/\.well-known/acme-challenge/).* https://wordpress.example.org$0
</VirtualHost>
# wordpress.example.org
# /home/wordpress/sites/default
<VirtualHost *:443>
ServerName wordpress.example.org
ServerAlias www.wordpress.example.org
SSLEngine on
SSLCertificateFile /etc/ssl/le/wordpress.wordpress.example.org.cert.pem
SSLCertificateKeyFile /etc/ssl/le/wordpress.wordpress.example.org.key.pem
SSLCertificateChainFile /etc/ssl/le/wordpress.wordpress.example.org.ca.pem
ServerAdmin "[email protected]"
<Location "/wp-config.php" >
# No AuthUserFile for this Location
AuthName "WordPress Config"
AuthType None
Require all denied
</Location>
DocumentRoot "/home/wordpress/sites/default"
<Directory "/home/wordpress/sites/default">
Options -Indexes +SymlinksIfOwnerMatch -MultiViews +IncludesNOEXEC -ExecCGI
DirectoryIndex index.php index.html index.htm index.shtml wsh.shtml
AllowOverride AuthConfig FileInfo Indexes Limit Options=Indexes,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC Nonfatal=Override
<IfModule proxy_fcgi_module>
<IfModule setenvif_module>
SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1
</IfModule>
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:/users/wordpress/home/wordpress/php-fpm.sock|fcgi://localhost"
</If>
</FilesMatch>
</IfModule>
Require all granted
IncludeOptional "/etc/apache2/conf-available/expires-medium.conf"
</Directory>
# No PHP allowed in this directory
<Directory "/home/wordpress/sites/default/wp-content/uploads">
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
Require all denied
</If>
</FilesMatch>
</Directory>
CustomLog /var/log/apache2/wordpress_access.log bandwidth
LogLevel error
ErrorLog /home/wordpress/logs/apache.error.log
CustomLog /home/wordpress/logs/apache.access.log combinedio
</VirtualHost>
# dev.wordpress.example.org
# /home/wordpress/sites/dev
<VirtualHost *:80>
ServerName dev.wordpress.example.org
ServerAlias www.dev.wordpress.example.org
RedirectMatch 301 ^(?!/\.well-known/acme-challenge/).* https://dev.wordpress.example.org$0
</VirtualHost>
# dev.wordpress.example.org
# /home/wordpress/sites/dev
<VirtualHost *:443>
ServerName dev.wordpress.example.org
ServerAlias www.dev.wordpress.example.org
SSLEngine on
SSLCertificateFile /etc/ssl/le/wordpress.wordpress.example.org.cert.pem
SSLCertificateKeyFile /etc/ssl/le/wordpress.wordpress.example.org.key.pem
SSLCertificateChainFile /etc/ssl/le/wordpress.wordpress.example.org.ca.pem
ServerAdmin "[email protected]"
IncludeOptional /etc/apache2/conf-available/robots-deny.conf
<Location "/wp-login.php" >
AuthUserFile "/home/wordpress/.htpasswd/dev.wordpress.example.org"
AuthName "WordPress Login"
AuthType Basic
Require valid-user
</Location>
<Location "/wp-config.php" >
# No AuthUserFile for this Location
AuthName "WordPress Config"
AuthType None
Require all denied
</Location>
DocumentRoot "/home/wordpress/sites/dev"
<Directory "/home/wordpress/sites/dev">
Options -Indexes +SymlinksIfOwnerMatch -MultiViews +IncludesNOEXEC -ExecCGI
DirectoryIndex index.php index.html index.htm index.shtml wsh.shtml
AllowOverride AuthConfig FileInfo Indexes Limit Options=Indexes,SymLinksIfOwnerMatch,MultiViews,IncludesNOEXEC Nonfatal=Override
<IfModule proxy_fcgi_module>
<IfModule setenvif_module>
SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1
</IfModule>
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:/users/wordpress/home/wordpress/php-fpm.sock|fcgi://localhost"
</If>
</FilesMatch>
</IfModule>
Require all granted
IncludeOptional "/etc/apache2/conf-available/expires-medium.conf"
</Directory>
# No PHP allowed in this directory
<Directory "/home/wordpress/sites/dev/wp-content/uploads">
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
Require all denied
</If>
</FilesMatch>
</Directory>
CustomLog /var/log/apache2/wordpress_access.log bandwidth
LogLevel error
ErrorLog /home/wordpress/logs/apache.error.log
CustomLog /home/wordpress/logs/apache.access.log combinedio
</VirtualHost>
# vim: set ft=apache:
-
Generate a CHANGELOG.md from the releases perhaps using Release Exporter
-
Better documentation
-
Add more options from the Ansible user module
Copyright 2018-2025 Chris Croome, <[email protected]>.
This role is released under the same terms as Ansible itself, the GNU GPLv3.