Moving to a new server, I also had to setup HTTP access for my Git repositories at git.cweiske.de.
My old server was Debian 8 with Apache 2.2, the new one Debian 9 with Apache 2.4. Simply copying the configuration did not work - I got an error:
AH01630: client denied by server configuration: /usr/lib/git-core/git-http-backend
I did not want to make the whole /usr/lib/git-core/ directory executable, but only git-http-backend. At first I tried it with:
<Files "/usr/lib/git-core/git-http-backend">
Options +ExecCGI
Require all granted
</Files>
- but that did not work. The documentation about files says:
Directives enclosed in a <Files> section apply to any file with the specified name, regardless of what directory it lies in.
So the correct way was to nest a <Files> tag inside a <Directory> tag:
<Directory "/usr/lib/git-core/">
<Files "git-http-backend">
Options +ExecCGI
Require all granted
</Files>
</Directory>
This is the full configuration for the virtual host:
ServerName git.cweiske.de
HeaderName HEADER
DocumentRoot /home/cweiske/www/git.cweiske.de/
Include /etc/apache2/sites-available/cweiske/includes/linkback.conf
Include /etc/apache2/sites-available/cweiske/includes/letsencrypt-acme-challenges.conf
SetEnv GITWEB_CONFIG /home/cweiske/www/git.cweiske.de/gitweb.conf
ScriptAlias /gitweb.cgi /usr/share/gitweb/gitweb.cgi
#smart http protocol
# by default allows GET only, not push.
# we serve it over the same URLs as the normal gitweb URLs!
SetEnv GIT_PROJECT_ROOT /var/lib/git-public/
ScriptAliasMatch \
"(?x)^/(.*git/(HEAD | \
info/refs | \
objects/(info/[^/]+ | \
[0-9a-f]{2}/[0-9a-f]{38} | \
pack/pack-[0-9a-f]{40}\.(pack|idx)) | \
git-(upload|receive)-pack))$" \
/usr/lib/git-core/git-http-backend/$1
CustomLog /var/log/apache2/cweiske/git.cweiske.de-access.log combined
ErrorLog /var/log/apache2/cweiske/git.cweiske.de-error.log
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/git.cweiske.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/git.cweiske.de/privkey.pem
ServerSignature Off
Options +FollowSymLinks +ExecCGI
AddHandler cgi-script .cgi
Require all granted
DirectoryIndex /gitweb.cgi
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.* /gitweb.cgi/$0 [L,PT]
Options +ExecCGI
Require all granted
Require all denied
]]>
PHP allows us to pack up whole applications in a single .phar file, but no web server software today lets PHP handle .phar files, giving you a download dialog or displaying the text contents of the file.
News about this topic in 2017: Webserver .phar handling lands in distributions.
Open /etc/apache2/mods-enabled/php5.conf and change the line
]]>
into
]]>
Corresponding bug report and patch: #639268
Edit /etc/httpd/conf.d/php.conf and add the line
Corresponding bug report: #1117140
Open /etc/apache2/other/php5.conf and add the line
AddType application/x-httpd-php .phar
below the similar line ending with .php.
Use the following configuration to make PHP-FPM handle .php and .phar files:
Be aware of PHP bug #67587. You will get an endless redirection loop for all requests with a non-empty PATH_INFO (file.phar/foo). The fix is already committed and will released with PHP 5.6.0, 5.5.15 and 5.4.31.
One online shop at work saw a drastic performance loss after upgrading: Even accessing static .js files took at least 0.7 seconds.
Upon debugging we saw that the slowness went away when we disabled Apache's RewriteEngine. That led us to inspect each RewriteRule, and in the end we found that this one was the troublemaker:
RewriteRule ^(.*)/(.*){1,15}/(.*)\.html$ http://../$2 [R=301,L]
The second * is the culprit here; it's not needed for the intended function and makes the regex the slower the longer the input URL is.
The totally correct rule would have been:
RewriteRule ^[^/]+/[^/]{1,15}/[^/]+\.html$ http://../$2 [R=301,L]
Your browser knows a list of Certificate Authorities (CAs) that it trusts; this list is called the "trust store". On Firefox 41 you can find it at Preferences / Advanced / Certificates / View Certificates / Authorities.
When connecting to a web site via HTTPS, the web server sends the public part of the web site's SSL certificate to your browser. The browser then checks if the certificate is signed by one of the CAs in its trust store.
Browser -----[asks]----> Trust store +----------------------+ +-------------+ | SSL certificate | |Do we trust | |valid for: example.org| | 0x123456789 | |signed by: 0x123456789| | ? | +----------------------+ +-------------+ ]]>
If it finds the certificate in the trust store, all is fine and the green lock icon is shown. If it does not find a trusted CA certificate, a warning is shown:
This Connection is Untrusted
You have asked Firefox to connect securely to example.org, but we can't confirm that your connection is secure.
Normally, when you try to connect securely, sites will present trusted identification to prove that you are going to the right place. However, this site's identity can't be verified.
What Should I Do?
If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn't continue.
example.org uses an invalid security certificate.
The certificate is not trusted because the issuer certificate is unknown. The server might not be sending the appropriate intermediate certificates. An additional root certificate may need to be imported.
(Error code: sec_error_unknown_issuer)
In reality, the web site's certificate will not be signed by the CA's certificate directly.
Web site certificate signing at CAs is done automatically these days. Imagine what happens when the CA's certificate gets stolen - the certificate's new owners would be able to issue certificates for all domain names, and the CA could do nothing against it - except telling browser vendors to remove its certificate from the trust store, which would mean that the CA could close its doors.
Instead, the certificate authority's main certificate (root certificate) is locked away on some offline storage medium in a safe. It was only used to sign some intermediate certificates, which have a limited life span, and which can be distrusted and revoked in case they get compromised.
|valid for: signing |--->|valid for: signing | |signed by: 0x112345678| |signed by: 0x223456789 | |in browser trust store| +----------------------+ +------------------------+ +----------------------+ ]]>
These intermediate certificates are used to sign web site certificates - but the browsers do not know about them. If your web server only sends out the domain's SSL certificate, the browser will show the "untrusted connection" warning, because it does only see that the certificate is signed by the intermediate certificate - which it does not know anything about it; it cannot make the connection to the CA's root certificate.
The solution to this problem is to send a certificate chain that contains both the web site's certificate as well as the intermediate certificate. With that information, the browser can follow the trust chain from the web site's certificate via the intermediate certificate up to the root certificate which is has in its trust store.
The Mozilla Certificate Authority FAQ writes about this :
Why does SSL handshake fail due to missing intermediate certificate?
This type of error indicates that the web server is incorrectly configured. The web server itself has to send the intermediate certificate along with their own SSL cert to complete the certificate chain. Only root certificates or trust anchors are included in the Mozilla root store.
Since Apache 2.4.8 you can use the SSLCertificateFile directive to point to a file that contains both the web site's certificate and the intermediate certificate. Versions lower than 2.4.8 have to use the SSLCertificateChainFile directive.
Simply concatenate first the website certificate, then the intermediate certificate into the file:
$ cat /path/to/example.org.pem > /path/to/example.org-chain.pem
$ cat /path/to/intermediate.pem >> /path/to/example.org-chain.pem
Then add the following code to your virtual host configuration:
SSLCertificateFile "/path/to/example.org-chain.pem"
SSLCertificateKeyFile "/path/to/example.org.key"
At work, our intranet search engine indexes meeting minutes and documentation we wrote in reStructuredText format. Finding and clicking such files results in plain text being shown in the browser, not always easy to read.
My goal last friday was to make Apache automatically convert .rst files to HTML when a browser accesses the file.
The python-docutils package contains rst2html which does what I wanted: Read rST from stdin and write HTML to stdout.
Apache on the other hand has mod_ext_filter which was made for piping content through an external tool. The setup was easy:
SetOutputFilter rst2html
]]>
This works fine but gives everyone HTML, even the tools that only understand plain text. Luckily, we can detect if a HTTP client supports HTML by reading the Accept header. mod_ext_filter is able to conditionally turn on filters based on environment variables, and with mod_setenvif we have a facility to set environment variables based on HTTP headers:
SetEnvIf Accept text/html supports_html
SetOutputFilter rst2html
]]>
The HTTP support check is very basic; we do not parse quality values at all. It suffices for now, but I'd also like a better solution.
If you know a better solution, please send a mail.
Today at work I needed to add a SSLProxyEngine directive to my Puppet server configuration.
Unfortunately the puppetlabs/apache module does not support this directive without enabling SSL on the vhost, so I had to manually generate the config file.
After an hour, I had the configuration in place but starting Apache gave me the following output:
$ /etc/init.d/apache2 restart
[....] Restarting web server: apache2no listening sockets available,
shutting down Unable to open logs
Action 'start' failed.
The Apache error log may have more information.
failed!
The log directory /var/log/apache2/ was really not owned by www-data, so I fixed that. I tried to fix many more things, and finally strace'ed the apache process: Still the same error message, but not a single access to a log directory or file.
The real cause of the problem was that my apache configuration did not include a Listen directive, which meant that my vhost, although defined, was not used.
Still: a message Unable to open logs is hardly proper for a missing Listen line in the config.
Imagine you visit a web site and are instantly and automatically logged in.
Without filling in username and password in a login form.
Without filling the OpenID field and clicking 3 times.
Without clicking the button of your browser's autologin extension.
Without a single cookie sent from your browser to the server.
Yet, you authenticate yourself at and get authorized by the web server.
Yes, this is possible - with SSL client certificates. I use them daily to access my self-hosted online bookmark manager and feed reader.
During the last weeks I spent quite some time implementing SSL Client Certificate support in SemanticScuttle, and want to share my experiences here.
Client certificates are, as the name indicates, installed on the client - that is the web browser - and transferred to the server when the server requests them and the user agrees to send it.
The certificates are issued by a Certificate Authority (CA), that is a commercial issuer, a free one like CAcert.org, your company or just you yourself, thanks to the power of the openssl command line tool (or a web frontend like OpenCA).
The CA is responsible for giving you a client certificate and a matching private key for it. The client certificate itself is sent to the server, while the private key is used to sign the request. This signature is verified on the server side, so the server knows that you are really the one that the certificate belongs to.
Note that client certificates can only be used when accessing the server with HTTPS.
The certificates also have an expiration date, after which they are not valid anymore and need to be renewed. When implementing access control, we need to take this into account.
Your web server must be configured for HTTPS, which means you need a SSL server certificate. Get that working first, before tackling the client certificates.
Until some years ago, there was a rule "one port, one certificate". You could only run one single HTTPS website on a single port on the server, except in certain circumstances. The HTTPS port is 443, and using another port for the next SSL-secured domain means to get problems with firewalls and much harder linking - just using "https://example.org/" does not work anymore, you need some port number in it which you - as a visitor - don't know in advance.
The "special circumstances" were wildcard domains and the assumption that you only wanted to secure subdomains: app1.example.org, app2.example.org etc.
The problems lie in the foundations of SSL: SSL certificate exchange is being made before any HTTP protocol data are submitted, and since the certificate contains the domain name, you cannot deliver the correct certificate when you have several SSL hosts on the same port.
Fast-forward to now. We have SNI which solves the problem and gives nobody an excuse anymore to not have SSL secured domains.
With SNI, the browser does send (indicate) the host name it wants to contact during certificate exchange, which causes the server to return the correct certificate.
All current browsers on current operating systems support that. Older systems with Windows XP or OpenSSL < 0.98f do not support it and will get the certificate of the first SSL host.
I assume you're going to get the certificate from CAcert.
First, generate a Certificate Signing Request with the CSR generator. Store the key file under
/etc/ssl/private/bookmarks.cweiske.de.key
Use the the .csr file and the CAcert web interface to generate a signed certificate. Store it as
/etc/ssl/private/bookmarks.cweiske.de-cacert.pem
Now fetch both official CAcert certificates (root and class 3) and put both together into
/etc/ssl/private/cacert-1and3.crt
A basic virtual host configuration with SSL looks like this:
ServerName bookmarks.cweiske.de
LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon
CustomLog /var/log/apache2/access_log vcommon
VirtualDocumentRoot /home/cweiske/Dev/html/hosts/bookmarks.cweiske.de
AllowOverride all
SSLEngine On
SSLCertificateFile /etc/ssl/private/bookmarks.cweiske.de-cacert.pem
SSLCertificateKeyFile /etc/ssl/private/bookmarks.cweiske.de.key
SSLCACertificateFile /etc/ssl/private/cacert-1and3.crt
]]>
Apart from that, you might need to enable the SSL module in your webserver, i.e. by executing
$ a2enmod ssl
Restart your HTTP server. You should be able to request the pages via HTTPS now.
A web server does not require any kind of client certificate by default; this is something that needs to be activated.
The client certs may be required or optional, which leaves you the comfortable option to let users login normally via username/password or with SSL certificates - just as they wish.
Modify your virtual host as follows:
SSLVerifyClient optional
SSLVerifyDepth 1
SSLOptions +StdEnvVars
]]>
There are several options you need to set:
You may choose optional or require here. optional asks the browser for a client certificate but accepts if the browser (the user) does choose not to send any certificate. This is the best option if you want to be able to login with and without a certificate.
The setting require makes the web server terminate the connection when no client certificate is sent by the browser. This option may be used when all users have their client certificate set.
If you want to allow self-signed certificates that are not signed by one of the official CAs, use SSLVerifyClient optional_no_ca.
Your client certificate is signed by a certificate authority (CA), and your web server trusts the CA specified in SSLCACertificateFile. CA certificates itself may be signed by another authority, i.e. like
CAcert >> your own CA >> your client certificate
In this case, you have a higher depth. For most cases, 1 is enough.
This makes your web server pass the SSL environment variables to PHP, so that your application can detect that a client certificate is available and read its data.
In case you need the complete client certificate, you have to add +ExportCertData to the line.
This multiplies the size of data exchanged between the web server process and PHP, which is why it's deactivated most times.
If you restart your web server now, it will request a client certificate from you.
It may happen that the browser does not pop up the cert selection dialog. The web server may send a list of CAs that it considers valid to the browser. If the browser does not have a certificate from one of those CAs, it does not display the popup.
You can fix this issue by setting SSLCADNRequestFile or SSLCADNRequestPath .
Thanks to Gerard Caulfield for bringing this to my attention.
With Apache, you may use SSL client certificate details in your log files: Create a new log format and use the SSL client environment variables :
%{SSL_CLIENT_S_DN_Email}e %{SSL_CLIENT_M_SERIAL}e
Thanks to Hans Schou for this idea.
Let's collect the requirements for SSL client cert support in a typical PHP application:
When a client certificate is available, the $_SERVER variable contains a bunch of SSL_CLIENT_* variables .
$_SERVER['SSL_CLIENT_VERIFY'] is an important one. Don't use the certificate if it does not equal SUCCESS. When no certificate is passed, it is NONE.
Another important variable is SSL_CLIENT_M_SERIAL with the serial that uniquely identifies a certificate from a certain Certificate Authority.
All variables with SSL_CLIENT_I_* are about the issuer, that is the CA. SSL_CLIENT_S_* are about the "subject", the user that sent the client certificate.
SSL_CLIENT_S_DN_CN for example contains the user's name - "Christian Weiske" in my case - which can be used during registration together with SSL_CLIENT_S_DN_Email to give a smooth user experience.
Always remember that you need to configure your web server to pass the variables to your PHP process.
Associating a user account with a client certificate is brutally possible by just enabling +ExportCertData and storing the client certificate in your user database.
This is bad for two reasons:
According to the PostgreSQL manual ,
The combination of certificate serial number and certificate issuer is guaranteed to uniquely identify a certificate (but not its owner — the owner ought to regularly change his keys, and get new certificates from the issuer).
So we can use SSL_CLIENT_M_SERIAL together with SSL_CLIENT_I_DN to uniquely identify a certificate, without storing its as a whole.
A "renewed" certificate is in reality a new certificate with a new serial number. Thus the serial changes after renewal.
The combination of issuer + SSL_CLIENT_S_DN_Email should be more stable; letting the user login even when he renewed his certificate - but only if he didn't change his email address.
SemanticScuttle uses the following code to check if a certificate is valid:
While you can calculate a hash over serial and issuer DN, it's better to store each of the values separately in the database. This has the advantage that you may display them in the user's certificate list, and allow him to identify the cert later on.
Here is the database table structure of SemanticScuttle:
Apart from the two required columns, we also store the subject's name and email to again allow better certificate identification:
Certificate list in SemanticScuttle
The user needs to be able to manually register his current certificate with the application. Automatic registration without confirmation may not be desired, so the best is to just drop that idea.
SemanticScuttle offers a "register current certificate" button on the user profile page:
No certificates and a registration button in SemanticScuttle
Another option that improves usability is to associate the client certificate upon user registration. There should be a checkbox which alerts the user of that detail and allows him to disable it.
The following code registers the current certificate with the user's account in SemanticScuttle:
getTableName()
. ' '. $this->db->sql_build_array(
'INSERT', array(
'uId' => $userId,
'sslSerial' => $_SERVER['SSL_CLIENT_M_SERIAL'],
'sslClientIssuerDn' => $_SERVER['SSL_CLIENT_I_DN'],
'sslName' => $_SERVER['SSL_CLIENT_S_DN_CN'],
'sslEmail' => $_SERVER['SSL_CLIENT_S_DN_Email']
)
);
]]>
You might want to store the certificate's expiration date and automatically remove them when it is reached.
The Online Certificate Status Protocol allows you to check if a certificate has been revoked. Revocation is useful if your certificate has been compromised somehow; it could be stolen or the password gotten public.
A certificate contains information about the CA's OCSP server, you can view it by running
$ openssl x509 -text -in /path/to/cert.pem
...
Authority Information Access:
OCSP - URI:http://ocsp.cacert.org
...
This information is not available in the standard SSL environment variables but need to be extracted from the certificate data, which means that +ExportCertData needs to be enabled.
PHP's OpenSSL extension does not have a method to generate, send or evaluate OCSP requests, so checking the certificate is only possible with the openssl ocsp commandline tool or by implementing it yourself.
The command line tool is easy to use after you stored the client certificate on disk. At first we need the URL
$ openssl x509 -in /tmp/client-cert.pem -noout -text |grep OCSP OCSP - URI:http://ocsp.cacert.org
Now that the URL is ours, it can be queried:
$ openssl ocsp -CAfile /etc/ssl/private/cacert-1and3.crt\
-issuer /etc/ssl/private/cacert-1and3.crt\
-cert /tmp/client-cert.pem\
-url http://ocsp.cacert.org
Example output of a revoked certificate:
At the time of writing, there sadly does not seem to be any PHP library that eases verifying SSL client certificates.
Apache since version 2.3 is able to do the OCSP checks itself if you activate the SSLOCSPEnable option.
Using Apache's HTTP Basic authentication is a quick and easy way to protect your website with username and passwords:
AuthType Basic
AuthName "Please enter username and password"
AuthUserFile /var/www/hostname/.htpasswd
Require valid-user
The problem I have is that AuthUserFile is not relative to the virtual host's document root, but to the server root, which is /etc/apache2/ on Debian and Ubuntu.
Putting .htpasswd files into /etc/apache2/ is weird (and requires root access), and the only other option is to make the AuthUserFile path absolute, e.g. /var/www/hostname/.htpasswd.
Absolute paths are not portable across servers - on my dev machine, the path is /home/cweiske/Dev/html/hostname/.htpasswd, while on the server in the data center it is /home/cweiske/www/hostname/.htpasswd.
Luckily, Apache provides the IfDefine directive. It's like an if statement in a programming language, but is limited to parameters that are passed to the httpd process as command line options, e.g. /usr/sbin/apache2 -Dfoo.
Now Apache is started via an init script, not via command line. Modifying the init script is also not something you want since it'll get overwritten with the next package update.
After digging around the apache2 init and configuration files, I found /etc/apache2/envvars: It allows you to set environment variables that get passed to the apache process on startup. One of these variables is APACHE_ARGUMENTS which apache2ctl passes as command line argument to the httpd process.
So the solution I am using now is the following. On my development machine, I define a development variable and activate the password check if that is not set.
I do not define a live variable because I don't always have access to the server configuration, and it also has the advantage that the password protection does still work when the server config gets reset somehow.
export APACHE_ARGUMENTS=-Ddevelopment
AuthType Basic
AuthName "Please enter username and password"
AuthUserFile /var/www/hostname/.htpasswd
Require valid-user
]]>
Our Apache web server handles more than two dozen virtual hosts, and we are all keen to see statistics about our visitors: From where they are, which pages are most frequented, which search terms are used to find us and so on. A nice tool to analyze the log files and generate HTML statistic pages from them is AWStats.
The stock AWStats on Debian is unfortunately only useful for a single vhost. It further does not keep the full historic data (detailled montly stats) and does not generate index files for them (since it doesn't need to).
To help with that situation, I built my awstats-helper scripts. Written in PHP, they do the following things:
$ cd /usr/local/src $ git clone git://git.cweiske.de/awstats-helper.git $ cd awstats-helper
First, copy config.php.dist to config.php and adjust it. Usually you only need to change the htmldir setting and point it to the directory you want the HTML files generated in.
Then create a awstats config file for each vhost in /etc/awstats/ and call it awstats.foo.example.org.conf. Put the following lines in it:
Include "/etc/awstats/awstats.conf" LogFile="/var/log/apache2/foo.example.org-access.log" SiteDomain="foo.example.org" DefaultFile=""
Now the initial setup is done and we can run the initial data collection:
$ php update-sites.php $ php create-historic.php
If that worked out, add two cronjobs: One to generate today's month's data every couple of hours, and one to finish last month's data at the next month:
$ crontab -e # enter the following: 5 */6 * * * php /usr/local/src/awstats-helper/update-sites.php 5 3 1 * * php /usr/local/src/awstats-helper/update-lastmonth.php
This updates the current data every 6 hours and updates last month's data 3:05 on the first of each month.
That's it! Happy analyzing.
Sometimes applications provide a HTTP interface but run standalone without a dedicated web server software like Apache or nginx. If you're already running such a HTTP server, port 80 is already in use and cannot be used by the application.
Using non-standard ports to expose the application to the world is possible, but makes firewall management harder and prevents some people from using your service because their company's firewall does not allow the non-standard port.
The applications often do not provide HTTPS, authentication or name based hosting support, so there are many reasons to put them behind a real web server that has everything.
What we need is the main web server to tunnel the HTTP requests to the applications and send the responses back to the visitor.
At first, enable the HTTP proxy module in Apache (example for Debian):
$ a2enmod proxy_http
Then add a virtual host with the following configuration:
ServerName application.example.org ProxyRequests OffOrder deny,allow Allow from all ProxyPass / http://localhost:8082/ ProxyPassReverse / http://localhost:8082/ CustomLog /var/log/apache2/application.example.org-access.log combined ErrorLog /var/log/apache2/application.example.org-error.log ]]>
Restart your web server. Fin.
You just set up a reverse proxy that tunnels client requests to http://application.example.org to the application listening on the local port 8082, and back. The application on port 8082 may be listening on the loopback interface only for more security.