Tags Archives: Apache

Website Not Rendering Correctly (including the WordPress Admin Dashboard)

The problem:


Checking in Chrome DevTools, we get the following output for the Knowledge Base plugin admin page which was not displaying correctly in the Admin Dashboard.


The site itself was also not rendering correctly.


Mixed Content: The page at ‘https://kevwells.com/wp-admin/options-general.php’ was loaded over HTTPS, but requested an insecure element ‘’. This request was not upgraded to HTTPS because its URL’s host is an IP address.
options-general.php:33 GET net::ERR_CERT_COMMON_NAME_INVALID


Note a wordpress plugin for the website is trying to call instead of https://kevwells.com




probably because the site definitions are for the IP address in the wordpress database for the site, instead of for the domain name.


Lets check.


We go into mariadb sql server database for the wordpress website, using the mariadb CLI….


and check the definitions set there:


select the kevwells database, and check the table entry for home and siteurl:


root@ip-172-31-82-94:~# mysql -u root -p
Enter password: * * * * * * (not displayed here)


Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 70292
Server version: 10.6.12-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04


Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.


Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

MariaDB [(none)]>


MariaDB [kevwells]> show databases ;

| Database |
| information_schema |
| kevwells |
| mysql |
| performance_schema |
| phpmyadmin |
| sys |
| wordpress |
7 rows in set (0.000 sec)


MariaDB [kevwells]> use kevwells;
Database changed


and what do we find when we check the definitions for home and siteurl in the wp_options table:


MariaDB [kevwells]> SELECT * from wp_options WHERE option_name = ‘home’ OR option_name = ‘siteurl’;
| option_id | option_name | option_value | autoload |
| 33 | home | | yes |
| 1 | siteurl | | yes |
2 rows in set (0.000 sec)

MariaDB [kevwells]>


This is the cause of the problem: the entries for home and siteurl


so, these definitions need to be set to https://kevwells.com for each


UPDATE wp_options SET option_value = http://www.example.com/blog WHERE option_name = ‘home’ OR option_name = ‘siteurl’;


UPDATE wp_options SET option_value = https://kevwells.com WHERE option_name = ‘home’ OR option_name = ‘siteurl’;


MariaDB [kevwells]> UPDATE wp_options SET option_value = https://kevwells.com WHERE option_name = ‘home’ OR option_name = ‘siteurl’;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘://kevwells.com WHERE option_name = ‘home’ OR option_name = ‘siteurl” at line 1
MariaDB [kevwells]>


slight problem with mariadb vs mysqldb… we have to use different inverted commas for part of the command:


you have to use ” ” :


So it needs to look like this:


UPDATE wp_options SET option_value = “https://kevwells.com” WHERE option_name = ‘home’ OR option_name = ‘siteurl’;


MariaDB [kevwells]> UPDATE wp_options SET option_value = “https://kevwells.com” WHERE option_name = ‘home’ OR option_name = ‘siteurl’;
Query OK, 2 rows affected (0.001 sec)
Rows matched: 2 Changed: 2 Warnings: 0


MariaDB [kevwells]>


now its set correctly.


finally, verify the change:


SELECT * from wp_options WHERE option_name = ‘home’ OR option_name = ‘siteurl’;


MariaDB [kevwells]> SELECT * from wp_options WHERE option_name = ‘home’ OR option_name = ‘siteurl’;

| option_id | option_name | option_value | autoload |
| 33 | home | https://kevwells.com | yes |
| 1 | siteurl | https://kevwells.com | yes |
2 rows in set (0.000 sec)

MariaDB [kevwells]>


all ok.


MariaDB [kevwells]> quit


The site is now displaying correctly.

Continue Reading

LAMP Server Migration

The following notes document the migration of a virtual Linux machine running LAMP – Linux Ubuntu, Apache webserver, MySql/MariaDB & PHP, from an existing machine to a new machine and the procedural steps involved.


Both old and new machines also run ssh, nfs-server shares, sslh port multiplexer, nextcloud, checkmk, wordpress, phpmyadmin, together with Linux shell script crontab-run backup routines to backup data to an external cloud provider.


The scripts and crontab entries had to be migrated, installed and reconfigured on the new server.


This is an overview of the steps involved. For more detailed discussion of the specific actions, for example covering mysql, apache, sslh multiplexer, SSL certificates, see the appropriate relevant subject sections of this IT Knowledge Base.



Initial Preparation


First task is to create the new virtual server with Linux Ubuntu OS. This has a new localhost name and new unique publicly accessible IP address.


After the migration, the old original server machine is then switched off and deleted from the virtual server provider environment. The new machine name is changed to the old machine name, taking the place of the old machine online.


After installing the basic operating system Linux Ubuntu version 20.04 LTS, we migrate existing /usr/local/bin scripts from the old to the new machine, together with any binaries and other files contained in this directory.


Later we will configure crontab on the new machine for backups and mysql nextcloud nightly database scan and updates for newly added data files (and deleted/modified data files).


We give the new machine the temporary localhost name of “gemininew”.


When the migration is complete we will change the name to “gemini”, taking the place of the old machine.


The /etc/hosts file entries on the server and on all connecting hosts will then need to be modified so that the entry for gemini points to the NEW IP address and no longer to the old one.


In the meantime, for ease of logging in, we have made an entry on the old gemini server /etc/hosts and on our connecting hosts as follows:


gemininew  <the new IP address for gemini>


while keeping the old gemini IP entry intact for now. Later on towards the end of the migration this old entry will be deleted from the file.




Modify DNS Record


We need to modify the DNS record for the domain kevwells.com for the new IP address of the new machine to replace the old one.


This is done on the DNS server of our virtual server provider.



Install Apache


Install apache2 on the new machine and define the virtual hosts that are required.




Next, for apache


To allow .htaccess files, we need to set the AllowOverride directive within a Directory block pointing to our document root. Add the following block of text inside the VirtualHost block in your configuration file, making sure to use the correct web root directory.



In my case it is:


root@gemininew:/etc/apache2/sites-available# nano 000-default.conf

add this under

<VirtualHost *:80>


<Directory /var/www/wordpress/>
AllowOverride All


By default the use of .htaccess files is disabled. WordPress and many WordPress plugins use these files extensively for in-directory tweaks to the web server’s behavior.



Next, we can enable mod_rewrite so that we can utilize the WordPress permalink feature:


a2enmod rewrite



root@gemininew:/etc/apache2/sites-available# a2enmod rewrite
Enabling module rewrite.
To activate the new configuration, you need to run:
systemctl restart apache2
root@gemininew:/etc/apache2/sites-available# systemctl restart apache2


This allows you to have more human-readable permalinks to your posts, like the following two examples:




The a2enmod command calls a script that enables the specified module within the Apache configuration.


next, check to make sure we haven’t made any syntax errors by running the following test.


apache2ctl configtest



root@gemininew:/etc/apache2/sites-available# apache2ctl configtest
AH00558: apache2: Could not reliably determine the server’s fully qualified domain name, using Set the ‘ServerName’ directive globally to suppress this message
Syntax OK



Ensure apache is running with


a2ensite <sites-enabled_configfile>


systemctl enable apache2
systemctl start apache2


Install Mysql/MariaDB


apt install mariadb-server


During the installation you’ll be requested to set a mysql server admin user password.


If the secure installation utility does not run automatically during the installation process, then you can run it explicitly:


mysql_secure_installation utility


Next install the php-mysql module:


root@gemininew:~# apt-get install php-mysql
Reading package lists… Done
Building dependency tree
Reading state information… Done
php-mysql is already the newest version (2:7.4+75).



Enable the mysql service and start it:




root@gemini:/var/lib/mysql# systemctl start mysql
root@gemini:/var/lib/mysql# systemctl status mysql
● mysql.service – LSB: Start and stop the mysql database server daemon
Loaded: loaded (/etc/init.d/mysql; generated)
Active: active (running) since Thu 2022-03-03 13:24:50 UTC; 4s ago
Docs: man:systemd-sysv-generator(8)
Process: 4328 ExecStart=/etc/init.d/mysql start (code=exited, status=0/SUCCESS)
Tasks: 33 (limit: 2274)
Memory: 64.0M
CGroup: /system.slice/mysql.service
├─4367 /bin/sh /usr/bin/mysqld_safe
├─4483 /usr/sbin/mysqld –basedir=/usr –datadir=/var/lib/mysql –plugin-dir=/usr/lib/x86_64-linux-gnu/mariadb19/plugin –user=mysql –skip-log-error –pid-file=/run/mysqld/mysqld.pid –socket=/var/run/mysqld/mysqld.sock
└─4484 logger -t mysqld -p daemon error


Mar 03 13:24:50 gemini /etc/mysql/debian-start[4538]: Upgrading MySQL tables if necessary.
Mar 03 13:24:50 gemini mysqld[4484]: 2022-03-03 13:24:50 10 [Warning] Access denied for user ‘debian-sys-maint’@’localhost’ (using password: YES)
Mar 03 13:24:50 gemini /etc/mysql/debian-start[4541]: Looking for ‘mysql’ as: /usr/bin/mysql
Mar 03 13:24:50 gemini /etc/mysql/debian-start[4541]: Reading datadir from the MariaDB server failed. Got the following error when executing the ‘mysql’ command line client
Mar 03 13:24:50 gemini /etc/mysql/debian-start[4541]: ERROR 1045 (28000): Access denied for user ‘debian-sys-maint’@’localhost’ (using password: YES)
Mar 03 13:24:50 gemini /etc/mysql/debian-start[4541]: FATAL ERROR: Upgrade failed
Mar 03 13:24:50 gemini /etc/mysql/debian-start[4546]: Checking for insecure root accounts.
Mar 03 13:24:50 gemini mysqld[4484]: 2022-03-03 13:24:50 11 [Warning] Access denied for user ‘debian-sys-maint’@’localhost’ (using password: YES)
Mar 03 13:24:50 gemini mysql[4549]: ERROR 1045 (28000): Access denied for user ‘debian-sys-maint’@’localhost’ (using password: YES)
Mar 03 13:24:51 gemini mysqld[4484]: 2022-03-03 13:24:51 12 [Warning] Access denied for user ‘wordpressuser’@’localhost’ (using password: YES)
lines 3-22/22 (END)



and we now have the socket for mysqld:


root@gemini:/run/mysqld# ls -lias
total 4
586 0 drwxr-xr-x 2 mysql root 80 Mar 3 13:24 .
2 0 drwxr-xr-x 29 root root 880 Mar 3 13:20 ..
678 4 -rw-rw—- 1 mysql mysql 5 Mar 3 13:24 mysqld.pid
677 0 srwxrwxrwx 1 mysql mysql 0 Mar 3 13:24 mysqld.sock



Test the server:


root@gemininew:~# mysqladmin -p -u root version
Enter password:
mysqladmin Ver 8.0.28-0ubuntu0.20.04.3 for Linux on x86_64 ((Ubuntu))
Copyright (c) 2000, 2022, Oracle and/or its affiliates.


Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective


Server version 8.0.28-0ubuntu0.20.04.3
Protocol version 10
Connection Localhost via UNIX socket
UNIX socket /var/run/mysqld/mysqld.sock
Uptime: 6 min 48 sec


Threads: 2 Questions: 14 Slow queries: 0 Opens: 130 Flush tables: 3 Open tables: 49 Queries per second avg: 0.034




Run the mysql secure installation utility







This will also set the root password for mysql



You have to set the root password here:


root@gemini:/run/mysqld# /usr/bin/mysql_secure_installation




In order to log into MariaDB to secure it, we’ll need the current
password for the root user. If you’ve just installed MariaDB, and
you haven’t set the root password yet, the password will be blank,
so you should just press enter here.


Enter current password for root (enter for none): ***NOTE**** if you try to enter the password for root here it gives an error at this stage:!!! see below
ERROR 1045 (28000): Access denied for user ‘root’@’localhost’ (using password: YES)
Enter current password for root (enter for none): ****PRESS ENTER***- you set the root password further below!***
OK, successfully used password, moving on…


Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.


Set root password? [Y/n] Y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
… Success!


By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them. This is intended only for testing, and to make the installation
go a bit smoother. You should remove them before moving into a
production environment.


Remove anonymous users? [Y/n]
… Success!


Normally, root should only be allowed to connect from ‘localhost’. This
ensures that someone cannot guess at the root password from the network.


Disallow root login remotely? [Y/n]
… Success!


By default, MariaDB comes with a database named ‘test’ that anyone can
access. This is also intended only for testing, and should be removed
before moving into a production environment.


Remove test database and access to it? [Y/n]
– Dropping test database…
… Success!
– Removing privileges on test database…
… Success!


Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.


Reload privilege tables now? [Y/n]
… Success!


Cleaning up…


All done! If you’ve completed all of the above steps, your MariaDB
installation should now be secure.


Thanks for using MariaDB!




root@gemini:~# mysqlshow -u root -p
Enter password:
| Databases |
| gitea |
| information_schema |
| kevwells |
| mysql |
| nextcloud |
| performance_schema |
| phpmyadmin |
| wordpress |


root@gemini:~# mysqldump -u root -p –all-databases > allgeminidatabases-backup.sql
Enter password:



to restore


mysql -u root -p < allgeminidatabases-backup.sql


root@gemininew:~# mysql -u root -p < allgeminidatabases-backup.sql
Enter password:


root@gemininew:~# mysqlshow -u root -p
Enter password:
| Databases |
| gitea |
| information_schema |
| kevwells |
| mysql |
| performance_schema |
| sys |



Set up a Mysql Database for WordPress


Create an exclusive database for WordPress to control. This can have any name, but we will use the standard name wordpress for this example.


Enter the mysql admin client, using:



root@gemininew:~# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 319
Server version: 10.3.34-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04


Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.


Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.


MariaDB [(none)]>


then do the following:


MariaDB [(none)]> CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
Query OK, 1 row affected (0.001 sec)

MariaDB [(none)]> use wordpress;
Database changed
MariaDB [wordpress]>

MariaDB [(none)]> CREATE USER ‘wordpressuser’@localhost IDENTIFIED BY ‘*********’;  
Query OK, 0 rows affected (0.000 sec)


