DEV Community

Jesús G. Calderín
Jesús G. Calderín

Posted on • Edited on

Install SSL server and client certificates in Apache Web Server

SSL (Secure Socket Layer) is the technology behind the https urls you can see when browsing secure sites (which is nearly all of them). It's a protocol that allows server and browser (or more generally, the "agent" or just the "client") to establish a "handshake" prior to start exchanging request and responses. Its two main purposes are to enable the client to reliably identify the server and allow both to communicate via an encrypted channel.

During this handshake, the server will present the client with its "certificate", which is a X501-formatted data structure. The way the client will verify that the server is who it claims to be is by checking that this certificate is signed by some organisation it trusts, a list of which it must have available on its side and also checking that the certificate is issued to the same domain we are accessing.

Additionally, the server can ask the client to provide him with a "client certificate" for identification purposes. The server can then use this information to grant or block the client from visiting certain areas of the site or executing certain functions.

We will see below how to

  • activate SSL in our Apache Web Server,
  • create a server certificate for our site,
  • instruct the server to ask the client to provide a certificate,
  • create and install a client certificate in our browser

Prerequisites

1) Verify Linux's builtin Apache Web Server is installed and running:

$ sudo systemctl status apache2
● apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; 
     enabled; vendor preset: enabled)
     Active: active (running) since Sun 2025-09-21 12:43:12 CEST;
Enter fullscreen mode Exit fullscreen mode

2) Although not mandatory, I find convenient to map the ip address of the Linux box containing our Apache server to a DNS name of our choosing, for example I will use:
test-980.com

In Windows, you can do this mapping by editing the file

C:\Windows\System32\drivers\etc\hosts

and adding your <Apache server ip> + blank-space + <domain name>:

# Added by me
172.31.36.52 test-980.com
Enter fullscreen mode Exit fullscreen mode

Now when I call that domain from my Windows host, I will get the home page of my Apache Web Server:

#Before changing C:\Windows\System32\drivers\etc\hosts file:
PS> curl.exe -v http://test-980.com/
* Could not resolve host: test-980.com
* shutting down connection #0
curl: (6) Could not resolve host: test-980.com

