diff --git a/.kitchen.yml b/.kitchen.yml index ef1a37d..ad344a9 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -13,7 +13,7 @@ provisioner: ansible_verbosity: 1 ansible_verbose: true ansible_extra_flags: <%= ENV['ANSIBLE_EXTRA_FLAGS'] %> - # requirements_path: requirements.yml + requirements_path: requirements.yml http_proxy: <%= ENV['ANSIBLE_PROXY'] %> idempotency_test: true additional_copy_path: @@ -54,17 +54,6 @@ platforms: extra_vars: ansible_python_interpreter: '/usr/local/bin/python' - - name: openbsd-snapshot-amd64 - driver: - box: trombik/ansible-openbsd-snapshot-amd64 - box_check_update: false - driver_config: - ssh: - shell: '/bin/sh' - provisioner: - extra_vars: - ansible_python_interpreter: '/usr/local/bin/python' - - name: ubuntu-16.04-amd64 driver: box: trombik/ansible-ubuntu-16.04-amd64 @@ -83,3 +72,13 @@ suites: verifier: name: shell command: rspec -c -f d -I tests/serverspec tests/serverspec/default_spec.rb + - name: x509 + provisioner: + name: ansible_playbook + playbook: tests/serverspec/x509.yml + verifier: + name: shell + command: rspec -c -f d -I tests/serverspec tests/serverspec/x509_spec.rb + includes: + - freebsd-11.1-amd64 + - openbsd-6.2-amd64 diff --git a/.travis.yml b/.travis.yml index 217372b..18a62f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,12 @@ addons: - python-pip - curl +cache: + directories: + - $HOME/.rvm/ + - $HOME/.bundler/ + - $HOME/.cache/pip/ + install: # Install ansible - pip install ansible @@ -40,8 +46,8 @@ script: - "if [ ! -f .private_repo ]; then ansible-playbook tests/travisci/tests.yml -i tests/travisci/inventory --syntax-check; fi" # use recent ruby, instead of default ruby 1.9 - - rvm install ruby-2.1.10 - - rvm use 2.1.10 + - rvm install 2.3 + - rvm use 2.3 # download the QA scripts - git clone https://github.com/trombik/qansible.git @@ -50,7 +56,7 @@ script: - ( cd qansible && git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) ) # install it - - ( cd qansible && bundle install --path vendor/bundle --with "test" && bundle exec rake build && gem install pkg/*.gem ) + - ( cd qansible && bundle install --path ${HOME}/.bundler --with "test" && bundle exec rake build && gem install pkg/*.gem ) - rm -rf qansible # git complains if user and email are not set @@ -64,7 +70,7 @@ script: - export PATH="${PATH}:`rvm gemdir`/bin" # bundle up because rubocop is installed via Gemfile - - bundle install --path vendor/bundle + - bundle install --path ${HOME}/.bundler # run rubocop - bundle exec rubocop diff --git a/Gemfile b/Gemfile index 03dfb5b..3b86cb2 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,6 @@ gem "kitchen-sync", "~> 2.1.1", git: "https://github.com/trombik/kitchen-sync.gi gem "kitchen-vagrant", "~> 0.20.0" gem "kitchen-verifier-serverspec", "~> 0.3.0" gem "kitchen-verifier-shell", "~> 0.2.0" -gem "rack", "~> 1.6.4" # rack 2.x requires ruby >= 2.2.2 gem "rake", "~> 11.1.2" gem "rspec", "~> 3.4.0" gem "rspec-retry", "~> 0.5.6" diff --git a/README.md b/README.md index 2a076e3..248850c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Configure `smtpd(8)`, aka [OpenSMTPD](https://www.opensmtpd.org/). # Requirements -None +When `opensmtpd_include_x509_certificate` is `yes`, `trombik.x509-certificate` +must have been available, usually via `requirements.yml`. # Role Variables @@ -23,6 +24,7 @@ None | `opensmtpd_virtual_user` | Virtual user for delivering mails to virtual users. See below. | `None` | | `opensmtpd_extra_groups` | Additional list of groups to add `smtpd(8)` user to | `[]` | | `opensmtpd_tables` | list of tables. See below. | `[]` | +| `opensmtpd_include_x509_certificate` | Include [`trombik.x509-certificate`](https://github.com/trombik/ansible-role-x509-certificate) role during the play | `no` | ## `opensmtpd_virtual_user` @@ -55,6 +57,13 @@ This list variable defines list of dict of `table(5)`. | `values` | List of content of the file. See `table(5)`. | yes | | `no_log` | When `yes`, enable `no_log` in the template task. Setting this to `no` causes everything in the variable logged, including credentials. The default is `yes` | no | +## `opensmtpd_include_x509_certificate` + +This `include_role` +[`trombik.x509-certificate`](https://github.com/trombik/ansible-role-x509-certificate) +role during the play. See an example in +[`tests/serverspec/x509.yml`](tests/serverspec/x509.yml). + ## FreeBSD | Variable | Default | diff --git a/defaults/main.yml b/defaults/main.yml index c55ec2b..79b7d80 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -10,5 +10,5 @@ opensmtpd_config: "" opensmtpd_makemap_bin: "{{ __opensmtpd_makemap_bin }}" opensmtpd_virtual_user: None opensmtpd_extra_groups: [] - opensmtpd_tables: [] +opensmtpd_include_x509_certificate: no diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..f0ab8a0 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,3 @@ +--- + +- name: trombik.x509-certificate diff --git a/tasks/main.yml b/tasks/main.yml index 89eb821..fdcb7d9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -37,6 +37,11 @@ - include_tasks: "install-{{ ansible_os_family }}.yml" +- include_role: + name: trombik.x509-certificate + when: + - opensmtpd_include_x509_certificate + - name: Create opensmtpd_conf_dir file: path: "{{ opensmtpd_conf_dir }}" diff --git a/tests/serverspec/x509.yml b/tests/serverspec/x509.yml new file mode 100644 index 0000000..c2bf930 --- /dev/null +++ b/tests/serverspec/x509.yml @@ -0,0 +1,208 @@ +- hosts: localhost + roles: + - ansible-role-opensmtpd + vars: + test_user: john@example.org + # smtpctl encrypt PassWord + test_password: "$2b$08$LT/AdE2YSHb19d3hB27.4uXd1/Cj0qQIWc4FdfLlcuqnCUGbRu2Mq" + # XXX table_passwd in Ubuntu package throws error when UID or GID field is + # empty + passwd_postfix: "{% if ansible_os_family == 'Debian' %}:12345:12345:::{% else %}:::::{% endif %}" + opensmtpd_extra_packages: "{% if ansible_os_family == 'FreeBSD' %}[ 'opensmtpd-extras-table-passwd' ]{% else %}[ 'opensmtpd-extras' ]{% endif %}" + opensmtpd_extra_groups: "{% if ansible_os_family == 'FreeBSD' or ansible_os_family == 'OpenBSD' %}[ 'nobody' ]{% else %}[ 'games' ]{% endif %}" + opensmtpd_virtual_user: + name: vmail + group: vmail + home: /var/vmail + comment: Virtual Mail User + opensmtpd_tables: + - name: aliases + path: "{{ opensmtpd_conf_dir }}/aliases" + type: file + format: aliases + mode: "644" + no_log: no + values: + - "MAILER-DAEMON: postmaster" + - "postmaster: root" + - "daemon: root" + - "ftp-bugs: root" + - "operator: root" + - "www: root" + - "foo: error:500 no such user" + - "bar: | cat - >/dev/null" + - name: secrets + path: "{{ opensmtpd_conf_dir }}/secrets" + type: file + owner: root + group: "{{ opensmtpd_group }}" + mode: "0640" + no_log: yes + values: + - "{{ test_user }} {{ test_password }}" + - name: passwd + # XXX Ubuntu package does not allow non-defalt path to smtpd.conf(5) + # as such, all files are under opensmtpd_conf_dir. use smtpd_passwd, + # instead of consistent file name, `passwd`. + path: "{{ opensmtpd_conf_dir }}/smtpd_passwd" + type: passwd + owner: root + group: "{{ opensmtpd_group }}" + mode: "0640" + no_log: no + values: + - "{{ test_user }}:{{ test_password }}{{ passwd_postfix }}" + - name: domains + path: "{{ opensmtpd_conf_dir }}/domains" + type: file + owner: root + group: "{% if ansible_os_family == 'FreeBSD' or ansible_os_family == 'OpenBSD' %}wheel{% else %}root{% endif %}" + mode: "0644" + no_log: no + values: + - example.org + - example.net + - name: virtuals + path: "{{ opensmtpd_conf_dir }}/virtuals" + type: db + dbtype: hash + format: aliases + owner: root + group: vmail + mode: "0444" + values: + - abuse@example.org john@example.org + - postmaster@example.org john@example.org + - john@example.org {{ opensmtpd_virtual_user.name }} + - abuse@example.net john@example.net + - postmaster@example.net john@example.net + - john@example.net {{ opensmtpd_virtual_user.name }} + - name: mynetworks + path: "{{ opensmtpd_conf_dir }}/mynetworks" + type: db + format: set + no_log: no + values: + - 192.168.21.0/24 + + opensmtpd_flags: -v + + # XXX do NOT set this to `yes` in production environment. otherwise, + # secret key is logged. + x509_certificate_debug_log: yes + opensmtpd_include_x509_certificate: yes + + # XXX x509_certificate_dir, which is defined in x509-certificate + # role, cannot be used here due to a bug in ansible. version 2.5 allegedly + # fixes the bug. + # https://github.com/ansible/ansible/issues/21890 + # https://github.com/ansible/ansible/issues/32503#issuecomment-366806353 + ssl_directory: "{{ opensmtpd_conf_dir }}/ssl" + public_key_file: "{{ ssl_directory }}/a.mx.trombik.org.crt" + secret_key_file: "{{ ssl_directory }}/a.mx.trombik.org.key" + opensmtpd_config: | + pki a.mx.trombik.org certificate "{{ public_key_file }}" + pki a.mx.trombik.org key "{{ secret_key_file }}" + {% for list in opensmtpd_tables %} + table {{ list.name }} {{ list.type }}:{{ list.path }}{% if list['type'] == 'db' %}.db{% endif %} + + {% endfor %} + listen on {% if ansible_os_family == 'FreeBSD' or ansible_os_family == 'OpenBSD' %}lo0{% else %}lo{% endif %} port 25 + listen on {% if ansible_os_family == 'FreeBSD' or ansible_os_family == 'OpenBSD' %}lo0{% else %}lo{% endif %} port 587 smtps pki a.mx.trombik.org auth hostname \ + a.mx.trombik.org received-auth + accept from any for domain virtual \ + deliver to maildir "{{ opensmtpd_virtual_user.home }}/%{dest.domain}/%{dest.user}/Maildir" + accept from source for any relay + + x509_certificate: + - name: a.mx.trombik.org + state: present + secret: + path: "{{ secret_key_file }}" + key: | + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + public: + path: "{{ public_key_file }}" + key: | + -----BEGIN CERTIFICATE----- + MIIFjTCCA3WgAwIBAgIJAOWEMp5KrvqIMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV + BAYTAlRIMRAwDgYDVQQIDAdCYW5na29rMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRn + aXRzIFB0eSBMdGQxGTAXBgNVBAMMEGEubXgudHJvbWJpay5vcmcwHhcNMTcxMjAx + MDM1NzA5WhcNMTgxMjAxMDM1NzA5WjBdMQswCQYDVQQGEwJUSDEQMA4GA1UECAwH + QmFuZ2tvazEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRkwFwYD + VQQDDBBhLm14LnRyb21iaWsub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC + CgKCAgEAsqn3qdkjpvXn3vWIdPCCwGCQdaPhOxiyR0lVd3HfZOZpUM9u2Y+alXZx + exaOeMVIc8Ucazz49e1cgAYW+j4Y7roortcWpGJxUqY0LL3i4rBXJMsIjMDQ8gC9 + ymC4ktPelzxWX1evs1j35ZJXynYTVYztLkSLnJuVjqUqjEj/EhjqQFqgRKlhKtED + 9JeNM2NGTihYe4o2pTNoosNSsDsvy6liBi2Soko3D0XFkqD7hgCBn6NwJuzCvLQr + ypftGS7Bb1vsq9mEXSDeGZE1zZ170CtJ5/bKfsj66ecUBkt7X4qiFxos8vwb+k9M + WHgTtwipYrXr214aymdEP5t8ze0z6MhFJQU9FYfJ/VqaxV6ug+NH995uKaalY3np + X1vPANODi33wVIze0AiAYCu/bimfw4imG5AiTe7yJZLTa7tUcJ+HgHyZ6kV1Z4MM + CvqqMRKR//yELwFunJ8smDneKV5KnEB1aiH2RcmhplXIcrBtNIGnoOPH84SNJ/Hx + ji3H5meQLYQ0hv5NUXSHDr0bykrHvk5s1fBxG2/CORrMcwTY3dN+1oGDn7o56Wzl + nQ5VGYHWhVyBbfx2utSFeyMgDFrO/NKQBo8jS1UrUW5smYWcCV3LPo+dw3SShIEc + Y/kmbiljQ9+989oLQMvWbYunPzOUrMJUL6XB6We+nuEWnWRDCncCAwEAAaNQME4w + HQYDVR0OBBYEFBdZboL9VDGD0csUCItPSFX+DSQzMB8GA1UdIwQYMBaAFBdZboL9 + VDGD0csUCItPSFX+DSQzMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIB + AGiG1DxpTue9Ji9VnkDmrf1uMWi+Xv+adGBjETR3ojR6H8VaMhPZUrGAcTeB3C8h + C6mS0uTm6gAthPP0b25GgYRuZn2TSsWaURH6fXJJISrvZVNfY7CrN7eyK19BYSf+ + 31KREZOftm/0q8E+dpmUxKXSgvn4NmXG5TD7gOU9d2v9n9RtyNVrXyOWVWMcP73u + StVHHONGwQcm5E7oyQsVcmqKziy7KZxMQwCQ13qzfwlALKDJN19uA/cBBySov05y + NxNHKnhXnzxI57kmm+J7qy7D+XFzEsZZmZVS5J9mOOos19EHXx/qepRtPdxtJgFO + HJV+4tR7gtMCTC84ZkvRC8mUKCcbkzVniutKzdKSZ0uUeDHpAHp/Sv3+5qaevZ9D + 2CcbxfqtSfEXr6VHBnBacLBF25P/d3gPN0TqXpR9fhhL1Q8LPW2VrEH9zfLDUDRA + argrFehyERw1WvSIhnz33kicpGC11viN6ITdAChNALttnBYOOPWaBSJL0HCw4HfN + DuUckexsq3zK5oNCCc3bua7tJCB/Jv0lgMZt7sRR7t2UY7Dy/EZH5Zfk9FxUKY/C + 98XdGs+SkGrSHyXaHBmcUfFtUW7JfYEcAFrIWLV6E7e1BBjExn53Z97xZgwOvq1x + m0f30I/wnrEVBZwTF/pl5dDLq3a3Pm0kEIcZkzwXve4+ + -----END CERTIFICATE----- diff --git a/tests/serverspec/x509_spec.rb b/tests/serverspec/x509_spec.rb new file mode 100644 index 0000000..68fc03e --- /dev/null +++ b/tests/serverspec/x509_spec.rb @@ -0,0 +1,41 @@ +require "default_spec" + +ssl_dir = case os[:family] + when "freebsd" + "/usr/local/etc/mail/ssl" + when "openbsd" + "/etc/mail/ssl" + else + raise format("unsupported platform %s", family: os[:family]) + end + +describe file "#{ssl_dir}/a.mx.trombik.org.key" do + it { should exist } + it { should be_file } +end + +describe file "#{ssl_dir}/a.mx.trombik.org.crt" do + it { should exist } + it { should be_file } +end + +describe port(587) do + it { should be_listening } +end + +stderr_msg = "depth=0 C = TH, ST = Bangkok, O = Internet Widgits Pty Ltd, CN = a.mx.trombik.org +verify error:num=18:self signed certificate +verify return:1 +depth=0 C = TH, ST = Bangkok, O = Internet Widgits Pty Ltd, CN = a.mx.trombik.org +verify return:1 +DONE +" + +# XXX sleep before disconnecting. otherwise, the SMTP banner cannot be +# captured in the stdout +describe command "(sleep 3; echo helo localhost)| openssl s_client -connect 127.0.0.1:587" do + its(:stderr) { should eq stderr_msg } + its(:stdout) { should match(/^#{Regexp.escape("subject=/C=TH/ST=Bangkok/O=Internet Widgits Pty Ltd/CN=a.mx.trombik.org")}$/) } + its(:stdout) { should match(/^#{Regexp.escape("220 a.mx.trombik.org ESMTP OpenSMTPD")}$/) } + its(:exit_status) { should eq 0 } +end