MariaDB [(none)]> GRANT ALL ON wordpress.* TO ‘wordpressuser’@’localhost’;
Query OK, 0 rows affected (0.001 sec)


Query OK, 0 rows affected (0.001 sec)


MariaDB [(none)]>



you can now exit mariadb:


MariaDB [(none)]> exit



Install WordPress


next, download and install wordpress:


you can do a wget from https://wordpress.org/latest.tar.gz


and then unpack at /var/www/wordpress


you can then create a /var/www/html -> /var/www/wordpress link


and set permissions as follows:


chown -R www-data:www-data /var/www/wordpress


Next we’ll run two find commands to set the correct permissions on the WordPress directories and files:


root@gemininew:~# find /var/www/wordpress/ -type d -exec chmod 750 {} \;
root@gemininew:~# find /var/www/wordpress/ -type f -exec chmod 640 {} \;


Next, modify some of the database connection settings, adjusting database name, the database user, and the associated password you configured within MySQL.


Also set the method WordPress should use to write to the filesystem, set this filesystem method to “direct”.


If we dont do this it could mean WordPress prompting for FTP credentials for some actions:



. . .

// ** MySQL settings – You can get this info from your web host ** //
/** The name of the database for WordPress */
define( ‘DB_NAME’, ‘wordpress’ );


/** MySQL database username */
define( ‘DB_USER’, ‘wordpressuser’ );


/** MySQL database password */
define( ‘DB_PASSWORD’, ‘***password commented out***’ );


/** MySQL hostname */
define( ‘DB_HOST’, ‘localhost’ );


/** Database Charset to use in creating database tables. */
define( ‘DB_CHARSET’, ‘utf8’ );


/** The Database Collate type. Don’t change this if in doubt. */
define( ‘DB_COLLATE’, ” );


. . .

define(‘FS_METHOD’, ‘direct’);



NOTE: we substitute the above with our own password as created for the wordpressuser in mariadb! (not shown here for security reasons)


next, in web-browser, open the http://servername/wp-admin link


and the wordpress admin dashboard initial setup page should appear.




Firewalling for apache


The following ports need to be opened, port 443 and 80.


root@gemini:/etc/apache2# ufw status
Status: active


To Action From
— —— —-
22 ALLOW Anywhere
80 ALLOW Anywhere
443 ALLOW Anywhere
3306 ALLOW Anywhere
9993 ALLOW Anywhere
2049 ALLOW Anywhere
22 (v6) ALLOW Anywhere (v6)
80 (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)
3306 (v6) ALLOW Anywhere (v6)
9993 (v6) ALLOW Anywhere (v6)
2049 (v6) ALLOW Anywhere (v6)




DO NOT open port 444!


NOTE that port 444 is NOT opened on the firewall – this is used internally by apache ie behind the firewall, between apache and sslh. It should not be accessible from outside.



MySql/MariaDB has to be installed and up and running.


Check it is enabled and running using


systemctl enable mysql
systemctl start mysql
systemctl status mysql


During installation, all being well, mysql/mariadb will start automatically, unless the port used by mysql is already in use.






Install SSL Certificates


SSL/TLS https certificates supplied by Lets Encrypt


The apache webserver needs to be running on port 80 to carry out registration to obtain the certificates for each virtual host.


Certificates are required for all virtual websites operating on the IP with https:


kevwells.com, nextcloud.kevwells.com


Use the certbot command-line utility to request the SSL certificates for the websites. You need to run certbot in turn for each virtual host website you are operating on https.


Make sure /etc/apache2/ports.conf is set to listen on port 80 and 443 initially for ssl.


After obtaining the certificates, and since we are using the sslh multiplexer, you need to change ports.conf all references to Listen 443 port to Listen 444 as this is the port used for sslh multiplexer.


The sslh multiplexer separates out incoming traffic on 443 to 444 for Apache and 22 for ssh.


Install wordpress under /var/www/wordpress


then create symbolic links as follows:


/var/www/html -> /var/www/kevwells.com


/var/www/kevwells.com -> /var/www/wordpress


Configure the wp-config.php file:


root@gemini:/var/www/wordpress# cat wp-config.php
//Begin Really Simple SSL session cookie settings
@ini_set(‘session.cookie_httponly’, true);
@ini_set(‘session.cookie_secure’, true);
@ini_set(‘session.use_only_cookies’, true);
//END Really Simple SSL


* The base configuration for WordPress
* The wp-config.php creation script uses this file during the installation.
* You don’t have to use the web site, you can copy this file to “wp-config.php”
* and fill in the values.
* This file contains the following configurations:
* * MySQL settings
* * Secret keys
* * Database table prefix
* @link https://wordpress.org/support/article/editing-wp-config-php/
* @package WordPress



!!!!! DB_NAME and other variables have been omitted here for security reasons!!!!



// ** MySQL settings – You can get this info from your web host ** //
/** The name of the database for WordPress */
define( ‘DB_NAME’, ‘***’ );


/** MySQL database username */
define( ‘DB_USER’, ‘***’ );


/** MySQL database password */
define( ‘DB_PASSWORD’, ‘***’ );


/** MySQL hostname */
define( ‘DB_HOST’, ‘localhost’ );


/** Database charset to use in creating database tables. */
define( ‘DB_CHARSET’, ‘utf8mb4’ );


/** The database collate type. Don’t change this if in doubt. */
define( ‘DB_COLLATE’, ” );


