Create Virtual Host
- 🐧 Linux Command Line
Ansible Playbook
Core
Create Linux User/Group and Web Root
groupadd aaa && useradd -g aaa -s /sbin/nologin aaa
mkdir -p /home/wwwroot/aaa.com/{public,tmp}
chown -R aaa:www /home/wwwroot/aaa.com
chmod -R 750 /home/wwwroot/aaa.com
mkdir -p /home/wwwlogs/aaa.com/
chown -R aaa:www /home/wwwlogs/aaa.com
Create Nginx Configuration
/usr/local/nginx/conf/vhosts/aaa.com.conf
server {
server_tokens off;
listen 80;
server_name aaa.com www.aaa.com;
root /home/wwwroot/aaa.com/public;
index index.php index.html index.htm;
access_log /var/log/nginx/access_for_fail2ban.log combined if=$fail2banlog;
access_log /home/wwwlogs/aaa.com/access.log;
error_log /home/wwwlogs/aaa.com/error.log;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|otf|eot)$ {
sendfile on;
tcp_nopush on;
add_header Cache-Control "public, max-age=31536000";
access_log off;
}
location / {
limit_req zone=req_limit_20 burst=50;
limit_conn conn_limit 15;
try_files $uri $uri/ /index.php?$args;
more_set_headers 'Cache-Control "no-cache, max-age=30, must-revalidate"';
}
location ~ \.php$ {
limit_req zone=req_limit_10 burst=30;
limit_conn conn_limit 5;
sendfile off;
tcp_nopush off;
if ($request_method !~ ^(HEAD|OPTIONS|GET|POST|PUT|PATCH|DELETE)$ ) {
return 405;
}
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm-aaa.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
more_set_headers 'Cache-Control "no-store"';
more_clear_headers 'Last-Modified' 'ETag';
}
location ~ /\.(ht|git|svn|vscode|DS_Store|idea|env|project|settings|history) {
deny all;
}
}
touch /home/wwwlogs/aaa.com/access.log
touch /home/wwwlogs/aaa.com/error.log
Create PHP-FPM Configuration
/usr/local/php/etc/php-fpm.d/aaa.conf
[aaa]
user = aaa
group = aaa
listen = /run/php-fpm-aaa.sock
listen.owner = aaa
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[session.save_path] = /home/wwwroot/aaa.com/tmp
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /home/wwwlogs/aaa.com/php-error.log
php_admin_value[open_basedir] = /home/wwwroot/aaa.com/:/tmp:/var/tmp/
security.limit_extensions = .php
Create Database User and Database
mariadb -uroot
CREATE DATABASE aaa_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'aaa_user'@'localhost' IDENTIFIED BY 'aaa_db_password';
GRANT
SELECT, INSERT, UPDATE, DELETE,
CREATE, ALTER, INDEX, DROP,
CREATE TEMPORARY TABLES, SHOW VIEW,
CREATE ROUTINE, ALTER ROUTINE, EXECUTE,
CREATE VIEW, EVENT, TRIGGER,
LOCK TABLES, REFERENCES
ON aaa_db.* TO 'aaa_user'@'localhost';
FLUSH PRIVILEGES;
Optional
Enable SSL in Nginx Virtual Host
server {
listen 443 ssl;
http2 on;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
}
Deny access to sensitive files
server {
location ~* ^/wp-content/uploads/.*\.php$ { deny all; }
location ~* ^/wp-content/cache/.*\.php$ { deny all; }
location ~* ^/wp-content/backup/.*\.php$ { deny all; }
}
Restrict Access to Admin Area
server {
location /admin {
allow 192.168.1.0/24;
deny all;
}
}
Extended
Explanations of Nginx Settings
# Prevent nginx version information from being displayed
server_tokens off;
# Reduce resource consumption by idle connections, improving server performance
keepalive_timeout 15s;
# Maximum time to wait for the client to send the complete request header, returning 408 on failure. This may interrupt access for slower clients or users with low network speeds.
client_header_timeout 10s;
# Prevent server resources from being occupied by slow clients. For clients with slower networks, this may cause response transmission failures.
send_timeout 10s;
# Set the timeout for reading the client's request body to prevent slow attacks (e.g., Slowloris).
client_body_timeout 10s;
# For users needing to upload large files or with slower networks, this may cause request failures. However, with client_max_body_size set to 8m, the request body timeout for large file uploads can be extended appropriately, for example, by overriding this setting in a specific location block.
# Prevent buffer overflow attacks, limiting the ability to upload large files.
client_max_body_size 10m;
# Buffer size for receiving the client's request body, reducing memory consumption.
client_body_buffer_size 1K;
# Buffer size for receiving the client's request header, preventing buffer overflow from large request headers.
client_header_buffer_size 1k;
# Allocate double the buffer size for very large client request headers.
large_client_header_buffers 2 1k;
# Disable outdated protocols (e.g., SSLv3, TLS 1.0, TLS 1.1).
ssl_protocols TLSv1.2 TLSv1.3;
# Use strong cipher suites.
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384;
# Prefer the server's cipher suite order when selecting an encryption method.
ssl_prefer_server_ciphers on;
# Reuse previous SSL sessions to reduce handshake frequency.
ssl_session_timeout 1d;
# Cache SSL sessions in memory; 10M can store approximately 10,000 sessions. The name le_nginx_SSL can be customized.
ssl_session_cache shared:le_nginx_SSL:10m;
# Disable SSL session tickets to prevent session reuse attacks.
ssl_session_tickets off;
# Reduce certificate validation latency by verifying validity, slightly increasing server load.
ssl_stapling on;
ssl_stapling_verify on;
# Add HSTS header to enforce HTTPS for one year, preventing downgrade attacks.
# If your website or subdomains need to use HTTP in the future, they will be inaccessible.
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
# Prevent embedding in iframes (clickjacking attacks).
# If your business needs to embed third-party pages, this may be restricted.
more_set_headers "X-Frame-Options: SAMEORIGIN";
# Enable browser XSS filtering.
more_set_headers "X-XSS-Protection: 1; mode=block";
# Only allow loading resources from the same origin, affecting the loading of JS libraries from other domains.
more_set_headers "Content-Security-Policy: default-src 'self';";
# Prevent the browser from guessing file types (e.g., .txt being parsed as .html).
more_set_headers "X-Content-Type-Options: nosniff";
Nginx Header Examples
A site with no external resources, allow inline scripts and styles, allow form actions to the same origin.
more_set_headers "Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'";
A site with external resources, allow scripts from the same origin and from https://aaa.com, allow styles from the same origin and from https://bbb.com, allow images from the same origin and from https://ddd.com, allow fonts from the same origin and from https://ccc.com, allow form actions to the same origin, disallow iframes, allow ajax connections to the same origin.
more_set_headers "Content-Security-Policy: default-src 'none'; base-uri 'self'; object-src 'none'; script-src 'self' https://aaa.com 'unsafe-inline'; style-src 'self' https://bbb.com; img-src 'self' data: https://ddd.com; font-src 'self' https://ccc.com; form-action 'self'; frame-ancestors 'none'; connect-src 'self';";
Disable unnecessary features
more_set_headers "Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()";
Check Security Headers
headers-more-nginx-module documentation
create-vhost.yml
- name: Create Virtual Host
hosts: all
remote_user: root
vars:
domain: "aaa.com" # Primary domain
additional_domains: ["www.aaa.com"] # Additional domains (can be multiple)
web_user: "aaa" # Website user
web_group: "aaa" # Website group
nginx_group: "www" # Nginx group
php_fpm_socket: "/run/php-fpm-{{ web_user }}.sock" # PHP-FPM socket path
web_root: "/home/wwwroot/{{ domain }}" # Website root directory
log_dir: "/home/wwwlogs/{{ domain }}" # Log directory
tasks:
- name: Ensure the group for website user exists
group:
name: "{{ web_group }}"
state: present
- name: Ensure the user for website exists
user:
name: "{{ web_user }}"
group: "{{ web_group }}"
shell: /sbin/nologin
create_home: false
state: present
- name: Create necessary directories
file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop:
- {path: "{{ web_root }}", owner: "{{ web_user }}", group: "{{ nginx_group }}", mode: "0750"}
- {path: "{{ web_root }}/public", owner: "{{ web_user }}", group: "{{ nginx_group }}", mode: "0750"}
- {path: "{{ web_root }}/tmp", owner: "{{ web_user }}", group: "{{ nginx_group }}", mode: "0750"}
- {path: "{{ log_dir }}", owner: "{{ web_user }}", group: "{{ nginx_group }}", mode: "0755"}
- name: Create necessary log files
file:
path: "{{ item }}"
state: touch
owner: "{{ web_user }}"
group: "{{ nginx_group }}"
mode: "0640"
loop:
- "{{ log_dir }}/php-error.log"
- "{{ log_dir }}/access.log"
- "{{ log_dir }}/error.log"
- name: Deploy Nginx virtual host configuration
copy:
dest: "/usr/local/nginx/conf/vhosts/{{ domain }}.conf"
owner: "root"
group: "root"
mode: "0644"
content: |
server {
server_tokens off;
listen 80;
server_name {{ domain }} {% for subdomain in additional_domains %} {{ subdomain }} {% endfor %};
root {{ web_root }}/public;
index index.php index.html index.htm;
access_log /var/log/nginx/access_for_fail2ban.log combined if=$fail2banlog;
access_log {{ log_dir }}/access.log;
error_log {{ log_dir }}/error.log;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|otf|eot)$ {
sendfile on;
tcp_nopush on;
expires 2d;
access_log off;
}
location / {
limit_req zone=req_limit_20 burst=50;
try_files $uri $uri/ /index.php?$args;
expires -1;
more_set_headers 'Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"';
}
location ~ \.php$ {
limit_req zone=req_limit_10 burst=30;
sendfile off;
tcp_nopush off;
if ($request_method !~ ^(HEAD|OPTIONS|GET|POST|PUT|PATCH|DELETE)$ ) {
return 405;
}
include fastcgi_params;
fastcgi_pass unix:{{ php_fpm_socket }};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* ^/wp-content/uploads/.*\.php$ { deny all; }
location ~* ^/wp-content/cache/.*\.php$ { deny all; }
location ~* ^/wp-content/backup/.*\.php$ { deny all; }
location ~ /\.(ht|git|svn|vscode|DS_Store|idea|env|project|settings|history) {
deny all;
}
}
- name: Deploy PHP-FPM configuration
copy:
dest: "/usr/local/php/etc/php-fpm.d/{{ domain }}.conf"
owner: "root"
group: "root"
mode: "0644"
content: |
[{{ web_user }}]
user = {{ web_user }}
group = {{ web_group }}
listen = {{ php_fpm_socket }}
listen.owner = {{ web_user }}
listen.group = {{ nginx_group }}
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[session.save_path] = {{ web_root }}/tmp
php_admin_flag[log_errors] = on
php_admin_value[error_log] = {{ log_dir }}/php-error.log
php_admin_value[open_basedir] = {{ web_root }}:/tmp:/var/tmp/
security.limit_extensions = .php
- name: Restart PHP-FPM
systemd:
name: php-fpm
state: restarted
- name: Reload Nginx
systemd:
name: nginx
state: reloaded