#After changing C:\Windows\System32\drivers\etc\hosts file:
PS> curl.exe -v http://test-980.com/
* Host test-980.com:80 was resolved.
* IPv6: (none)
* IPv4: 172.31.36.52
*   Trying 172.31.36.52:80...
* Connected to test-980.com (172.31.36.52) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: test-980.com
> User-Agent: curl/8.14.1
> Accept: */*
>
< HTTP/1.1 200 OK
... full content of the home page follows here ...
Enter fullscreen mode Exit fullscreen mode

3) Create a virtual host for my test-980.com site, so we don't mess around with default virtual host in apache:

$ sudo mkdir -p /var/sites/test-980.com/conf
$ sudo mkdir -p /var/sites/test-980.com/www/html
$ sudo vi /var/sites/test-980.com/www/html/index.html
#add the content shown in the cat command below

$ cat /var/sites/test-980.com/www/html/index.html
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>test-980.com: It works</title>
  </head>
  <body>
        Welcome to test-980.com
  </body>
</html>
$ sudo cp /etc/apache2/sites-available/000-default.conf \
/var/sites/test-980.com/conf/localhost.conf

$ sudo vi /var/sites/test-980.com/conf/localhost.conf
#add the content shown in the cat command below

$ cat /var/sites/test-980.com/conf/localhost.conf
<VirtualHost *:80>
        ServerName test-980.com

        ServerAdmin webmaster@test-980.com
        DocumentRoot /var/sites/test-980.com/www/html

        <Directory /var/sites/test-980.com/www/>
          Options Indexes FollowSymLinks
          AllowOverride None
          Require all granted
        </Directory>

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

#Make our site available
$ sudo ln -s /var/sites/test-980.com/conf/localhost.conf \
/etc/apache2/sites-available/local-test-980.com.conf

#Maker our site enabled:
$ sudo a2ensite local-test-980.com
Enabling site local-test-980.com.
To activate the new configuration, you need to run:
  systemctl reload apache2

$ sudo  systemctl reload apache2
Enter fullscreen mode Exit fullscreen mode

Now, curl will return our new page:

PS> curl.exe http://test-980.com/
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>test-980.com: It works</title>
  </head>
  <body>
        Welcome to test-980.com
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

However, we cannnot access our site via https for the moment:

PS> curl.exe https://test-980.com/
curl: (35) schannel: next InitializeSecurityContext failed:
SEC_E_INVALID_TOKEN (0x80090308) - 
El token proporcionado a la función no es válido
Enter fullscreen mode Exit fullscreen mode

We'll see next how to secure our site via SSL.

Enable SSL on Apache

Run

$ sudo a2ensite default-ssl.conf
Enabling site default-ssl.
To activate the new configuration, you need to run:
  systemctl reload apache2
$ sudo systemctl reload apache2
Enter fullscreen mode Exit fullscreen mode

If we look at the content of /etc/apache2/sites-enabled/default-ssl.conf, we will see that we have done the following:

  • listen on port 443:

<VirtualHost _default_:443>

  • serve the pages under /var/www/html:

DocumentRoot /var/www/html

  • use SSL protocol on this virtual host:

SSLEngine on

  • send the client the following server certificate:

SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem

So, if we now test https, we will get a warning that the certificate is not trusted:

PS> curl.exe https://test-980.com/
curl: (60) schannel: SEC_E_UNTRUSTED_ROOT (0x80090325) - 
La cadena de certificación fue emitida por una entidad en la que no se confía.
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
Enter fullscreen mode Exit fullscreen mode

However, we can force curl to trust the certificate via the -k option:

PS> curl.exe -k https://test-980.com/
   ... full content of page /var/www/html/index.html will follow ...
Enter fullscreen mode Exit fullscreen mode

But we don't want the default page in /var/www/html/index.html to be served, but the page in our test-980.com site, so we have to create a virtual host for our site that speaks https.
We do it as follows:

  • copy default ssl conf:
$ sudo cp /etc/apache2/sites-available/default-ssl.conf \
/var/sites/test-980.com/conf/localhost-ssl.conf
Enter fullscreen mode Exit fullscreen mode
  • adapt it to our site by adding our domain name in ServerName and our html folder in DocumentRoot:
$ cat /var/sites/test-980.com/conf/localhost-ssl.conf
...
ServerName test-980.com
...
DocumentRoot /var/sites/test-980.com/www/html
Enter fullscreen mode Exit fullscreen mode

We set our site available, enable it and reload Apache:

$ sudo ln -s /var/sites/test-980.com/conf/localhost-ssl.conf \
/etc/apache2/sites-available/local-test-980.com-ssl.conf
$ sudo a2ensite local-test-980.com-ssl.conf
$ sudo systemctl reload apache2
Enter fullscreen mode Exit fullscreen mode

If we try again we will get:

PS> curl.exe -k https://test-980.com/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.52 (Ubuntu) Server at test-980.com Port 443</address>
</body></html>
Enter fullscreen mode Exit fullscreen mode

That's because we have to grant access to our home folder in our new ssl conf, in the same way we did in /var/sites/test-980.com/conf/localhost.conf.

So, we have to modify localhost-ssl.conf to add the following Directory element:

$ more /var/sites/test-980.com/conf/localhost-ssl.conf
    ...
    DocumentRoot /var/sites/test-980.com/www/html
    <Directory /var/sites/test-980.com/www/>
      Options Indexes FollowSymLinks
      AllowOverride None
      Require all granted
    </Directory>
    ...
Enter fullscreen mode Exit fullscreen mode

and reload:

$ sudo systemctl reload apache2

Now, we can get our home page via https:

PS> curl.exe -k https://test-980.com/
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>test-980.com: It works</title>
  </head>
  <body>
        Welcome to test-980.com
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Remember we are able to retrieve the page only because we have instructed curl to ignore (via option k) the fact that the certificate presented (or more precisely, the signer of the certificate) is not trusted:

PS> curl.exe -k https://test-980.com/
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>test-980.com: It works</title>
  </head>
  <body>
        Welcome to test-980.com
  </body>
</html>

#Removing the -k
PS> curl.exe https://test-980.com/
curl: (60) schannel: SEC_E_UNTRUSTED_ROOT (0x80090325) - La cadena de certificación fue emitida por una entidad en la que no se confía.
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
Enter fullscreen mode Exit fullscreen mode

We can tell curl to trust the certificate with the option cacert:

PS> curl.exe https://test-980.com --cacert ^
\\wsl.localhost\Ubuntu-22.04\etc\ssl\certs\ssl-cert-snakeoil.pem

curl: (60) schannel: CertGetNameString() failed to match connection hostname (test-980.com) against server certificate names
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
Enter fullscreen mode Exit fullscreen mode

We're not there yet. The reason is that the certificate is issued for a domaine named "DESKTOP-IUMEIDO", which is my computer's name:

$ openssl x509 -in /etc/ssl/certs/ssl-cert-snakeoil.pem -noout -text|\
grep "Subject:"
        Subject: CN = DESKTOP-IUMEID0.
Enter fullscreen mode Exit fullscreen mode

, whereas our request is sent to a different domain (test-980.com). The client is being presented a certificate signed by someone he now trusts, but that is issued to a domain different that the one we are trying to connect to. This is deemed dangerous, hence the message and the refusal to connect.

In next section, we'll see how to create a server certificate for our domain test-980

Create SSL server certificate

First, let's generate a key of length 2048:

$ openssl genrsa -out test-980.com.key 2048

Now we need to create a certificate sign request or csr. We will be asked to provide information about the owner of the certificate such as it's company name, it's country, etc... Only Common Name is mandatory:

$ openssl req -new \
-key test-980.com.key \
-out test-980.com.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:test-980.com
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:.
An optional company name []:.
Enter fullscreen mode Exit fullscreen mode

Finally, we sign the request with the key generated above. This will generate our certificate and place it in the file specified in out:

$ openssl x509 -req -days 365 \
-in test-980.com.csr \
-signkey test-980.com.key \
-out test-980.com.crt
Certificate request self-signature ok
subject=CN = test-980.com
Enter fullscreen mode Exit fullscreen mode

If we list our current folder, we will see the 3 files that we have just generated:

$ ls test-980*
test-980.com.crt  test-980.com.csr  test-980.com.key
Enter fullscreen mode Exit fullscreen mode

Our certificate is the crt file.
We will now copy the crt and the key files inside our test-980.com's site configuration folder and reference them in our ssl conf file:

$ sudo cp test-980.com.crt /var/sites/test-980.com/conf
$ sudo cp test-980.com.key /var/sites/test-980.com/conf

#Modify localhost-ssl.conf to point to our new files:
$ cat /var/sites/test-980.com/conf/localhost-ssl.conf|grep SSLCertificate
SSLCertificateFile      /var/sites/test-980.com/conf/test-980.com.crt
SSLCertificateKeyFile   /var/sites/test-980.com/conf/test-980.com.key

#restart:
$ sudo systemctl stop apache2.service
$ sudo systemctl start apache2.service
Enter fullscreen mode Exit fullscreen mode

Let's now copy the server certificate in our windows home folder (notice I've been using all this time my curl tool in Windows) for the sake of legibility:

PS> cp \\wsl.localhost\Ubuntu-22.04\var\sites\test-980.com\conf\test-980.com.crt .
Enter fullscreen mode Exit fullscreen mode

If we try again, it should work:

PS> curl.exe https://test-980.com --cacert test-980.com.crt
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>test-980.com: It works</title>
  </head>
  <body>
        Welcome to test-980.com
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Create SSL client certificate

We can instruct our server to require a certificate to the client by editing localhost-ssl.conf as follows:

  • uncomment the following line:

SSLVerifyClient require

  • indicate where our signer certificate is with:

SSLCACertificateFile /var/sites/test-980.com/conf/test-980.com.crt

After restarting the server, we can't access our page anymore:

PS> curl.exe https://test-980.com --cacert test-980.com.crt
curl: (56) schannel: failed to read data from server: SEC_E_ILLEGAL_MESSAGE (0x80090326) - This error usually occurs when a fatal SSL/TLS alert is received (e.g. handshake failed). More detail may be available in the Windows System event log.
Enter fullscreen mode Exit fullscreen mode

The message is quite cryptic but it is due to our curl client not providing a certificate.

Let's then create a client certificate for James Bond and sign it with our test-980.com server certificate:

#Step 1: generate key
$ openssl genpkey -algorithm RSA -out james.bond.key

#Step 2: generate request
$ openssl req -new -key james.bond.key -out james.bond.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:.
State or Province Name (full name) [Some-State]:.
Locality Name (eg, city) []:.
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:.
Common Name (e.g. server FQDN or YOUR name) []:BOND.James
Email Address []:.

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:.
An optional company name []:.

#Step 3: generate the certificate and sign it with our test-980.com server 
#certificate (notice that all files in the command are assumed to be in 
#current folder):
$ openssl x509 -req -in james.bond.csr -CA test-980.com.crt \
-CAkey test-980.com.key \
-CAcreateserial \
-out james.bond.crt \
-days 365 \
-sha256
Certificate request self-signature ok
subject=CN = BOND.James

#Step 4: optional
#If we want to import it in our browser or test it via curl in Windows, 
#we might need to convert it to .pfx format:
$ openssl pkcs12 -export -out james.bond.pfx \
-inkey james.bond.key \
-in james.bond.crt
Enter Export Password: mypwd
Verifying - Enter Export Password: mypwd
Enter fullscreen mode Exit fullscreen mode

Now let's copy the .pfx in our Windows machine and try again by providing our client certificate and the password wiht the --cert option:

PS> curl.exe https://test-980.com/ ^
--cacert ./test-980.com.crt ^
--cert james.bond.pfx:mypwd
curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_INTERNAL_ERROR (0x80090304) - No es posible ponerse en contacto con la autoridad de seguridad local
Enter fullscreen mode Exit fullscreen mode

It's not working but it should :( . You might want to check this article for the reasons behind and try your luck, but I rather have my balls torched than messing around with my Windows registry. I will use my Chrome browser instead.

To import the client certificate in Chrome, go to chrome://certificate-manager/clientcerts and from there I'm sure you'll manage.

Now, when accessing our site via https, we will be asked to select a client certificate:

If you select it and click OK, you'll be granted access to the site:

Top comments (0)