* Authentication unique keys and salts.
* Change these to different unique phrases! You can generate these using
* the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
* You can change these at any point in time to invalidate all existing cookies.
* This will force all users to have to log in again.
* @since 2.6.0
define( ‘AUTH_KEY’, ‘;




* WordPress database table prefix.
* You can have multiple installations in one database if you give each
* a unique prefix. Only numbers, letters, and underscores please!
$table_prefix = ‘wp_’;


* For developers: WordPress debugging mode.
* Change this to true to enable the display of notices during development.
* It is strongly recommended that plugin and theme developers use WP_DEBUG
* in their development environments.
* For information on other constants that can be used for debugging,
* visit the documentation.
* @link https://wordpress.org/support/article/debugging-in-wordpress/
define( ‘WP_DEBUG’, false );


/* define( ‘WP_DEBUG’, false ); */


/* Add any custom values between this line and the “stop editing” line. */



/* That’s all, stop editing! Happy publishing. */


/** Absolute path to the WordPress directory. */
if ( ! defined( ‘ABSPATH’ ) ) {
define( ‘ABSPATH’, __DIR__ . ‘/’ );


/** Sets up WordPress vars and included files. */
require_once ABSPATH . ‘wp-settings.php’;


/* define(‘WP_ALLOW_REPAIR’, true); */


define(‘FS_METHOD’, ‘direct’);


@ini_set( ‘upload_max_filesize’ , ‘12800M’ );
@ini_set( ‘post_max_size’, ‘12800M’);
@ini_set( ‘memory_limit’, ‘256M’ );
@ini_set( ‘max_execution_time’, ‘300’ );
@ini_set( ‘max_input_time’, ‘300’ );


define(‘FORCE_SSL_ADMIN’, true);


define( ‘WP_HOME’, ‘’ );
define( ‘WP_SITEURL’, ‘’ );








Install Nextcloud


How to configure Nextcloud


Nextcloud has to be downloaded and installed under /var/www/nextcloud


enter a database name for nextcloud – usually “nextcloud”


port is localhost:3306


a database username and a password.


this gets saved in the nextcloud config file under /var/www/nextcloud/config/config.php


Create a virtual host for nextcloud in /etc/apache2/sites-available/<apacheconfigfile>.conf


You will also need to define the virtual host ie nextcloud for http port 80 in order to obtain the SSL/TLS certificate from Lets Encrypt.


Once this is obtained and installed, you can then delete this port 80 http virtual host for nextcloud.




Switch WordPress from http to https


Once you have obtained and installed the SSL certificates for the virtual hosts, you can switch wordpress to https.


In practice, the switch from http to https for wordpress was complicated.


There are two aspects to this: apache, and wordpress.


The apache virtual host definitions are relatively straightforward. See the apache section above.


WordPress is a little more involved and complicated.


First, you need to install the wordpress plugin Really Simple SSL (if only it were “really simple” in practice).


It was difficult trying to switch the wordpress site name in the wordpress dashboard from http to https.


Also you need to save permalinks twice, using the Permalinks plugin which must first be installed.


Trying to change the wordpress site definitions in the wordpress dashboard under settings – general, did not work, wordpress kept changing them back.


I also tried changing the site names using the mysql admin client and sql commands, but these too would get changed back!


Finally, I had to add the two definitions to the wp-config.php and also the FORCE_SSL_ADMIN directive:


define(‘FORCE_SSL_ADMIN’, true);


define( ‘WP_HOME’, ‘’ );
define( ‘WP_SITEURL’, ‘’ );



Then after numerous attempts, it finally accepted the change from http to https.


You also have to save the links again in WordPress Permalinks plugin after changing from http to https:


select plain, then back to custom, then plain, then finally set to custom.


The links should then function correctly.



Also in this case, don’t forget we are using sslh multiplexer, so the virtual host definitions for the ports need to be 444 for https and not 443.


Make sure these are set in the sites-enabled config file as well as in the apache ports.conf file! (Lets Encrypt changes then to 443 – you have to change them back manually!).


Then make sure sslh is running correctly, listening on 443, and passing ssh traffic to 22 sshd, and https traffic on to 444 apache.



Firewalling – Portmapper


Make sure port 111 for nfs portmapper is closed.


Portmapper is a service usually used with NFS. When this is not properly firewalled, it can be abused to conduct DDOS attacks.


All portmapper services should therefore be behind a firewall and restricted to IPs that need to contact them.


For Linux machines, add firewall rules to block port 111 on both UDP and TCP as follows:


iptables -I INPUT 1 -m tcp -p tcp –dport 111 -j DROP
iptables -I INPUT 1 -m udp -p udp –dport 111 -j DROP


Alternatively, set using the ufw firewall utility in ubuntu with:


root@gemini:# ufw deny 111
Rule updated
Rule updated (v6)
root@gemini:~# ufw status
Status: active


To Action From
— —— —-
22 ALLOW Anywhere
80 ALLOW Anywhere
443 ALLOW Anywhere
3306 ALLOW Anywhere
9993 ALLOW Anywhere
111 DENY Anywhere
2049 ALLOW Anywhere
22 (v6) ALLOW Anywhere (v6)
80 (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)
3306 (v6) ALLOW Anywhere (v6)
9993 (v6) ALLOW Anywhere (v6)
111 (v6) DENY Anywhere (v6)
2049 (v6) ALLOW Anywhere (v6)





Modify hosts files


The hosts file on the new server and all connecting clients must be updated to point the server name to the new IP address.


The new server machine name is changed from gemininew to gemini.


Modify /etc/hosts files to define the new machine IP with the server localhost name ie gemini


Distribute the new /etc/hosts to all connecting clients. This can be done using scp.




Switch off old virtual machine


The final task is to switch off and delete the old virtual machine.


This does not actually have to be switched off in order to delete, but to keep things clean in the event of having to request a special restore from the server provider we will switch off correctly first and then delete.


Login on the OLD machine,


Check the ip address of the machine to make absolutely sure you are logged in on the OLD machine:




then at the command line, do a:


shutdown -h now


Then switch off the old machine on the virtual server provider account dashboard.


Next you can delete the old virtual server.


Make absolutely sure first that you are clicking on and deleting the correct machine!



Finally, delete any associated snapshot backups associated with the old machine.


With that the server migration is now complete. You’ve earned a good cup of coffee!











Continue Reading

MySQL-WordPress Problem: “Error Connecting to Database”

One day without warning and for no clear reason, my website suddenly started sending “error connecting to database” messages in web-browser for http://kevwells.com. 



No content was displayed. The WordPress WP-Admin could not be accessed either. Big Problem!


Non-Wordpress html pages were displaying and functioning ok, so it was obviously a Mysql-Wordpress communication issue.


Also pure PHP pages also displayed without problem – eg phpmyadmin, nextcloud, checkmk. So PHP was running ok.



Possible Causes of the Problem



Most support suggestions list 3 main possibilities:


Your database login credentials are wrong or have been changed.
Your database server is unresponsive.
Your database has been corrupted.


I checked, checked, and rechecked all this in line with all the suggestions – and found everything to be order. No errors, no mistakes, nothing missing.


Searching online for possible alternative explanations for this problem yielded little of use. Most suggestions simply regurgitated the standard textbook stuff – check login credentials, check database is running, check apache is running. Check the database isn’t corrupted.


I encountered other people who also faced this sudden same problem – so I was not alone with this issue – but they were as stuck as I was. 



However, there was a fourth possibility which no-one appeared to have mentioned and did not seem to have occurred to anyone: 


WordPress plugins causing the problem.



After extensive investigation I ascertained that this was indeed the cause in this case.


The solution was to move plugins to a new directory: plugins.disabled.


Then move plugins one by one back to the original plugin directory, login to the wp-admin and activate one by one, test the website content functionality and move plugins back one by one, repeating the procedure for each in turn.




Below are the steps I used in analysing this problem:


Check MySQL is running and listening on the correct port


First check the mysqld or mysql service is up and running, and that apache is running:


ps -ef | grep apache


ps -ef | grep mysql


also check they are listening for connections, and listening on the correct ports:


netstat -tulpn


Check version of mysql with:


root@gemini:/var/www/kevwells.com# mysql –version
mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2



Check your WordPress wp-config.php is correct and does not contain any errors


Next check the wp-config.php definitions for the mysql database. These are essential definitions that must be present – substitute your own definitions for your system and make sure they are correct:


Check your wp-config.php file in your WordPress document root directory.


Your wp-config.php should contain following:


// ** MySQL settings – You can get this info from your web host ** //
/** The name of the database for WordPress */
define( ‘DB_NAME’, ‘database_name_here’ );
/** MySQL database username */
define( ‘DB_USER’, ‘username_here’ );
/** MySQL database password */
define( ‘DB_PASSWORD’, ‘password_here’ );
/** MySQL hostname */
define( ‘DB_HOST’, ‘localhost’ );



How to Debug wp-config.php


The following code, inserted in your wp-config.php file, will log all errors, notices, and warnings to a file called debug.log in the wp-content directory.  It will also hide the errors so they do not interrupt page generation.


This code will need be to inserted before the comment /* That’s all, stop editing! Happy blogging. */ in the wp-config.php file.



// Enable WP_DEBUG mode
define( ‘WP_DEBUG’, true );

// Enable Debug logging to the /wp-content/debug.log file
define( ‘WP_DEBUG_LOG’, true );

// Disable display of errors and warnings
define( ‘WP_DEBUG_DISPLAY’, false );
@ini_set( ‘display_errors’, 0 );

// Use dev versions of core JS and CSS files (only needed if you are modifying these core files)
define( ‘SCRIPT_DEBUG’, true );


WP_DEBUG is a PHP constant (a permanent global variable) that can be used to trigger the “debug” mode throughout WordPress. It is assumed to be false by default and is usually set to true in the wp-config.php file on development copies of WordPress.


// This enables debugging.
define( ‘WP_DEBUG’, true );
// This disables debugging.
define( ‘WP_DEBUG’, false );


Obviously you do not want both true and false defined at the same time, so comment out the one you do not currently want to be active.


Note: The true and false values in the example are not surrounded by apostrophes (‘) because they are boolean (true/false) values. If you set constants to ‘false’, they will be interpreted as true because the quotes make it a string rather than a boolean.


It is not recommended to use WP_DEBUG or the other debug tools on live site; they are meant for local testing, debugging, and install stage only.





WP_DEBUG_LOG is a companion to WP_DEBUG that causes all errors to also be saved to a debug.log log file This is useful if you want to review all notices later or need to view notices generated off-screen (e.g. during an AJAX request or wp-cron run).


Note that this allows you to write to log file using PHP’s built in error_log() function, which can be useful for instance when debugging Ajax events.


When set to true, the log is saved to debug.log in the content directory (usually wp-content/debug.log) within your site’s filesystem. Alternatively, you can set it to a valid file path to have the file saved elsewhere.


define( ‘WP_DEBUG_LOG’, true );
define( ‘WP_DEBUG_LOG’, ‘/tmp/wp-errors.log’ );
Note: for WP_DEBUG_LOG to do anything, WP_DEBUG must be enabled (true). Remember you can turn off WP_DEBUG_DISPLAY independently.




Does the problem occur for /wp-admin/ as well?


Does it occur with webpages on the server which are not provided by the wordpress database? eg plain html, php-driven services, and the like, if there are any on the server?


eg checkmk, or nextcloud, or simple html pages.



Try Repairing the WordPress Database



If the error is only on the front end. try to repair the database by adding the following line of code in wp-config.php


define(‘WP_ALLOW_REPAIR’, true);


Then go to ‘http://www.yoursite.com/wp-admin/maint/repair.php’ and click ‘repair and optimize database’ button.



/usr/bin/mysqlcheck –all-databases –auto-repair





mysqlshow -u database_username -p



root@gemini:~# mysqlshow –verbose
| Databases | Tables |



| 12 |

9 rows in set.




Check Your index.php is Present and Error-Free


Check the index.php is present and is the correct instance. This is important!


Whenever someone visits a web page on a WordPress site, the server attempts to run index.php or index.html from the defined WordPress document root directory.


Your website document root directory index.php contains the code that generates a WordPress site.


However there is also an index.php in the wp-content directory and in other locations. If you examine the index.php located under wp-content and other directories you will find that it contains no active code. It is simply a placeholder.



Without index.php, anyone could access your site’s /wp-content folder and see all of the media, files, and directories it contains.


The index.php file at this location and others functions like a privacy screen: it blocks visitors from directly accessing your directories.


Alternativey you can use the following directive in your .htaccess file to prevent directory listings even if that index.php file is not present:


Options –Indexes


However, be aware that you do NOT want this file in your main document root folder, else no one can access any website content!



Check Apache Can Connect To MySQL Server


Next, check your web-host can connect to your mysql server.


In PHP you can easily do this using the mysqli_connect() function. All communication between PHP and the MySQL database server takes place through this connection. Here’re the basic syntaxes for connecting to MySQL using MySQLi extensions:


Syntax: MySQLi, Procedural way
$link = mysqli_connect(“hostname”, “username”, “password”, “database”);


Syntax: MySQLi, Object Oriented way
$mysqli = new mysqli(“hostname”, “username”, “password”, “database”);


Create a new file called testconnection.php and paste the following code in it:


$link = mysqli_connect(‘localhost’, ‘username’, ‘password’);
if (!$link) {
die(‘Could not connect: ‘ . mysqli_error());

echo ‘Connected successfully’;


(substitute localhost, username and password with the correct parameters for your site)


Then I called up the file in the webbrowser:


Result, it displayed:


Connected successfully



So that means the connection parameters – dbhost name, username and password are ok.



Check the Error Logs for MySQL and Apache



If the configuration seems correct and the service is running, but your website still doesn’t load as it should, try checking the logs for any hints to as what might be the cause.



Error logs for Apache are located at /var/log/apache/error.log


Error logs for MySQL are located at /var/log/mysql/error.log


Should you not be able to find anything within the most recent logs, check the archived ones as well. To do this, use ‘zgrep’ with otherwise the same command as regular ‘grep’


sudo zgrep -i error /var/log/mysql/error.log.1.gz


Since the database under CentOS is named MariaDB instead of MySQL, the logs are also saved under a different name. You can search the logs with the following command.


sudo grep -i error /var/log/mariadb/mariadb.log


Debian systems also report MySQL events to /var/log/syslog, to filter out everything else, use ‘grep’ with two keywords separated by .* to express ‘and’ like in the command below.


sudo grep -i -E ‘mysql.*error’ /var/log/syslog


If you are having difficulties finding anything helpful, try different keywords such as ‘start’ to see when the service was last restarted, or ‘failed’ to find any less critical problems that might not be reported as errors.


When we try


 we see in /var/log/mysql/error.log:


2021-12-29 19:39:44 264 [Warning] Access denied for user ”@’localhost’ (using password: NO)
2021-12-29 19:40:22 270 [Warning] Access denied for user ”@’localhost’ (using password: NO)


so, it looks like there is some login issue for the mysql database user:




Check the MySQL Database




MariaDB []> SELECT User, Host FROM mysql.user;
| User | Host |
| * | % |
| * | localhost |
| * | localhost |
| * | localhost |
| * | localhost |
| * | localhost |
6 rows in set (0.000 sec)

MariaDB [kevwells]>


NOTE! Usernames have been removed from the above for security reasons.


Use the desc mysql.user; statement to display information about the table’s columns. Once you know the column name, you can run a query against a selected data.


For example, to get a list of all MySQL users accounts including information about the password and whether it is active or expired, you would use the following query:


SELECT User, Host, Password, password_expired FROM mysql.user;



MariaDB []> SELECT User, Host, Password, password_expired FROM mysql.user;
| User | Host | Password | password_expired |
| * | localhost | * | N |
| * | localhost | * | |
| * | localhost | * | N |
| * | localhost | * | N |
| * | % | * | N |
| * | localhost | * | N |
6 rows in set (0.001 sec)

MariaDB []>


#NOTE! Usernames and passwords have been removed from the above for security reasons.


Show Users that Have Access to a Particular Database


The information about the database-level privileges is stored in the mysql.db table.
You can query the table to find out which users have access to a given database and the level of the privileges.


For example, to get a list of all users that have some level access to the database named db_name you would use the following query:


SELECT * FROM mysql.db WHERE Db = ‘db_name’\G;



MariaDB []> SELECT * FROM mysql.db WHERE Db = ‘*****’\G;
*************************** 1. row ***************************
Host: localhost
Db: ****
User: ***
Select_priv: Y
Insert_priv: Y
Update_priv: Y
Delete_priv: Y
Create_priv: Y
Drop_priv: Y
Grant_priv: N
References_priv: Y
Index_priv: Y
Alter_priv: Y
Create_tmp_table_priv: Y
Lock_tables_priv: Y
Create_view_priv: Y
Show_view_priv: Y
Create_routine_priv: Y
Alter_routine_priv: Y
Execute_priv: Y
Event_priv: Y
Trigger_priv: Y
Delete_history_priv: Y
1 row in set (0.001 sec)

ERROR: No query specified

MariaDB []>



NOTE! Usernames, dbname and passwords have been removed from the above for security reasons.



To fetch information only about the user accounts that have access to a given database, without displaying the privileges use:


SELECT db, host, user FROM mysql.db WHERE db = ‘db_name’;



MariaDB []> SELECT db, host, user FROM mysql.db WHERE db = ‘***’
-> ;
| db | host | user |
| * | localhost | * |
1 row in set (0.001 sec)

MariaDB []>


NOTE: Sensitive information has been removed from the output for security reasons!


The following query will show you information about all databases and associated users:


SELECT db, host, user FROM mysql.db;


Try restoring database from a backup made using wp-db-backup plugin:




Put the backed-up SQL back into MySQL/MariaDB:
user@linux:~/files/blog> mysql -h mysqlhostserver -u mysqlusername -p databasename < blog.bak.sql


Enter password: (enter your mysql password)


root@gemini:/var/www/kevwells.com# mysql -h localhost -u *** -p *** < /var/www/kevwells.com/*****.sql

Enter password:



NOTE! Usernames, dbname and passwords have been removed from the above for security reasons



To delete a user use:


DROP USER ‘exampleuser1’@localhost;



then check the user is no longer present:


MariaDB [(none)]> SELECT User FROM mysql.user;



The syntax for changing a password using the SET PASSWORD statement in MariaDB is:


SET PASSWORD [ FOR user_name ] =
| OLD_PASSWORD(‘plaintext_password2’)
| ‘encrypted_password’



For example, if you had an entry with User and Host column values of ‘bob’ and ‘%.loc.gov’, you would write the statement like this:


SET PASSWORD FOR ‘bob’@’%.loc.gov’ = PASSWORD(‘newpass’);


If you want to delete a password for a user, you would do:


SET PASSWORD FOR ‘bob’@localhost = PASSWORD(“”);


I was then able to access phpmyadmin, so one step forward on the way to solving the problem.


In phpmyadmin update the site url if necessary with:


UPDATE wp_options SET option_value=’YOUR_SITE_URL’ WHERE option_name=’siteurl’


UPDATE wp_options SET option_value=’′ WHERE option_name=’siteurl’



Show query box
1 row affected. (Query took 0.0024 seconds.)
UPDATE wp_options SET option_value=’′ WHERE option_name=’siteurl’



1 row affected.. yes, but it also says


apache2, mysql and sshd are all running.


check version of mysql with


root@gemini:/var/www/kevwells.com# mysql –version
mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2



Set the MySQL user password
Type the following commands if you have MySQL 5.7.6 and later or MariaDB 10.1.20 and later:







Query OK, 0 rows affected (0.001 sec)

MariaDB [mysql]>


that is now working ok!


but the website still can’t connect…




Note the command and its arguments are of the form:


mysql -h locahost -u username -p database



The arguments are


-h followed by the server host name (localhost)
-u followed by the account user name (use your MySQL username)
-p which tells mysql to prompt for a password
database the name of the database (use your database name).
Once mysql is running, you can type SQL statements and see the output in the terminal window.





so lets try:


mysql -h locahost -u username -p database




It works, we can connect to the database…



but the website still does not connect!



To reset a user’s MySQL/MariaDB password, log in to the command-line client as root with the command:


mysql -u root -p


Next, reset the user’s password with the command:


update mysql.user set password = MD5(‘(new_password)’) where user = “jdoe”;


Replace new_password with the new password, and jdoe with the username.




Next, show the user’s grants with the command:


show grants for ‘jdoe’@’localhost’;


Replace jdoe with the username. If applicable, change localhost to the host name.


You should get a list of the privileges that user has for the relevant database. It should look something like this:


MariaDB [(none)]> show grants for ‘***’@’localhost’;
| Grants for ***@localhost |
| GRANT ALL PRIVILEGES ON *.* TO `****`@`localhost` IDENTIFIED BY PASSWORD ‘********** ‘ |
| GRANT ALL PRIVILEGES ON `<database>`.* TO `****`@`localhost` |
| GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER ON `<database`.* TO `***`@`localhost` |
3 rows in set (0.000 sec)

MariaDB [(none)]>



NOTE: Sensitive data removed from above for security reasons!


to create a new user:


Log into MYSQL as root


mysql -u root
Grant privileges. For a new user, execute:

CREATE USER ‘newuser’@’localhost’ IDENTIFIED BY ‘password’;
GRANT ALL PRIVILEGES ON *.* TO ‘newuser’@’localhost’;


I created a new php user for admin  as the original user could no longer login all of a sudden!



can now login once again to phpmyadmin



Check the MySQL hostname


If the script is hosted on the same server as the MySQL database (e.g. your Linux Cloud Server), the hostname is localhost by default.


To double-check the MySQL hostname, log into the command-line client as root with the command:


mysql -u root -p


Next, show the hostname with the command:


SHOW VARIABLES WHERE Variable_name = ‘hostname’;



MariaDB [(none)]> SHOW VARIABLES WHERE Variable_name = ‘hostname’;

| Variable_name | Value |
| hostname | gemini |
1 row in set (0.001 sec)

MariaDB [(none)]>



On Debian and Ubuntu servers the configuration file for MySQL is usually saved at /etc/mysql/. It’s also possible to have user-specific settings stored at /home/<user>/.my.cnf, which would override the global configurations. Check if any user level overrides have been set.


It is commonly advised to have separate usernames for different web applications, so check at least those relevant to your page loading issues. You can open the global configuration file with first of the following two commands below, and the user-specific with the latter by replacing the <user> with a database username.


sudo nano /etc/mysql/my.cnf


sudo nano /home/<user>/.my.cnf


By scrolling down past [client] and [mysqld_safe] settings you’ll find something like the example here.



# * Basic Settings
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind-address =



The lines here to pay close attention to are ‘socket’, ‘datadir’ and ‘bind-address’. The parameters in the example above are in their default values, and in most cases, your configuration would look the same. Make sure the settings point to the correct directories so that MySQL can actually find the required files.


The easiest way to check the ‘datadir’ is to use this command below


sudo ls -l /var/lib/mysql/


The output will list all files in that directory, it should contain at least the following plus any databases you have created.


drwx—— 2 mysql root 4096 Aug 5 12:23 mysql
drwx—— 2 mysql mysql 4096 Aug 5 12:29 performance_schema


If the data directory or socket has been moved and MySQL doesn’t know where they are, fix the configuration file to point to the correct directories. You can search for the folders with the following command.


sudo find / -name performance_schema && sudo find / -name mysql.sock


The third parameter you’ll need to check is the bind-address, this is only really relevant if your database needs to be accessed remotely. In Debian and Ubuntu installations the bind is by default set to the loopback address, which prevents database calls from outside the localhost. CentOS doesn’t have the same parameter unless manually set. For any setup where your web service is on a different server to the database, this bind-address should be set to the server’s own private IP.






Step 2 — Importing a MySQL or MariaDB Database


To import an existing dump file into MySQL or MariaDB, you will have to create a new database. This database will hold the imported data.


First, log in to MySQL as root or another user with sufficient privileges to create new databases:


mysql -u root -p

This command will bring you into the MySQL shell prompt. Next, create a new database with the following command. In this example, the new database is called new_database:


CREATE DATABASE new_database;

You’ll see this output confirming the database creation.


Query OK, 1 row affected (0.00 sec)


Then exit the MySQL shell by pressing CTRL+D. From the normal command line, you can import the dump file with the following command:


mysql -u username -p new_database < data-dump.sql

username is the username you can log in to the database with
newdatabase is the name of the freshly created database
data-dump.sql is the data dump file to be imported, located in the current directory


If the command runs successfully, it won’t produce any output. If any errors occur during the process, mysql will print them to the terminal instead. To check if the import was successful, log in to the MySQL shell and inspect the data. Selecting the new database with USE new_database and then use SHOW TABLES; or a similar command to look at some of the data.



The Actual Cause of the Problem In This Case – WordPress Plugins



I finally ascertained that the cause of the problem was WordPress plugins. Presumably one or more WordPress plugins have been automatically updated and have suddenly resulted in this bug being released.




The Solution 


The solution was to move all plugins to a new directory: plugins.disabled.


Then move each plugin one by one back to the original plugin directory, for each  plugin, login to the wp-admin and activate one by one, test the website content functionality.


Continue in this way, moving plugins back one by one, repeating the procedure for each in turn.


Make sure when you test the pages content to call up different pages each time – else you could be receiving a “false positive” from cached website content from caches in the web-browser or internet provider!


In addition, I made sure to disable the automatic WordPress Plugin update option. As so often happens with these kind of “smart functionality” bright ideas for auto-updating dreamt up by programmers, it ends up in practice causing more problems than the one it was originally intended to solve. 


So, the website is now connecting to the database successfully and is once again able to display the content without errors. Let’s hope we can keep it that way.


Until the next software update.


Continue Reading

How To Install Apache2 on Linux Ubuntu

This article explains the procedure for installing apache2 on Linux Ubuntu version 20.


apt install apache2
Next, create an initial virtual host entry in the /etc/apache2/sites-available/kevwells.com.conf file
<VirtualHost *:80>
ServerName kevwells.com
ServerAlias www.kevwells.com
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
You can check the config with:
apachectl configtest
Next, enable the configuration:
a2ensite kevwells.com.conf
systemctl start apache2
This config can then be used to obtain an SSL certificate from Let’s Encrypt. See further below. 
Important Note re HTTPS and ssh 
My server is using the sslh multiplexer daemon, this uses incoming port 443 for both ssh and https.
This is to avoid ssh connection problems when trying to connect to the server from outgoing routers which do not permit outgoing ssh port 22 connections (some routers block this. If you have administrator access to the router you can modify this, but if you don’t then a workaround is to use port 443 for outgoing ssh connections, since port 443 is hardly ever blocked by routers and can thus be relied upon to be accessible).
On my server, incoming port 443 ssh connections are therefore redirected to sshd on port 22, while incoming https 443 connections are redirected to https port 444 on apache.
Thus apache must be configured to listen on port 444 instead of the default 443.
Note that some programs, such as Lets Encrypt’s SSL Certbot automatically define the https port as the default 443, so you need to remove this and set it to 444, otherwise apache will not start.
Apache ports.conf needs to look like this. Note that port 443 is not used:
root@gemini:/etc/apache2# cat ports.conf
# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf
Listen 80 

<IfModule mod_ssl.c>
#Listen 443
Listen 444
The sites-enabled will also use 444 instead of 443 for virtual host definitions:
ServerName kevwells.com
ServerAlias www.kevwells.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/kevwells.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/kevwells.com/privkey.pem

Continue Reading

The Ultimate .htaccess Apache File Guide

This is a copy of The Ultimate .htaccess Apache File Guide by Charles Torvalds from www.askapache.com

This document is reproduced here by kind permission of Charles Torvalds from www.askapache.com

Full copyright details relating to this document can be found at the foot of this page.

The Ultimate .htaccess Apache File Guide

htaccess is a very ancient configuration file that controls the Web Server running your website, and is one of the most powerful configuration files you will ever come across.

.htaccess has the ability to control access of the WWW‘s HyperText Transfer Protocol (HTTP) using Password Protection, 301 Redirects, rewrites, and much much more. This is because this configuration file was coded in the earliest days of the web (HTTP), for one of the first Web Servers ever!

Eventually these Web Servers (configured with htaccess) became known as the World Wide Web, and eventually grew into the Internet we use today.

This is not an introduction to .htaccess.  This is the evolution of the best of the best. You’ve come to the right place if you are looking to acquire mad skills for using .htaccess files.

Originally (2003) this guide was known in certain hacker circles and hidden corners of the net as an ultimate .htaccess due to the powerful htaccess tricks and tips to bypass security on a webhost, and also because many of the tricks and examples were pretty impressive back then in that group.

Contents [hide]

.htaccess – Evolved ^

The Hyper Text Transfer Protocol (HTTP) was initiated at the CERN in Geneve
(Switzerland), where it emerged (together with the HTML presentation
language) from the need to exchange scientific information on a computer
network in a simple manner. The first public HTTP implementation only
allowed for plain text information, and almost instantaneously became a
replacement of the GOPHER service. One of the first text-based browsers
was LYNX which still exists today; a graphical HTTP client appeared very
quickly with the name NCSA Mosaic. Mosaic was a popular browser back in
1994. Soon the need for a more rich multimedia experience was born, and
the markup language provided support for a growing multitude of media

Htaccess file know-how will do several things for you:

  • Make your website noticeably faster.
  • Allow you to debug your server with ease.
  • Make your life easier and more rewarding.
  • Allow you to work faster and more productively.

AskApache Htaccess Journey ^

Skip this – still under edit

discovered these tips and tricks mostly while working as a network
security penetration specialist hired to find security holes in web
hosting environments. Shared hosting is the most common and cheapest
form of web-hosting where multiple customers are placed on a single
machine and “share” the resources (CPU/RAM/SPACE). The machines are
configured to basically ONLY do HTTP and FTP. No shells or any
interactive logins, no ssh, just FTP access. That is when I started
examining htaccess files in great detail and learned about the
incredible untapped power of htaccess. For 99% of the worlds best Apache
admins, they don’t use .htaccess much, if AT ALL. It’s much easier,
safer, and faster to configure Apache using the httpd.conf file instead.
However, this file is almost never readable on shared-hosts, and I’ve
never seen it writable. So the only avenue left for those on
shared-hosting was and is the .htaccess file, and holy freaking
fiber-optics.. it’s almost as powerful as httpd.conf itself!

all .htaccess code works in the httpd.conf file, but not all httpd.conf
code works in .htaccess files, around 50%. So all the best Apache
admins and programmers never used .htaccess files. There was no
incentive for those with access to httpd.conf to use htaccess, and the
gap grew. It’s common to see “computer gurus” on forums and mailing
lists rail against all uses and users of .htaccess files, smugly
announcing the well known problems with .htaccess files compared with
httpd.conf – I wonder if these “gurus” know the history of the htaccess
file, like it’s use in the earliest versions of the HTTP Server- NCSA’s
HTTPd, which BTW, became known as Apache HTTP. So you could easily say
that htaccess files predates Apache itself.

Once I discovered
what .htaccess files could do towards helping me enumerate and exploit
security vulnerabilities even on big shared-hosts I focused all my
research into .htaccess files, meaning I was reading the venerable
Apache HTTP Source code 24/7! I compiled every released version of the
Apache Web Server, ever, even NCSA’s, and focused on enumerating the
most powerful htaccess directives. Good times! Because my focus was on
protocol/file/network vulnerabilites instead of web dev I built up a
nice toolbox of htaccess tricks to do unusual things. When I switched
over to webdev in 2005 I started using htaccess for websites, not
research. I documented most of my favorites and rewrote the htaccess
guide for webdevelopers. After some great encouragement on various
forums and nets I decided to start a blog to share my work with
everyone, AskApache.com was registered, I published my guide, and it was
quickly plagiarized and scraped all over the net. Information is
freedom, and freedom is information, so this blog has the least
restrictive copyright for you. Feel free to modify, copy, republish,
sell, or use anything on this site 😉

What Is .htaccess ^

Specifically, .htaccess is the default file name of a special configuration file that provides a number of directives (commands) for controlling and configuring the Apache Web Server, and also to control and configure modules
that can be built into the Apache installation, or included at run-time
like mod_rewrite (for htaccess rewrite), mod_alias (for htaccess
redirects), and mod_ssl (for controlling SSL connections).

allows for decentralized management of Web Server configurations which
makes life very easy for web hosting companies and especially their
savvy consumers. They set up and run “server farms” where many hundreds
and thousands of web hosting customers are all put on the same Apache
Server. This type of hosting is called “virtual hosting” and without
.htaccess files would mean that every customer must use the same exact
settings as everyone else on their segment. So that is why any
half-decent web host allows/enables (DreamHost, Powweb, MediaTemple, GoDaddy) .htaccess files,
though few people are aware of it. Let’s just say that if I was a
customer on your server-farm, and .htaccess files were enabled, my
websites would be a LOT faster than yours, as these configuration files
allow you to fully take advantage of and utilize the resources allotted
to you by your host. If even 1/10 of the sites on a server-farm took
advantage of what they are paying for, the providers would go out of

SKIP: History of Htaccess in 1st Apache.

of the design goals for this server was to maintain external
compatibility with the NCSA 1.3 server — that is, to read the same
configuration files, to process all the directives therein correctly,
and in general to be a drop-in replacement for NCSA. On the other hand,
another design goal was to move as much of the server’s functionality
into modules which have as little as possible to do with the monolithic
server core. The only way to reconcile these goals is to move the
handling of most commands from the central server into the modules.

just giving the modules command tables is not enough to divorce them
completely from the server core. The server has to remember the commands
in order to act on them later. That involves maintaining data which is
private to the modules, and which can be either per-server, or
per-directory. Most things are per-directory, including in particular
access control and authorization information, but also information on
how to determine file types from suffixes, which can be modified by
AddType and DefaultType directives, and so forth. In general, the
governing philosophy is that anything which can be made configurable by
directory should be; per-server information is generally used in the
standard set of modules for information like Aliases and Redirects which
come into play before the request is tied to a particular place in the
underlying file system.

Another requirement for emulating the NCSA server is being able to handle the per-directory configuration files, generally called .htaccess files,
though even in the NCSA server they can contain directives which have
nothing at all to do with access control. Accordingly, after URI ->
filename translation, but before performing any other phase, the server
walks down the directory hierarchy of the underlying filesystem,
following the translated pathname, to read any .htaccess files which
might be present. The information which is read in then has to be merged
with the applicable information from the server’s own config files
(either from the sections in access.conf, or from defaults in srm.conf, which actually behaves for most purposes almost exactly like ).

Finally, after having served a request which involved reading .htaccess files,
we need to discard the storage allocated for handling them. That is
solved the same way it is solved wherever else similar problems come up,
by tying those structures to the per-transaction resource pool.

Creating Htaccess Files ^

What an Htaccess File Looks Like in Windows ExplorerHtaccess files use the default filename “.htaccess” but any unix-style file name can be specified from the main server config using the AccessFileName directive. The file isn’t .htaccess.txt, its literally just named .htaccess.

View .htaccess filesIn
a Windows Environment like the one I use for work, you can change how
Windows opens and views .htaccess files by modifying the Folder Options
in explorer. As you can see, on my computer files ending in .htaccess
are recognized as having the HTACCESS extension and are handled/opened
by Adobe Dreamweaver CS4.

Htaccess Scope ^

Unlike the main server configuration files like httpd.conf, Htaccess files are read on every request
therefore changes in these files take immediate effect. Apache searches
all directories and subdirectories that are htaccess-enabled for an
.htaccess file which results in performance loss due to file accesses.
I’ve never noticed a performance loss but OTOH, I know how to use them.
If you do have access to your main server configuration file, you should
of course use that instead, and lucky for you ALL the .htaccess tricks
and examples can be used there as well (just not vice versa).

Htaccess File Syntax ^

Htaccess files follow the same syntax as the main Apache configuration files, for powerusers here’s an apache.vim for VI. The one main difference is the context
of the directive, which means whether or not that directive is ALLOWED
to be used inside of an .htaccess file. Htaccess files are incredibly
powerful, and can also be very dangerous as some directives allowed in
the main configuration files would allow users/customers to completely
bypass security/bandwidth-limits/resource-limits/file-permissions, etc..
About 1/4 of all Apache directives cannot be used inside an .htaccess
file (also known as a per-directory context config). The Apache
Developers are well-regarded throughout the world as being among some of
the best programmers, ever. To enable a disallowed directive inside a
.htaccess file would require modifying the source code and re-compiling
the server (which they allow and encourage if you are the owner/admin).

Htaccess Directives ^

Don’t ask why, but I personally downloaded each major/beta release of the Apache HTTPD source code from version 1.3.0 to version 2.2.10 (all 63 Apache versions!), then I configured and compiled each version for a custom HTTPD installation built from source. This allowed me to find every directive allowed in .htaccess files for each particular version, which has never been done before, or since. YES!I think that is so cool..

An .htaccess directive
is basically a command that is specific to a module or builtin to the
core that performs a specific task or sets a specific setting for how
Apache serves your WebSite. Directives placed in Htaccess files apply to the directory they are in, and all sub-directories. Here’s the 3 top links (official Apache Docs) you will repeatedly use, bookmark/print/save them.

htaccess Context Legend

  1. Terms Used to Describe Directives
  2. Official List of Apache Directives
  3. Directive Quick-Reference — with Context

Main Server Config Examples ^

lets take a look at some htaccess examples to get a feel for the syntax
and some general ideas at the capabilities. Some of the best examples
for .htaccess files are included with Apache for main server config
files, so lets take a quick look at a couple of them on our way down to
the actual .htaccess examples further down the page (this site has
thousands, take your time). The basic syntax is a line starting with # is a comment, everything else are directives followed by the directive argument.

httpd-multilang-errordoc.conf: The configuration below implements multi-language error documents through content-negotiation

Here are the rest of them if you wanna take a look. (httpd-mpm.conf, httpd-default.conf, httpd-ssl.conf, httpd-info.conf, httpd-vhosts.conf, httpd-dav.conf)

Example .htaccess Code Snippets ^

Here are some specific examples, this is the most popular section of this page. Updated frequently.

Redirect Everyone Except IP address to alternate page ^

ErrorDocument 403 http://www.yahoo.com/
Order deny,allow
Deny from all
Allow from


When developing sites ^

lets google crawl the page, lets me access without a password, and lets
my client access the page WITH a password. It also allows for XHTML and
CSS validation! (w3.org)

AuthName "Under Development"
AuthUserFile /web/sitename.com/.htpasswd
AuthType basic
Require valid-user
Order deny,allow
Deny from all
Allow from w3.org htmlhelp.com googlebot.com
Satisfy Any


Fix double-login prompt ^

Redirect non-https requests to https server and ensure that .htpasswd authorization can only be entered across HTTPS

SSLOptions +StrictRequire
SSLRequire %{HTTP_HOST} eq "askapache.com"
ErrorDocument 403 https://askapache.com


Set Timezone of the Server (GMT) ^

SetEnv TZ America/Indianapolis


Administrator Email for ErrorDocument ^

SetEnv SERVER_ADMIN webmaster@google.com


ServerSignature for ErrorDocument ^

ServerSignature off | on | email


Charset and Language headers ^

Article: Setting Charset in htaccess, and article by Richard Ishida

AddDefaultCharset UTF-8
DefaultLanguage en-US


Disallow Script Execution ^

Options -ExecCGI
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi


Deny Request Methods ^

RewriteRule .* - [F]


Force “File Save As” Prompt ^

AddType application/octet-stream .avi .mpg .mov .pdf .xls .mp4


Show CGI Source Code ^

RemoveHandler cgi-script .pl .py .cgi
AddType text/plain .pl .py .cgi


Serve all .pdf files on your site using .htaccess and mod_rewrite with the php script. ^

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.+).pdf$  /cgi-bin/pdf.php?file=$1 [L,NC,QSA]


Rewrite to www ^

RewriteCond %{REQUEST_URI} !^/(robots.txt|favicon.ico|sitemap.xml)$
RewriteCond %{HTTP_HOST} !^www.askapache.com$ [NC]
RewriteRule ^(.*)$ /$1 [R=301,L]


Rewrite to www dynamically ^

RewriteCond %{REQUEST_URI} !^/robots.txt$ [NC]
RewriteCond %{HTTP_HOST} !^www.[a-z-]+.[a-z]{2,6} [NC]
RewriteCond %{HTTP_HOST} ([a-z-]+.[a-z]{2,6})$   [NC]
RewriteRule ^/(.*)$ http://%1/$1 [R=301,L]


301 Redirect Old File ^

Redirect 301 /old/file.html /new/file.html


301 Redirect Entire Directory ^

RedirectMatch 301 /blog(.*) /$1


Protecting your php.cgi ^

<FilesMatch "^php5?.(ini|cgi)$">
Order Deny,Allow
Deny from All
Allow from env=REDIRECT_STATUS


Set Cookie based on Request ^

This code sends the Set-Cookie header to create a cookie on the client with the value of a matching item in 2nd parantheses.

RewriteEngine On
RewriteBase /
RewriteRule ^(.*)(de|es|fr|it|ja|ru|en)/$ - [co=lang:$2:.askapache.com:7200:/]


Set Cookie with env variable ^

Header set Set-Cookie "language=%{lang}e; path=/;" env=lang


Custom ErrorDocuments ^

ErrorDocument 100 /100_CONTINUE
ErrorDocument 101 /101_SWITCHING_PROTOCOLS
ErrorDocument 102 /102_PROCESSING
ErrorDocument 200 /200_OK
ErrorDocument 201 /201_CREATED
ErrorDocument 202 /202_ACCEPTED
ErrorDocument 203 /203_NON_AUTHORITATIVE
ErrorDocument 204 /204_NO_CONTENT
ErrorDocument 205 /205_RESET_CONTENT
ErrorDocument 206 /206_PARTIAL_CONTENT
ErrorDocument 207 /207_MULTI_STATUS
ErrorDocument 300 /300_MULTIPLE_CHOICES
ErrorDocument 301 /301_MOVED_PERMANENTLY
ErrorDocument 302 /302_MOVED_TEMPORARILY
ErrorDocument 303 /303_SEE_OTHER
ErrorDocument 304 /304_NOT_MODIFIED
ErrorDocument 305 /305_USE_PROXY
ErrorDocument 307 /307_TEMPORARY_REDIRECT
ErrorDocument 400 /400_BAD_REQUEST
ErrorDocument 401 /401_UNAUTHORIZED
ErrorDocument 402 /402_PAYMENT_REQUIRED
ErrorDocument 403 /403_FORBIDDEN
ErrorDocument 404 /404_NOT_FOUND

ErrorDocument 405 /405_METHOD_NOT_ALLOWED
ErrorDocument 406 /406_NOT_ACCEPTABLE
ErrorDocument 408 /408_REQUEST_TIME_OUT
ErrorDocument 409 /409_CONFLICT
ErrorDocument 410 /410_GONE
ErrorDocument 411 /411_LENGTH_REQUIRED
ErrorDocument 412 /412_PRECONDITION_FAILED
ErrorDocument 413 /413_REQUEST_ENTITY_TOO_LARGE
ErrorDocument 414 /414_REQUEST_URI_TOO_LARGE
ErrorDocument 415 /415_UNSUPPORTED_MEDIA_TYPE
ErrorDocument 416 /416_RANGE_NOT_SATISFIABLE
ErrorDocument 417 /417_EXPECTATION_FAILED
ErrorDocument 422 /422_UNPROCESSABLE_ENTITY
ErrorDocument 423 /423_LOCKED
ErrorDocument 424 /424_FAILED_DEPENDENCY
ErrorDocument 426 /426_UPGRADE_REQUIRED
ErrorDocument 500 /500_INTERNAL_SERVER_ERROR
ErrorDocument 501 /501_NOT_IMPLEMENTED
ErrorDocument 502 /502_BAD_GATEWAY
ErrorDocument 503 /503_SERVICE_UNAVAILABLE
ErrorDocument 504 /504_GATEWAY_TIME_OUT
ErrorDocument 505 /505_VERSION_NOT_SUPPORTED
ErrorDocument 506 /506_VARIANT_ALSO_VARIES
ErrorDocument 507 /507_INSUFFICIENT_STORAGE
ErrorDocument 510 /510_NOT_EXTENDED


Implementing a Caching Scheme with .htaccess ^

# year
<FilesMatch ".(ico|pdf|flv|jpg|jpeg|png|gif|swf|mp3|mp4)$">
Header set Cache-Control "public"
Header set Expires "Thu, 15 Apr 2010 20:00:00 GMT"
Header unset Last-Modified
#2 hours
<FilesMatch ".(html|htm|xml|txt|xsl)$">
Header set Cache-Control "max-age=7200, must-revalidate"
<FilesMatch ".(js|css)$">
SetOutputFilter DEFLATE
Header set Expires "Thu, 15 Apr 2010 20:00:00 GMT"


Password Protect single file ^

<Files login.php>
AuthName "Prompt"
AuthType Basic
AuthUserFile /web/askapache.com/.htpasswd
Require valid-user


Password Protect multiple files ^

<FilesMatch "^(private|phpinfo).*$">
AuthName "Development"
AuthUserFile /.htpasswd
AuthType basic
Require valid-user


Send Custom Headers ^

Header set P3P "policyref="/w3c/p3p.xml""
Header set X-Pingback "/xmlrpc.php"
Header set Content-Language "en-US"
Header set Vary "Accept-Encoding"


Blocking based on User-Agent Header ^

SetEnvIfNoCase ^User-Agent$ .*(craftbot|download|extract|stripper|sucker|ninja|clshttp|webspider|leacher|collector|grabber|webpictures) HTTP_SAFE_BADBOT
SetEnvIfNoCase ^User-Agent$ .*(libwww-perl|aesop_com_spiderman) HTTP_SAFE_BADBOT
Deny from env=HTTP_SAFE_BADBOT


Blocking with RewriteCond ^

RewriteCond %{HTTP_USER_AGENT} ^.*(craftbot|download|extract|stripper|sucker|ninja|clshttp|webspider|leacher|collector|grabber|webpictures).*$ [NC]
RewriteRule . - [F,L]


.htaccess for mod_php ^

SetEnv PHPRC /location/todir/containing/phpinifile


.htaccess for php as cgi ^

AddHandler php-cgi .php .htm
Action php-cgi /cgi-bin/php5.cgi


Shell wrapper for custom php.ini ^

exec php5.cgi -c /abs/php5/php.ini


Add values from HTTP Headers ^

SetEnvIfNoCase ^If-Modified-Since$ "(.+)" HTTP_IF_MODIFIED_SINCE=$1
SetEnvIfNoCase ^If-None-Match$ "(.+)" HTTP_IF_NONE_MATCH=$1
SetEnvIfNoCase ^Cache-Control$ "(.+)" HTTP_CACHE_CONTROL=$1
SetEnvIfNoCase ^Connection$ "(.+)" HTTP_CONNECTION=$1
SetEnvIfNoCase ^Keep-Alive$ "(.+)" HTTP_KEEP_ALIVE=$1
SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1
SetEnvIfNoCase ^Cookie$ "(.+)" HTTP_MY_COOKIE=$1


Stop hotlinking ^

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www.)?askapache.com/.*$ [NC]
RewriteRule .(gif|jpg|swf|flv|png)$ /feed.gif [R=302,L]


Turn logging off for IP ^

SecFilterSelective REMOTE_ADDR "" "nolog,noauditlog,pass"


Turn logging on for IP ^

SecFilterSelective REMOTE_ADDR "!^" "nolog,noauditlog,pass"
SecFilterSelective REMOTE_ADDR "" "log,auditlog,pass"


Example .htaccess Files ^

are some samples and examples taken from different .htaccess files I’ve
used over the years. Specific solutions are farther down on this page
and throughout the site.

# Set the Time Zone of your Server
SetEnv TZ America/Indianapolis

# ServerAdmin:  This address appears on some server-generated pages, such as error documents.
SetEnv SERVER_ADMIN webmaster@askapache.com

# Possible values for the Options directive are "None", "All", or any combination of:
#  Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
Options -ExecCGI -MultiViews -Includes -Indexes FollowSymLinks

# DirectoryIndex: sets the file that Apache will serve if a directory is requested.
DirectoryIndex index.html index.php /index.php

# Action lets you define media types that will execute a script whenever
# a matching file is called. This eliminates the need for repeated URL
# pathnames for oft-used CGI file processors.
# Format: Action media/type /cgi-script/location
# Format: Action handler-name /cgi-script/location
Action php5-cgi /bin/php.cgi

# AddHandler allows you to map certain file extensions to "handlers":
# actions unrelated to filetype. These can be either built into the server
# or added with the Action directive (see below)
# To use CGI scripts outside of ScriptAliased directories:
# (You will also need to add "ExecCGI" to the "Options" directive.)
AddHandler php-cgi .php .inc

# Commonly used filename extensions to character sets.
AddDefaultCharset UTF-8

# AddType allows you to add to or override the MIME configuration
AddType 'application/rdf+xml; charset=UTF-8' .rdf
AddType 'application/xhtml+xml; charset=UTF-8' .xhtml
AddType 'application/xhtml+xml; charset=UTF-8' .xhtml.gz
AddType 'text/html; charset=UTF-8' .html
AddType 'text/html; charset=UTF-8' .html.gz
AddType application/octet-stream .rar .chm .bz2 .tgz .msi .pdf .exe
AddType application/vnd.ms-excel .csv
AddType application/x-httpd-php-source .phps
AddType application/x-pilot .prc .pdb
AddType application/x-shockwave-flash .swf
AddType application/xrds+xml .xrdf
AddType text/plain .ini .sh .bsh .bash .awk .nawk .gawk .csh .var .c .in .h .asc .md5 .sha .sha1
AddType video/x-flv .flv

# AddEncoding allows you to have certain browsers uncompress information on the fly. Note: Not all browsers support this.
AddEncoding x-compress .Z
AddEncoding x-gzip .gz .tgz

# DefaultType: the default MIME type the server will use for a document.
DefaultType text/html

# Optionally add a line containing the server version and virtual host
# name to server-generated pages (internal error documents, FTP directory
# listings, mod_status and mod_info output etc., but not CGI generated
# documents or custom error documents).
# Set to "EMail" to also include a mailto: link to the ServerAdmin.
# Set to one of:  On | Off | EMail
ServerSignature Off


Options +ExecCGI -Indexes
DirectoryIndex index.html index.htm index.php
DefaultLanguage en-US
AddDefaultCharset UTF-8
ServerSignature Off

SetEnv PHPRC /webroot/includes
SetEnv TZ America/Indianapolis

SetEnv SERVER_ADMIN webmaster@askapache.com

AddType video/x-flv .flv
AddType application/x-shockwave-flash .swf
AddType image/x-icon .ico

# http://www.htaccesselite.com/addtype-addhandler-action-vf6.html
AddType application/octet-stream .mov .mp3 .zip

# http://askapache.com/htaccess/apache-status-code-headers-errordocument.html
ErrorDocument 400 /e400/
ErrorDocument 401 /e401/
ErrorDocument 402 /e402/
ErrorDocument 403 /e403/
ErrorDocument 404 /e404/

# Handlers be builtin, included in a module, or added with Action directive
# default-handler: default, handles static content (core)
#   send-as-is: Send file with HTTP headers (mod_asis)
#   cgi-script: treat file as CGI script (mod_cgi)
#    imap-file: Parse as an imagemap rule file (mod_imap)
#   server-info: Get server config info (mod_info)
#  server-status: Get server status report (mod_status)
#    type-map: type map file for content negotiation (mod_negotiation)
#  fastcgi-script: treat file as fastcgi script (mod_fastcgi)
# /php/custom-phpini-tips-and-tricks.html

AddHandler cgi-script .cgi .pl .spl

AddHandler application/x-httpd-php .php .htm

AddHandler php-cgi .php .htm

AddHandler phpini-cgi .php .htm
Action phpini-cgi /cgi-bin/php5-custom-ini.cgi

AddHandler fastcgi-script .fcgi
AddHandler php-cgi .php .htm
Action php-cgi /cgi-bin/php5-wrapper.fcgi

AddHandler php-cgi .php .htm
Action php-cgi /cgi-bin/php.cgi

Action image/gif /cgi-bin/img-create.cgi

AddHandler custom-processor .ssp
Action custom-processor /cgi-bin/myprocessor.cgi

# /htaccess/speed-up-sites-with-htaccess-caching.html
<FilesMatch ".(flv|gif|jpg|jpeg|png|ico)$">
Header set Cache-Control "max-age=2592000"
<FilesMatch ".(js|css|pdf|swf)$">
Header set Cache-Control "max-age=604800"
<FilesMatch ".(html|htm|txt)$">
Header set Cache-Control "max-age=600"
<FilesMatch ".(pl|php|cgi|spl|scgi|fcgi)$">
Header unset Cache-Control

# htaccesselite.com/d/use-htaccess-to-speed-up-your-site-discussion-vt67.html
ExpiresActive On
ExpiresDefault A604800
ExpiresByType image/x-icon A2592000
ExpiresByType application/x-javascript A2592000
ExpiresByType text/css A2592000
ExpiresByType text/html A300

<FilesMatch ".(pl|php|cgi|spl|scgi|fcgi)$">
ExpiresActive Off

<FilesMatch ".(html|htm|php)$">
Header set imagetoolbar "no"


Here are some default MOD_REWRITE code examples.

RewriteEngine On
RewriteBase /

RewriteCond %{HTTP_HOST} !^$
RewriteCond %{HTTP_HOST} !^subdomain.askapache.com$ [NC]
RewriteRule ^/(.*)$ http://subdomain.askapache.com/$1 [L,R=301]

RewriteRule ^(.*)/ve/(.*)$ $1/voluntary-employee/$2 [L,R=301]
RewriteRule ^(.*)/hsa/(.*)$ $1/health-saving-account/$2 [L,R=301]

RewriteCond %{REQUEST_FILENAME} !-f  # Existing File
RewriteCond %{REQUEST_FILENAME} !-d  # Existing Directory
RewriteRule . /index.php [L]

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(subdomain.)?askapache.com/.*$ [NC]
RewriteRule ^.*.(bmp|tif|gif|jpg|jpeg|jpe|png)$ - [F]

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(subdomain.)?askapache.com/.*$ [NC]
RewriteRule ^.*.(bmp|tif|gif|jpg|jpeg|jpe|png)$ http://google.com [R]

RewriteRule ^.*$ - [F]

RewriteRule ^(.*)$ /cgi-bin/form-upload-processor.cgi?p=$1 [L,QSA]

RewriteCond %{HTTPS} !=on [NC]
RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R,L]

