Note this bundle is in active development (it is not ready for the moment).
The ACME protocol is a protocol defined by the Let's Encrypt Certificate Authority. You can see the complete specification on https://letsencrypt.github.io/acme-spec/.
The ACME PHP project aims to implement the ACME protocol in PHP to be able to use it easily in various projects.
This repository is the Symfony bundle based on the PHP library.
Require the acmephp/symfony-bundle
with composer Composer.
$ composer require acmephp/symfony-bundle
Enable the bundle in the kernel:
<?php
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new AcmePhp\Bundle\AcmePhpBundle(),
// ...
);
}
Below is a minimal example of the configuration necessary to use the
AcmePhpBundle
in your application:
# app/config/config.yml
acme_php:
contact_email: [email protected]
default_distinguished_name:
country: FR
state: France
locality: Paris
organization_name: MyCompany
organization_unit_name: IT
domains:
myapp.com: ~
Now that you have activated and configured the bundle, all that is left to do is import the AcmePhpBundle routing files.
# app/config/routing.yml
acme_php:
resource: "@AcmePhpBundle/Resources/config/routing.xml"
Once the bundle is installed and configured, you can request your certificates
by runnig the command acmephp:generate
$ ./bin/console acmephp:generate
The first time you run this command, AmePhpBundle will request a new
certificate to the configured Certificate Autority (default
Letsencrypt) and store the generated certificate in
the configured folder (default ~/.acmephp
).
Each time you run the command acmephp:generate
the certificate will be renew
with a lifetime of 90 days (defined by the Certificate Autority). You can add
a crontab to perform this task.
$ crontab -e
0 0 1 * * /var/www/my_app/bin/console acmephp:generate
After regenerating a certificate you have to reload the web server to take the changes into account.
If you use a dedicated cron file in
/etc/cron.d/
be carrefull of the certificate storage location (configured by default in the$HOME
directory) which is related to the user who run the command.
# app/config/config.yml
acme_php:
# Certificates locations. Default: `~/.acmephp`
# Beware to use a directory readable by the web server
# It should be writable too, to store certificates, keys and challenges.
certificate_dir: ~/.acmephp
# Certificate Authority used to deliver certificates. Default: `letsencrypt`. Available values : `letsencrypt`
# You can use your own Certificate Authority by :
# - implementing the CertificateAuthorityConfigurationInterface interface
# - registering the service with the tag "acme_php.certificate_authority" and with an alias to use here
certificate_authority: letsencrypt
# Email addresse associated to the account used to generate certificate
contact_email: [email protected]
# Default Distinguished Name (or a DN) informations used to request certificates.
default_distinguished_name: # https://scotthelme.co.uk/setting-up-le/
# Country Name (2 letter code)
country: FR
# State or Province Name (full name)
state: France
# Locality Name (eg, city)
locality: Paris
# Organization Name (eg, company)
organization_name: MyCompany
# Organizational Unit Name (eg, section)
organization_unit_name: IT
# Email Address. When missing, the adresse defined in the parameter contact_email will be used
email_address: [email protected]
# List of domains to request
domains:
myapp.com: ~
www.myapp.com: ~
invoice.myapp.com:
# You can override default distinguished name define above for each domain
organization_unit_name: sales
You can both, request and renew a certificate with the single commande
$ bin/console acmephp:generate
The certificates will be stored in the folder defined by the parameter certificate_dir
.
$ tree ~/.acmephp
├── account # Your account's keys
│ ├── private.pem
│ └── public.pem
├── challenges # Pending challenges
├── domains # Contains all domain's certificate (1 sub directory per domain)
│ ├── company.com # Contains the certificates for domain `company.com`
│ │ ├── cert.pem # Server certificate only
│ │ ├── chain.pem # All certificates that need to be served by the browser **excluding** server certificate
│ │ ├── combined.pem # All certificates plus private key
│ │ ├── fullchain.pem # All certificates, **including** server certificate
│ │ ├── private.pem # Private key for the certificate
│ │ └── public.pem # Public key for the certificate
│ └── www.company.com
│ └── ...
└── domains-backup # Previous versions of the certificates
├── company.com # domains as subdirectory
│ └── 20160314-144416 # each subdirectory is a backup
│ └── ...
└── www.company.com
└── ...
If your application use monolog (which should be the case by default), Acme PHP Symfony Bundle will log in a acme_php
channel. By this way, you can handle logs and being notified on renewal failure.
Here is a sample of configuration
# app/config/config.yml
monolog:
handlers:
certificate_slack:
type: slack
token: # Your slack's token : https://api.slack.com/web
channel: "#production" # name of the slack's channel
bot_name: CertificatesBot
icon_emoji: lock_with_ink_pen
level: NOTICE
channels: [acme_php]
Certificate Authority: You can add your own certificate authority by implementing the interface
AcmePhp\Bundle\Acme\CertificateAuthority\Configuration\CertificateAuthorityConfigurationInterface
and adding the
service with tag acme_php.certificate_authority
as follow :
# app/config/services.yml
services:
app.custom_certificate_authority:
class: AppBundle\Acme\CustomConfiguration
public: false
tags:
- name: acme_php.certificate_authority
alias: custom
You just have to reference it in your configuration
# app/config/config.yml
acme_php:
certificate_authority: custom
Domain Loader: By default, the bundle loads domain's configurations through the config's file. But, you can add
your own loader by implementing the interface AcmePhp\Bundle\Acme\Domain\Loader\LoaderInterface
and adding the service
with tag acme_php.domains_configurations_loader
as follow:
# app/config/services.yml
services:
app.custom_certificate_authority:
class: AppBundle\Acme\CustomLoader
public: false
tags:
- name: acme_php.domains_configurations_loader
Certificate Formatter: When a new certificate is requested, it is dumped in several formats (cert.pem,
chain.pem, combined.pem, ...). You can add your own formatter by implementing the interface
AcmePhp\Bundle\Acme\Certificate\Formatter\FormatterInterface
and adding the service with tag
acme_php.certificate_formatter
as follow:
# app/config/services.yml
services:
app.custom_certificate_authority:
class: AppBundle\Acme\CustomFormatter
public: false
tags:
- name: acme_php.certificate_formatter
Events: AcmePhpBundle triggers many events:
- CERTIFICATE_REQUESTED: When a certificate is requested on renewed
- CHALLENGE_REQUESTED: When a new challenge is requested
- CHALLENGE_CHECKED: When the challenge is checked (whether or not it have been accepted)
- CHALLENGE_ACCEPTED: When the challenge have been accepted
- CHALLENGE_REJECTED: When the challenge have been rejected
Here are some basic configurations for the common web servers.
Mozilla provide a tool to generate more advance configuration: https://mozilla.github.io/server-side-tls/ssl-config-generator/
You can also check your online certificate with this tool: https://www.ssllabs.com/ssltest/
### Apache 2.2
# /etc/apache2/sites-available/<domain>.
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /var/www/.acmephp/domains/<domain>/cert.pem
SSLCertificateKeyFile /var/www/.acmephp/domains/<domain>/private.pem
SSLCACertificateFile /var/www/.acmephp/domains/<domain>/chain.pem
...
</VirtualHost>
$ sudo service apache2 reload
### Apache 2.4
# /etc/apache2/sites-available/<domain>.
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /var/www/.acmephp/domains/<domain>/fullchain.pem
SSLCertificateKeyFile /var/www/.acmephp/domains/<domain>/private.pem
...
</VirtualHost>
$ sudo service apache2 reload
### Nginx
# /etc/nginx/sites-available/<domain>.
server {
listen 443 ssl default_server;
server_name my-domain;
ssl_certificate /var/www/.acmephp/domains/<domain>/fullchain.pem
ssl_certificate_key /var/www/.acmephp/domains/<domain>/private.pem
...
}
$ sudo service nginx reload
# /etc/haproxy/haproxy.cfg
frontend www
bind :80
bind :443 ssl crt /var/www/.acmephp/domains/<domain>/combined.pem
...
$ sudo service haproxy reload
Unit tests
composer install
./bin/phpunit
Functionnal testing
composer install
docker run -d --net host acmephp/testing-ca
docker run --rm --net host martin/wait -c localhost:4000 -t 120
features/fixtures/TestApp/console acmephp:server:start
./bin/behat
features/fixtures/TestApp/console acmephp:server:stop
Manual testing
Because Letsencrypt has a rate limiting, We recommends to use
Boulder as Certificate Authority.
Which is include and fully package in the docker image acmephp/testing-ca
You'll find a micro symfony application in the folder
features/fixtures/TestApp
.
It allow you to easily test the application. You just have to edit the config
file in features/fixtures/TestApp/config/config.yml
. Then start Boulder and
the symfony's server (to handle challenge requests).
composer install
# Launch boulder
docker run -d --net host acmephp/testing-ca
docker run --rm --net host martin/wait -c localhost:4000 -t 120
# Launche the application to listen to challenge checks
features/fixtures/TestApp/console acmephp:server:start
# Generate the certificate
features/fixtures/TestApp/console acmephp:generate
ls -al features/fixtures/TestApp/certs/domains/*/