# http://www.htaccesselite.com/d/htaccess-errordocument-examples-vt11.html
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /error.php [L]

Redirect 301 /2006/oldfile.html http://subdomain.askapache.com/newfile.html
RedirectMatch 301 /o/(.*)$ http://subdomain.askapache.com/s/dl/$1


Examples of protecting your files and securing with password protection.

# Require (user|group|valid-user) (username|groupname)
AuthType basic
AuthName "prompt"
AuthUserFile /.htpasswd
AuthGroupFile /dev/null
Require valid-user

Require valid-user
Allow from
Satisfy Any

<FilesMatch ".(htaccess|htpasswd|ini|phps|fla|psd|log|sh)$">
Order Allow,Deny
Deny from all

SetEnvIfNoCase Referer "^http://subdomain.askapache.com/" good
SetEnvIfNoCase Referer "^$" good
<FilesMatch ".(png|jpg|jpeg|gif|bmp|swf|flv)$">
Order Deny,Allow
Deny from all
Allow from env=good
ErrorDocument 403 http://www.google.com/intl/en_ALL/images/logo.gif
ErrorDocument 403 /images/you_bad_hotlinker.gif

#bytes, 0-2147483647(2GB)
LimitRequestBody 10240000

# /htaccess/apache-ssl-in-htaccess-examples.html
SSLOptions +StrictRequire
SSLRequire %{HTTP_HOST} eq "askapache.com"
ErrorDocument 403 https://askapache.com

<FilesMatch ".(flv|gif|jpg|jpeg|png|ico|js|css|pdf|swf|html|htm|txt)$">
Header set Cache-Control "max-age=5"
AuthType basic
AuthName "Ooops! Temporarily Under Construction..."
AuthUserFile /.htpasswd
AuthGroupFile /dev/null
Require valid-user      # password prompt for everyone else
Order Deny,Allow
Deny from all
Allow from   # Your, the developers IP address
Allow from w3.org      # css/xhtml check jigsaw.w3.org/css-validator/
Allow from googlebot.com   # Allows google to crawl your pages
Satisfy Any        # no password required if host/ip is Allowed

ExpiresDefault A5 #If using mod_expires
<FilesMatch ".(flv|gif|jpg|jpeg|png|ico|js|css|pdf|swf|html|htm|txt)$">
Header set Cache-Control "max-age=5"

AuthType basic
AuthName "Ooops! Temporarily Under Construction..."
AuthUserFile /.htpasswd
AuthGroupFile /dev/null
Require valid-user      # password prompt for everyone else
Order Deny,Allow
Deny from all
Allow from   # Your, the developers IP address
Allow from w3.org      # css/xhtml check jigsaw.w3.org/css-validator/
Allow from googlebot.com   # Allows google to crawl your pages
Satisfy Any        # no password required if host/ip is Allowed


Advanced Mod_Rewrites ^

Here are some specific htaccess
examples taken mostly from my WordPress Password Protection plugin,
which does alot more than password protection as you will see from the
following mod_rewrite examples. These are a few of the mod_rewrite uses
that BlogSecurity declared pushed the boundaries of Mod_Rewrite!
Some of these snippets are quite exotic and unlike anything you may
have seen before, also only for those who understand them as they can
kill a website pretty quick.

Directory Protection ^

Enable the DirectoryIndex Protection, preventing directory index listings and defaulting. [Disable]

Options -Indexes
DirectoryIndex index.html index.php /index.php


Password Protect wp-login.php ^

Requires a valid user/pass to access the login page[401]

<Files wp-login.php>
Order Deny,Allow
Deny from All
Satisfy Any
AuthName "Protected By AskApache"
AuthUserFile /web/askapache.com/.htpasswda1
AuthType Basic
Require valid-user


Password Protect wp-admin ^

Requires a valid user/pass to access any non-static (css, js, images) file in this directory.[401]

Options -ExecCGI -Indexes +FollowSymLinks -Includes
DirectoryIndex index.php /index.php
Order Deny,Allow
Deny from All
Satisfy Any
AuthName "Protected By AskApache"
AuthUserFile /web/askapache.com/.htpasswda1
AuthType Basic
Require valid-user
<FilesMatch ".(ico|pdf|flv|jpg|jpeg|mp3|mpg|mp4|mov|wav|wmv|png|gif|swf|css|js)$">
Allow from All
<FilesMatch "(async-upload).php$">
<IfModule mod_security.c>
SecFilterEngine Off
Allow from All


Protect wp-content ^

Denies any Direct request for files ending in .php with a 403 Forbidden.. May break plugins/themes [401]

RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /wp-content/.*$ [NC]
RewriteCond %{REQUEST_FILENAME} !^.+flexible-upload-wp25js.php$
RewriteCond %{REQUEST_FILENAME} ^.+.(php|html|htm|txt)$
RewriteRule .* - [F,NS,L]


Protect wp-includes ^

Denies any Direct request for files ending in .php with a 403 Forbidden.. May break plugins/themes [403]

RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /wp-includes/.*$ [NC]
RewriteCond %{THE_REQUEST} !^[A-Z]{3,9} /wp-includes/js/.+/.+ HTTP/ [NC]
RewriteCond %{REQUEST_FILENAME} ^.+.php$
RewriteRule .* - [F,NS,L]


Common Exploits ^

Block common exploit requests with 403 Forbidden. These can help alot, may break some plugins. [403]

RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} ///.* HTTP/ [NC,OR]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*?=?(http|ftp|ssl|https):/.* HTTP/ [NC,OR]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*??.* HTTP/ [NC,OR]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*.(asp|ini|dll).* HTTP/ [NC,OR]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*.(htpasswd|htaccess|aahtpasswd).* HTTP/ [NC]
RewriteRule .* - [F,NS,L]


Stop Hotlinking ^

Denies any request for static files (images, css, etc) if referrer is not local site or empty. [403]

RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteCond %{HTTP_REFERER} !^http://www.askapache.com.*$ [NC]
RewriteRule .(ico|pdf|flv|jpg|jpeg|mp3|mpg|mp4|mov|wav|wmv|png|gif|swf|css|js)$ - [F,NS,L]


Safe Request Methods ^

Denies any request not using GET,PROPFIND,POST,OPTIONS,PUT,HEAD[403]

RewriteRule .* - [F,NS,L]


Forbid Proxies ^

Denies any POST Request using a Proxy Server. Can still access site, but not comment. See Perishable Press [403]

RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteRule .* - [F,NS,L]


Real wp-comments-post.php ^

Denies any POST attempt made to a non-existing wp-comments-post.php[403]

RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*/wp-comments-post.php.* HTTP/ [NC]
RewriteRule .* - [F,NS,L]



Denies any badly formed HTTP PROTOCOL in the request, 0.9, 1.0, and 1.1 only[403]

RewriteCond %{THE_REQUEST} !^[A-Z]{3,9} .+ HTTP/(0.9|1.0|1.1) [NC]
RewriteRule .* - [F,NS,L]



any request for a url containing characters other than
“a-zA-Z0-9.+/-?=&” – REALLY helps but may break your site depending
on your links. [403]

RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteCond %{THE_REQUEST} !^[A-Z]{3,9} [a-zA-Z0-9.+_/-?=&]+ HTTP/ [NC]
RewriteRule .* - [F,NS,L]


BAD Content Length ^

Denies any POST request that doesnt have a Content-Length Header[403]

RewriteCond %{HTTP:Content-Length} ^$
RewriteCond %{REQUEST_URI} !^/(wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteRule .* - [F,NS,L]


BAD Content Type ^

Denies any POST request with a content type other than application/x-www-form-urlencoded|multipart/form-data[403]

RewriteCond %{HTTP:Content-Type} !^(application/x-www-form-urlencoded|multipart/form-data.*(boundary.*)?)$ [NC]
RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteRule .* - [F,NS,L]


Missing HTTP_HOST ^

Denies requests that dont contain a HTTP HOST Header.[403]

RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteCond %{HTTP_HOST} ^$
RewriteRule .* - [F,NS,L]


Bogus Graphics Exploit ^

Denies obvious exploit using bogus graphics[403]

RewriteCond %{HTTP:Content-Disposition} .php [NC]
RewriteCond %{HTTP:Content-Type} image/.+ [NC]
RewriteRule .* - [F,NS,L]


No UserAgent, Not POST ^

Denies POST requests by blank user-agents. May prevent a small number of visitors from POSTING. [403]

RewriteCond %{HTTP_USER_AGENT} ^-?$
RewriteCond %{REQUEST_URI} !^/(wp-login.php|wp-admin/|wp-content/plugins/|wp-includes/).* [NC]
RewriteRule .* - [F,NS,L]


No Referer, No Comment ^

Denies any comment attempt with a blank HTTP_REFERER field, highly indicative of spam. May prevent some visitors from POSTING. [403]

RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.*/wp-comments-post.php.* HTTP/ [NC]
RewriteCond %{HTTP_REFERER} ^-?$
RewriteRule .* - [F,NS,L]


Trackback Spam ^

Denies obvious trackback spam. See Holy Shmoly! [403]

RewriteCond %{HTTP_USER_AGENT} ^.*(opera|mozilla|firefox|msie|safari).*$ [NC]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9} /.+/trackback/? HTTP/ [NC]
RewriteRule .* - [F,NS,L]


Map all URIs except those corresponding to existing files to a handler ^

RewriteEngine On
RewriteRule . /script.php


Map any request to a handler ^

the case where all URIs should be sent to the same place (including
potentially requests for static content) the method to use depends on
the type of the handler. For php scripts, use:For other handlers such as
php scripts, use:

RewriteEngine On
RewriteCond %{REQUEST_URI} !=/script.php
RewriteRule .* /script.php


And for CGI scripts: ^

ScriptAliasMatch .* /var/www/script.cgi


Map URIs corresponding to existing files to a handler instead ^

RewriteEngine On
RewriteCond %{REQUEST_URI} !=/script.php
RewriteRule .* /script.php


the existing files you wish to have handled by your script have a
common set of file extensions distinct from that of the hander, you can
bypass mod_rewrite and use instead mod_actions. Let’s say you want all
.html and .tpl files to be dealt with by your script:

Action foo-action /script.php
AddHandler foo-action html tpl


Deny access if var=val contains the string foo. ^

RewriteCond %{QUERY_STRING} foo
RewriteRule ^/url - [F]


Removing the Query String ^

RewriteRule ^/url /url?


Adding to the Query String ^

Keep the existing query string using the Query String Append flag, but add var=val to the end.

RewriteRule ^/url /url?var=val [QSA]


Rewriting For Certain Query Strings ^

Rewrite URLs like http://askapache.com/url1?var=val to http://askapache.com/url2?var=val but don’t rewrite if val isn’t present.

RewriteCond %{QUERY_STRING} val
RewriteRule ^/url1 /url2


Modifying the Query String ^

any single instance of val in the query string to other_val when
accessing /path. Note that %1 and %2 are back-references to the matched
part of the regular expression in the previous RewriteCond.

RewriteCond %{QUERY_STRING} ^(.*)val(.*)$
RewriteRule /path /path?%1other_val%2


Best .htaccess Articles ^

.htaccess for Webmasters ^

Mod_Rewrite URL Rewriting ^

Undocumented techniques and methods will allow you to utilize mod_rewrite at an “expert level” by showing you how to unlock its secrets.

301 Redirects without mod_rewrite ^

Secure PHP with .htaccess ^

Locking down your php.ini and php cgi with .htaccessIf
you have a php.cgi or php.ini file in your /cgi-bin/ directory or other
pub directory, try requesting them from your web browser. If your
php.ini shows up or worse you are able to execute your php cgi, you’ll
need to secure it ASAP. This shows several ways to secure these files,
and other interpreters like perl, fastCGI, bash, csh, etc.

.htaccess Cookie Manipulation ^

Cookie Manipulation in .htaccess with RewriteRuleFresh .htaccess code
for you! Check out the Cookie Manipulation and environment variable
usage with mod_rewrite! I also included a couple Mod_Security .htaccess
examples. Enjoy!

.htaccess Caching ^

Password Protection and Authentication ^

Control HTTP Headers ^

Blocking Spam and bad Bots ^

Block Bad RobotWant
to block a bad robot or web scraper using .htaccess files? Here are 2
methods that illustrate blocking 436 various user-agents. You can block
them using either SetEnvIf methods, or by using Rewrite Blocks.

PHP htaccess tips ^

using some cool .htaccess tricks we can control PHP to be run as a cgi
or a module. If php is run as a cgi then we need to compile it ourselves
or use .htaccess to force php to use a local php.ini file. If it is
running as a module then we can use various directives supplied by that
modules in .htaccess

HTTP to HTTPS Redirects with mod_rewrite ^

HTTP to HTTPS Redirects with mod_rewriteThis is freaking sweet if you use SSL I promise you! Basically instead of having to check for HTTPS using a RewriteCond %{HTTPS} =on
for every redirect that can be either HTTP or HTTPS, I set an
environment variable once with the value “http” or “https” if HTTP or
HTTPS is being used for that request, and use that env variable in the

SSL in .htaccess ^

SetEnvIf and SetEnvIfNoCase in .htaccess ^

Site Security with .htaccess ^

.htpasswd files 640, chmod .htaccess 644, php files 600, and chmod
files that you really dont want people to see as 400. (NEVER chmod 777,
try 766)

Merging Notes ^

The order of merging is:

  1. <Directory> (except regular expressions) and .htaccess done simultaneously (with .htaccess, if allowed, overriding <Directory>)
  2. <DirectoryMatch> (and <Directory ~>)
  3. <Files> and <FilesMatch> done simultaneously
  4. <Location> and <LocationMatch> done simultaneously

My Favorite .htaccess Links ^

are just some of my favorite .htaccess resources. I’m really into doing
your own hacking to get knowledge and these links are all great
resources in that respect. I’m really interested in new or unusual
htaccess solutions or htaccess hacks using .htaccess files, so let me
know if you find one.

NCSA HTTPd Tutorials

Robert Hansen
Here’s a great Hardening HTAccess part 1, part 2, part 3 article that goes into detail about some of the rarer security applications for .htaccess files.

Some very detailed and helpful .htaccess articles, such as the “.htaccess – gzip and cache your site for faster loading and bandwidth saving.”

Stupid .htaccess tricks is probably the best explanation online
for many of the best .htaccess solutions, including many from this
page. Unlike me they are fantastic writers, even for technical stuff
they are very readable, so its a good blog to kick back on and read.
They also have a fantastic article detailing how to block/deny specific requests using mod_rewrite.

a site for… blog security (which is really any web-app security) this
blog has a few really impressive articles full of solid information for
Hardening WordPress with .htaccess among more advanced topics that can
be challenging but effective. This is a good site to subscribe to their
feed, they publish plugin exploits and breakfreerebel core vulnerabilities
quite a bit.

security/unix dude with some incredibly detailed mod_rewrite tutorials,
helped me the most when I first got into this, and a great guy too. See:
Basic Mod_Rewrite Guide, and Advanced Mod_Rewrite Tutorial

Alot of .htaccess tutorials and code. See: Hardening WordPress with Mod Rewrite and htaccess

jdMorgan is the Moderator of the Apache Forum
at WebmasterWorld, a great place for answers. In my experience he can
answer any tough question pertaining to advanced .htaccess usage,
haven’t seen him stumped yet.

The W3C
Setting Charset in .htaccess is very informative.

Holy Shmoly!
A great blogger with analysis of attacks and spam. See: More ways to stop spammers and unwanted traffic.

Apache Week
A partnership with Red Hat back in the 90’s that produced some excellent documentation.

a resource that I consider to have some of the most creative and
ingenious ideas for .htaccess files, although the author is somewhat of a
character 😉 Its a trip trying to navigate around the site, a fun trip.
Its like nothing I’ve ever seen. There are only a few articles on the
site, but the htaccess articles are very original and well-worth a look.
See: htaccess tricks and tips.

Htaccess Directives ^

This is an AskApache.com exclusive you won’t find this anywhere else.

DirectoryMatch, Files, FilesMatch, IfDefine, IfVersion, IfModule,
Limit, LimitExcept, Location, LocationMatch, Proxy, ProxyMatch,
VirtualHost, AcceptMutex, AcceptPathInfo, AccessFileName, Action,
AddCharset, AddDefaultCharset, AddDescription, AddEncoding, AddHandler,
AddInputFilter, AddLanguage, AddOutputFilter, AddOutputFilterByType,
AddType, Alias, AliasMatch, AllowCONNECT, AllowOverride, Anonymous,
Anonymous_Authoritative, Anonymous_LogEmail, Anonymous_MustGiveEmail,
Anonymous_NoUserId, Anonymous_VerifyEmail, AuthAuthoritative,
AuthDBMAuthoritative, AuthDBMGroupFile, AuthDBMType, AuthDBMUserFile,
AuthDigestAlgorithm, AuthDigestDomain, AuthDigestFile,
AuthDigestGroupFile, AuthDigestNcCheck, AuthDigestNonceFormat,
AuthDigestNonceLifetime, AuthDigestQop, AuthDigestShmemSize,
AuthGroupFile, AuthName, AuthType, AuthUserFile, BS2000Account,
BrowserMatch, BrowserMatchNoCase, CacheNegotiatedDocs, CharsetDefault,
CharsetOptions, CharsetSourceEnc, CheckSpelling, ContentDigest,
CookieDomain, CookieExpires, CookieName, CookieStyle, CookieTracking,
CoreDumpDirectory, DAV, DAVDepthInfinity, DAVMinTimeout, DefaultIcon,
DefaultLanguage, DefaultType, DocumentRoot, ErrorDocument, ErrorLog,
ExtFilterDefine, ExtFilterOptions, FancyIndexing, FileETag,
ForceLanguagePriority, ForceType, GprofDir, Header, HeaderName,
HostnameLookups, IdentityCheck, ImapBase, ImapDefault, ImapMenu,
Include, IndexIgnore, LanguagePriority, LimitRequestBody,
LimitRequestFields, LimitRequestFieldsize, LimitRequestLine,
LimitXMLRequestBody, LockFile, LogLevel, MaxRequestsPerChild,
MultiviewsMatch, NameVirtualHost, NoProxy, Options, PassEnv, PidFile,
Port, ProxyBlock, ProxyDomain, ProxyErrorOverride, ProxyIOBufferSize,
ProxyMaxForwards, ProxyPass, ProxyPassReverse, ProxyPreserveHost,
ProxyReceiveBufferSize, ProxyRemote, ProxyRemoteMatch, ProxyRequests,
ProxyTimeout, ProxyVia, RLimitCPU, RLimitMEM, RLimitNPROC, ReadmeName,
Redirect, RedirectMatch, RedirectPermanent, RedirectTemp, RemoveCharset,
RemoveEncoding, RemoveHandler, RemoveInputFilter, RemoveLanguage,
RemoveOutputFilter, RemoveType, RequestHeader, Require, RewriteCond,
RewriteRule, SSIEndTag, SSIErrorMsg, SSIStartTag, SSITimeFormat,
SSIUndefinedEcho, Satisfy, ScoreBoardFile, Script, ScriptAlias,
ScriptAliasMatch, ScriptInterpreterSource, ServerAdmin, ServerAlias,
ServerName, ServerPath, ServerRoot, ServerSignature, ServerTokens,
SetEnv, SetEnvIf, SetEnvIfNoCase, SetHandler, SetInputFilter,
SetOutputFilter, Timeout, TypesConfig, UnsetEnv, UseCanonicalName,
XBitHack, allow, deny, order, CGIMapExtension, EnableMMAP,
ISAPIAppendLogToErrors, ISAPIAppendLogToQuery, ISAPICacheFile,
ISAPIFakeAsync, ISAPILogNotSupported, ISAPIReadAheadBuffer, SSLLog,
SSLLogLevel, MaxMemFree, ModMimeUsePathInfo, EnableSendfile,
ProxyBadHeader, AllowEncodedSlashes, LimitInternalRecursion,
EnableExceptionHook, TraceEnable, ProxyFtpDirCharset,
AuthBasicAuthoritative, AuthBasicProvider, AuthDefaultAuthoritative,
AuthDigestProvider, AuthLDAPAuthzEnabled, AuthLDAPBindDN,
AuthLDAPBindPassword, AuthLDAPCharsetConfig, AuthLDAPCompareDNOnServer,
AuthLDAPDereferenceAliases, AuthLDAPGroupAttribute,
AuthLDAPGroupAttributeIsDN, AuthLDAPRemoteUserIsDN, AuthLDAPURL,
AuthzDBMAuthoritative, AuthzDBMType, AuthzDefaultAuthoritative,
AuthzGroupFileAuthoritative, AuthzLDAPAuthoritative,
AuthzOwnerAuthoritative, AuthzUserAuthoritative, BalancerMember,
DAVGenericLockDB, FilterChain, FilterDeclare, FilterProtocol,
FilterProvider, FilterTrace, IdentityCheckTimeout, IndexStyleSheet,
ProxyPassReverseCookieDomain, ProxyPassReverseCookiePath, ProxySet,
ProxyStatus, ThreadStackSize, AcceptFilter, Protocol,
AuthDBDUserPWQuery, AuthDBDUserRealmQuery, UseCanonicalPhysicalPort,
CheckCaseOnly, AuthLDAPRemoteUserAttribute, ProxyPassMatch,
SSIAccessEnable, Substitute, ProxyPassInterpolateEnv

Htaccess Modules ^

Here are most of the modules that come with Apache. Each one can have new commands that can be used in .htaccess file scopes.

mod_actions, mod_alias, mod_asis, mod_auth_basic, mod_auth_digest, mod_authn_anon, mod_authn_dbd, mod_authn_dbm, mod_authn_default, mod_authn_file, mod_authz_dbm, mod_authz_default, mod_authz_groupfile, mod_authz_host, mod_authz_owner, mod_authz_user, mod_autoindex, mod_cache, mod_cern_meta, mod_cgi, mod_dav, mod_dav_fs, mod_dbd, mod_deflate, mod_dir, mod_disk_cache, mod_dumpio, mod_env, mod_expires, mod_ext_filter, mod_file_cache, mod_filter, mod_headers, mod_ident, mod_imagemap, mod_include, mod_info, mod_log_config, mod_log_forensic, mod_logio, mod_mem_cache, mod_mime, mod_mime_magic, mod_negotiation, mod_proxy, mod_proxy_ajp, mod_proxy_balancer, mod_proxy_connect, mod_proxy_ftp, mod_proxy_http, mod_rewrite, mod_setenvif, mod_speling, mod_ssl, mod_status, mod_substitute, mod_unique_id, mod_userdir, mod_usertrack, mod_version, mod_vhost_alias

Htaccess Software ^

Apache HTTP Server comes with the following programs.

Apache hypertext transfer protocol server
Apache HTTP server control interface
Apache HTTP server benchmarking tool
APache eXtenSion tool
Create and update user authentication files in DBM format for basic authentication
Start a FastCGI program
Clean up the disk cache
Create and update user authentication files for digest authentication
Manipulate DBM password databases.
Create and update user authentication files for basic authentication
Create dbm files for use with RewriteMap
Resolve hostnames for IP-addresses in Apache logfiles
Periodically log the server’s status
Rotate Apache logs without having to kill the server
Split a multi-vhost logfile into per-host logfiles
Switch User For Exec

Technical Look at .htaccess ^

Source: Apache API notes

Per-directory configuration structures ^

look out how all of this plays out in mod_mime.c, which defines the
file typing handler which emulates the NCSA server’s behavior of
determining file types from suffixes. What we’ll be looking at, here, is
the code which implements the AddType and AddEncoding commands. These
commands can appear in .htaccess files, so they must be handled in the
module’s private per-directory data, which in fact, consists of two
separate tables for MIME types and encoding information, and is declared
as follows:

table *forced_types;      /* Additional AddTyped stuff */
table *encoding_types;    /* Added with AddEncoding... */


the server is reading a configuration file, or <Directory>
section, which includes one of the MIME module’s commands, it needs to
create a mime_dir_config structure, so those commands have something to
act on. It does this by invoking the function it finds in the module’s
`create per-dir config slot’, with two arguments: the name of the
directory to which this configuration information applies (or NULL for
srm.conf), and a pointer to a resource pool in which the allocation
should happen.

(If we are reading a .htaccess file, that resource
pool is the per-request resource pool for the request; otherwise it is a
resource pool which is used for configuration data, and cleared on
restarts. Either way, it is important for the structure being created to
vanish when the pool is cleared, by registering a cleanup on the pool
if necessary).

For the MIME module, the per-dir config creation
function just ap_pallocs the structure above, and a creates a couple of
tables to fill it. That looks like this:

void *create_mime_dir_config (pool *p, char *dummy)
mime_dir_config *new = (mime_dir_config *) ap_palloc (p, sizeof(mime_dir_config));

new->forced_types = ap_make_table (p, 4);
new->encoding_types = ap_make_table (p, 4);


suppose we’ve just read in a .htaccess file. We already have the
per-directory configuration structure for the next directory up in the
hierarchy. If the .htaccess file we just read in didn’t have any AddType
or AddEncoding commands, its per-directory config structure for the
MIME module is still valid, and we can just use it. Otherwise, we need
to merge the two structures somehow.

To do that, the server
invokes the module’s per-directory config merge function, if one is
present. That function takes three arguments: the two structures being
merged, and a resource pool in which to allocate the result. For the
MIME module, all that needs to be done is overlay the tables from the
new per-directory config structure with those from the parent:

void *merge_mime_dir_configs (pool *p, void *parent_dirv, void *subdirv)
mime_dir_config *parent_dir = (mime_dir_config *)parent_dirv;
mime_dir_config *subdir = (mime_dir_config *)subdirv;
mime_dir_config *new =  (mime_dir_config *)ap_palloc (p, sizeof(mime_dir_config));
new->forced_types = ap_overlay_tables (p, subdir->forced_types, parent_dir->forced_types);
new->encoding_types = ap_overlay_tables (p, subdir->encoding_types, parent_dir->encoding_types);


a note — if there is no per-directory merge function present, the
server will just use the subdirectory’s configuration info, and ignore
the parent’s. For some modules, that works just fine (e.g., for the
includes module, whose per-directory configuration information consists
solely of the state of the XBITHACK), and for those modules, you can
just not declare one, and leave the corresponding structure slot in the
module itself NULL.

Command handling ^

that we have these structures, we need to be able to figure out how to
fill them. That involves processing the actual AddType and AddEncoding
commands. To find commands, the server looks in the module’s command
table. That table contains information on how many arguments the
commands take, and in what formats, where it is permitted, and so forth.
That information is sufficient to allow the server to invoke most
command-handling functions with pre-parsed arguments. Without further
ado, let’s look at the AddType command handler, which looks like this
(the AddEncoding command looks basically the same, and won’t be shown

char *add_type(cmd_parms *cmd, mime_dir_config *m, char *ct, char *ext)
if (*ext == '.') ++ext;
ap_table_set (m->forced_types, ext, ct);


command handler is unusually simple. As you can see, it takes four
arguments, two of which are pre-parsed arguments, the third being the
per-directory configuration structure for the module in question, and
the fourth being a pointer to a cmd_parms structure. That structure
contains a bunch of arguments which are frequently of use to some, but
not all, commands, including a resource pool (from which memory can be
allocated, and to which cleanups should be tied), and the (virtual)
server being configured, from which the module’s per-server
configuration data can be obtained if required.

Another way in
which this particular command handler is unusually simple is that there
are no error conditions which it can encounter. If there were, it could
return an error message instead of NULL; this causes an error to be
printed out on the server’s stderr, followed by a quick exit, if it is
in the main config files; for a .htaccess file, the syntax error is
logged in the server error log (along with an indication of where it
came from), and the request is bounced with a server error response
(HTTP error status, code 500).

The MIME module’s command table has entries for these commands, which look like this:

command_rec mime_cmds[] =
{ "AddType", add_type, NULL, OR_FILEINFO, TAKE2, "a mime type followed by a file extension" },
{ "AddEncoding", add_encoding, NULL, OR_FILEINFO, TAKE2, "an encoding (e.g., gzip), followed by a file extension" },


a taste of that famous Apache source code that builds the directives
allowed in .htaccess file context, the key that tells whether its
enabled in .htaccess context is the DIR_CMD_PERMS and then the
OR_FILEINFO, which means a directive is enabled dependent on the
AllowOverride directive that is only allowed in the main config. First
Apache 1.3.0, then Apache 2.2.10

mod_autoindex ^
AddIcon, add_icon, BY_PATH, DIR_CMD_PERMS, an icon URL followed by one or more filenames
AddIconByType, add_icon, BY_TYPE, DIR_CMD_PERMS, an icon URL followed by one or more MIME types
AddIconByEncoding, add_icon, BY_ENCODING, DIR_CMD_PERMS, an icon URL followed by one or more content encodings
AddAlt, add_alt, BY_PATH, DIR_CMD_PERMS, alternate descriptive text followed by one or more filenames
AddAltByType, add_alt, BY_TYPE, DIR_CMD_PERMS, alternate descriptive text followed by one or more MIME types
AddAltByEncoding, add_alt, BY_ENCODING, DIR_CMD_PERMS, alternate descriptive text followed by one or more content encodings
IndexOptions, add_opts, DIR_CMD_PERMS, RAW_ARGS, one or more index options
IndexIgnore, add_ignore, DIR_CMD_PERMS, ITERATE, one or more file extensions
AddDescription, add_desc, BY_PATH, DIR_CMD_PERMS, Descriptive text followed by one or more filenames
HeaderName, add_header, DIR_CMD_PERMS, TAKE1, a filename
ReadmeName, add_readme, DIR_CMD_PERMS, TAKE1, a filename
FancyIndexing, fancy_indexing, DIR_CMD_PERMS, FLAG, Limited to 'on' or 'off' (superseded by IndexOptions FancyIndexing)
DefaultIcon, ap_set_string_slot, (void *) XtOffsetOf(autoindex_config_rec, default_icon), DIR_CMD_PERMS, TAKE1, an icon URL


mod_rewrite ^
// mod_rewrite
RewriteEngine, cmd_rewriteengine, OR_FILEINFO, On or Off to enable or disable (default)
RewriteOptions, cmd_rewriteoptions, OR_FILEINFO, List of option strings to set
RewriteBase, cmd_rewritebase, OR_FILEINFO, the base URL of the per-directory context
RewriteCond, cmd_rewritecond, OR_FILEINFO, an input string and a to be applied regexp-pattern
RewriteRule, cmd_rewriterule, OR_FILEINFO, an URL-applied regexp-pattern and a substitution URL
RewriteMap, cmd_rewritemap, RSRC_CONF, a mapname and a filename
RewriteLock, cmd_rewritelock, RSRC_CONF, the filename of a lockfile used for inter-process synchronization
RewriteLog, cmd_rewritelog, RSRC_CONF, the filename of the rewriting logfile
RewriteLogLevel, cmd_rewriteloglevel, RSRC_CONF, the level of the rewriting logfile verbosity (0=none, 1=std, .., 9=max)
RewriteLog, fake_rewritelog, RSRC_CONF, [DISABLED] the filename of the rewriting logfile
RewriteLogLevel, fake_rewritelog, RSRC_CONF, [DISABLED] the level of the rewriting logfile verbosity


The entries in these tables are:

  • The name of the command
  • The
    function which handles it a (void *) pointer, which is passed in the
    cmd_parms structure to the command handler — this is useful in case
    many similar commands are handled by the same function.
  • A bit
    mask indicating where the command may appear. There are mask bits
    corresponding to each AllowOverride option, and an additional mask bit,
    RSRC_CONF, indicating that the command may appear in the server’s own
    config files, but not in any .htaccess file.
  • A flag indicating
    how many arguments the command handler wants pre-parsed, and how they
    should be passed in. TAKE2 indicates two pre-parsed arguments. Other
    options are TAKE1, which indicates one pre-parsed argument, FLAG, which
    indicates that the argument should be On or Off, and is passed in as a
    boolean flag, RAW_ARGS, which causes the server to give the command the
    raw, unparsed arguments (everything but the command name itself). There
    is also ITERATE, which means that the handler looks the same as TAKE1,
    but that if multiple arguments are present, it should be called multiple
    times, and finally ITERATE2, which indicates that the command handler
    looks like a TAKE2, but if more arguments are present, then it should be
    called multiple times, holding the first argument constant.
  • Finally,
    we have a string which describes the arguments that should be present.
    If the arguments in the actual config file are not as required, this
    string will be used to help give a more specific error message. (You can
    safely leave this NULL).

Finally, having set this all up,
we have to use it. This is ultimately done in the module’s handlers,
specifically for its file-typing handler, which looks more or less like
this; note that the per-directory configuration structure is extracted
from the request_rec’s per-directory configuration vector by using the
ap_get_module_config function.

Side notes — per-server configuration, virtual servers, etc. ^

basic ideas behind per-server module configuration are basically the
same as those for per-directory configuration; there is a creation
function and a merge function, the latter being invoked where a virtual
server has partially overridden the base server configuration, and a
combined structure must be computed. (As with per-directory
configuration, the default if no merge function is specified, and a
module is configured in some virtual server, is that the base
configuration is simply ignored).

The only substantial difference
is that when a command needs to configure the per-server private module
data, it needs to go to the cmd_parms data to get at it. Here’s an
example, from the alias module, which also indicates how a syntax error
can be returned (note that the per-directory configuration argument to
the command handler is declared as a dummy, since the module doesn’t
actually have per-directory config data):

Litespeed Htaccess support ^

Unlike other lightweight web servers, Apache compatible per-directory configuration overridden is fully supported by LiteSpeed Web Server.
With .htacess you can change configurations for any directory under
document root on-the-fly, which in most cases is a mandatory feature in
shared hosting environment. It is worth noting that enabling .htaccess support in LiteSpeed Web Server will not degrade server’s performance, comparing to Apache’s 40% drop in performance.

This is part 2 of the exhaustive Htaccess Tutorial.
I realized it was so lengthy that search engines and visitors were
having real problems with it, so I moved half of it here. But this gave
me the opportunity to add a ton of new stuff that I hadn’t been able to
add to the main htaccess tutorial. And now this new part 2 is already
twice as big as the original! To sum up, this is a work in progress.

Moving Half of Original Tutorial to a new URL – SEO Rewriting ^

the original tutorial was at /htaccess/apache-htaccess.html, but it was
such a huge article that search engines were dropping it! So I split it
into 3 new urls.

  1. /htaccess/htaccess.html – 1
  2. /htaccess/htaccess-rewrites.html – Rewrites
  3. /htaccess/apache-htaccess-2.html – Part 2

/htaccess/apache-htaccess-2.html is a lot of unfinished new stuff, and
the /htaccess/htaccess-rewrites.html (this very page) still needs to be
split several times to get the filesize down.

301 Link Juice ^

/apache-htaccess.html url has been around since 2006, and it was full
of original ideas and examples for using htaccess. Almost every htaccess
guide or tutorial published since then has many of the examples and
ideas from that tutorial, (I encourage people to modify and republish
everything on this site according to copyright). I know it, the authors
sometimes know it, but the main point is Google for sure knows it.
That’s the beauty of creating unique content, Google sees that. So that
/apache-htaccess.html link has extreme link juice, from all the sites,
books, papers, and presentations that have linked to it since 2006.

Filesize Is important ^

like to use my blog as a way to keep notes about my research organized,
and I am extremely good at doing research, unfortunately, that means I
have a huge article. The filesize for the html alone is larger than all
the other resources like javascript and images, combined.

  • So that means it is very difficult to view on a mobile device, or a slow connection.
  • For
    google-bot and other search engine crawlers and robots, this is a huge
    problem (I made a big mistake letting it get that size).
  • The
    robot has to parse an enormous single html file, containing
    hundreds/thousands of external and internal links, and its such an issue
    they can decide to just skip indexing that url until the filesize is
  • Once again, it’s all about the human experience, a
    huge single file is not good for anyone who isn’t printing it out to
    read offline.

Starting Fresh without losing juice ^

since the page wasn’t being indexed much since it was so huge, I
decided to split up the content into new separate urls and utilize a 301
Redirect to transfer all the link juice from /apache-htaccess.html to

301 Redirect Timeline ^

  1. Now,
    the idea is to take the first pages from the original multi-page guide,
    do a little improvement on that content, and save it to the new url
  2. Then just continue taking the next page from the
    remaining original guide and creating new pages using the original
    links structure. /apache-htaccess-2.html, /apache-htaccess-3.html, etc..
    This is a secondary backup to the new /htaccess.html url, which will
    receive the 301 link juice from the old url, but these secondary pages
    will help keep the links on external sites good.
  3. Then, I setup a 301 Redirect in my .htaccess file to redirect the old url to the new /htaccess.html url.
  4. Finally,
    I delete the old url and it is replaced forever by a 301 Redirect
    pointing to my new location, filesize problems eliminated.

301 Redirect Code Used ^

is so easy to do with RedirectMatch, way faster and easier than using
mod_rewrite to handle this, and much less overhead. Note that this is a
general command that I will leave up for a few weeks and slowly tighten
it up by looking at my Google Analytics and Apache Logs. For instance,
this first redirectmatch rule is an older RedirectMatch I still have
active to redirect all the old links pointing at
/2006/htaccess/apache-htaccess.html to /htaccess/apache-htaccess.html
from when I ditched the date-based permalinks back in 2007, and you can
see it is a little tighter than the 2nd one which also redirects
requests for apache-htaccess.html/feed/ or trackback or whatever.

RedirectMatch 301 ^/2006/.*apache-htaccess.html$ /htaccess/apache-htaccess.html
RedirectMatch 301 ^/.*apache-htaccess.html(.*)$ /htaccess/htaccess.html$1


Advanced 301 Redirects for SEO ^

wrote a couple of articles that go into detail about maximizing the SEO
with linking and redirects, it remains one of my most helpful articles
to anyone trying to rank higher the right way, the Google way.. SEO Secrets of AskApache Part 2

Tags ^

Original Copyright Notice For This Document:

Except where otherwise noted, content on this site (www.askapache.com) is licensed under a Creative Commons Attribution 3.0 License, just credit with a link.

This site is not supported or endorsed by The Apache Software Foundation (ASF). All software and documentation produced by The ASF is licensed. “Apache” is a trademark of The ASF. NCSA HTTPd.

UNIX ® is a registered Trademark of The Open Group. POSIX ® is a registered Trademark of The IEEE.

September 14th, 2015

The original version of this document can be found at www.askapache.com/htaccess/htaccess.html

Continue Reading