# Protonmail - System Test Report
This report was made to explain all the steps I did during the System Test provided by Protonmail during the recruitment process.
# Create Mail Account
- Go to https://protonmail.com
- Create a free account
- share with the examinator
# Create a VPN Account
- Go to https://account.protonvpn.com/signup
- create a new free account
- if you already have a protonmail account, it will work.
NOTE: a IKEv2 configuration (username/password) is available on the account page. You can use it to create your own openvpn configuration.
# Using ProtonVPN cli on OpenBSD
ProtonVPN is based on OpenVPN. To use it you must install these packages:
doas pkg_add python3 openvpn
ProtonVPN is an open-source project. The code source is available on Github.
mkdir src
git clone https://github.com/protonvpn/protonvpn-cli.git
git clone https://github.com/protonvpn/protonvpn-cli-ng.git
# ProtonVPN-cli
This version is not supported anymore. A ticket was open in 2019 to port the shell script on OpenBSD.
# ProtonVPN-cli-ng
After created the clone of the repository.
cd protonvpn-cli-ng
python setup.py build
doas python setup.py intsall
protonvpn-cli-ng require some Linux commands to work, like ip (used to manage route table).
Here a quick and dirty patch to solve this problem.
diff --git a/protonvpn_cli/utils.py b/protonvpn_cli/utils.py
index 076f64d..f05b380 100644
--- a/protonvpn_cli/utils.py
+++ b/protonvpn_cli/utils.py
@@ -333,7 +333,7 @@ def check_root():
sys.exit(1)
else:
# Check for dependencies
- dependencies = ["openvpn", "ip", "sysctl", "pgrep", "pkill"]
+ dependencies = ["openvpn"] # , "ip", "sysctl", "pgrep", "pkill"]
for program in dependencies:
check = subprocess.run(["which", program],
stdout=subprocess.PIPE,
This patch is useful only to initialize a configuration
with protonvpn init. Note: the file /usr/local/bin/protonvpn was not
working due to the shebang (set to #!.). A simple modification
of the file make it usable by adding a correct shebang
(#!/usr/local/bin/python3).
To understand how this tool is working, I will with python debugger
module (pdb) and try to see if something goes wrong.
doas python3 -m pdb $(which protonvpn)
This tool create all the configuration in /root/.pvpn-cli. OpenVPN
configuration file is stored in /root/.pvpn-cli/pvpn-cli.cfg.
[USER]
username = ${USERNAME}
tier = 0
default_protocol = udp
initialized = 1
dns_leak_protection = 1
custom_dns = None
check_update_interval = 3
killswitch = 0
[metadata]
last_api_pull = 1587137886
last_update_check = 1587137881
The user's credentials are stored in raw (!) in the
/root/.pvpn-cli/pvpnpass file!
${USER_PASSWORD}
Another file is created, probably from a remote API call,
called /root/.vpn-cli/serverinfo.json and contain all
available server. That pretty interesting. Here some
command to make some queries with jq.
# list available servers in a country (e.g. CA)
cat serverinfo.json \
| jq '.LogicalServers[] | select(.EntryCountry == "CA")'
# select all IP in a country
cat serverinfo.json \
| jq '.LogicalServers[] | select(.EntryCountry == "CA") | .Servers[].EntryIP'
# list all available IP
cat serverinfo.json \
| jq ".LogicalServers[].Servers[].EntryIP"
A template is also created, called template.ovpn:
# ==============================================================================
# Copyright (c) 2016-2017 ProtonVPN A.G. (Switzerland)
# Email: contact@protonvpn.com
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# ==============================================================================
client
dev tun
remote-random
resolv-retry infinite
nobind
cipher AES-256-CBC
auth SHA512
comp-lzo no
verb 3
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450
persist-key
persist-tun
reneg-sec 0
remote-cert-tls server
auth-user-pass
pull
fast-io
<ca>
-----BEGIN CERTIFICATE-----
MIIFozCCA4ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBAMQswCQYDVQQGEwJDSDEV
MBMGA1UEChMMUHJvdG9uVlBOIEFHMRowGAYDVQQDExFQcm90b25WUE4gUm9vdCBD
QTAeFw0xNzAyMTUxNDM4MDBaFw0yNzAyMTUxNDM4MDBaMEAxCzAJBgNVBAYTAkNI
MRUwEwYDVQQKEwxQcm90b25WUE4gQUcxGjAYBgNVBAMTEVByb3RvblZQTiBSb290
IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt+BsSsZg7+AuqTq7
vDbPzfygtl9f8fLJqO4amsyOXlI7pquL5IsEZhpWyJIIvYybqS4s1/T7BbvHPLVE
wlrq8A5DBIXcfuXrBbKoYkmpICGc2u1KYVGOZ9A+PH9z4Tr6OXFfXRnsbZToie8t
2Xjv/dZDdUDAqeW89I/mXg3k5x08m2nfGCQDm4gCanN1r5MT7ge56z0MkY3FFGCO
qRwspIEUzu1ZqGSTkG1eQiOYIrdOF5cc7n2APyvBIcfvp/W3cpTOEmEBJ7/14RnX
nHo0fcx61Inx/6ZxzKkW8BMdGGQF3tF6u2M0FjVN0lLH9S0ul1TgoOS56yEJ34hr
JSRTqHuar3t/xdCbKFZjyXFZFNsXVvgJu34CNLrHHTGJj9jiUfFnxWQYMo9UNUd4
a3PPG1HnbG7LAjlvj5JlJ5aqO5gshdnqb9uIQeR2CdzcCJgklwRGCyDT1pm7eoiv
WV19YBd81vKulLzgPavu3kRRe83yl29It2hwQ9FMs5w6ZV/X6ciTKo3etkX9nBD9
ZzJPsGQsBUy7CzO1jK4W01+u3ItmQS+1s4xtcFxdFY8o/q1zoqBlxpe5MQIWN6Qa
lryiET74gMHE/S5WrPlsq/gehxsdgc6GDUXG4dk8vn6OUMa6wb5wRO3VXGEc67IY
m4mDFTYiPvLaFOxtndlUWuCruKcCAwEAAaOBpzCBpDAMBgNVHRMEBTADAQH/MB0G
A1UdDgQWBBSDkIaYhLVZTwyLNTetNB2qV0gkVDBoBgNVHSMEYTBfgBSDkIaYhLVZ
TwyLNTetNB2qV0gkVKFEpEIwQDELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFByb3Rv
blZQTiBBRzEaMBgGA1UEAxMRUHJvdG9uVlBOIFJvb3QgQ0GCAQEwCwYDVR0PBAQD
AgEGMA0GCSqGSIb3DQEBDQUAA4ICAQCYr7LpvnfZXBCxVIVc2ea1fjxQ6vkTj0zM
htFs3qfeXpMRf+g1NAh4vv1UIwLsczilMt87SjpJ25pZPyS3O+/VlI9ceZMvtGXd
MGfXhTDp//zRoL1cbzSHee9tQlmEm1tKFxB0wfWd/inGRjZxpJCTQh8oc7CTziHZ
ufS+Jkfpc4Rasr31fl7mHhJahF1j/ka/OOWmFbiHBNjzmNWPQInJm+0ygFqij5qs
51OEvubR8yh5Mdq4TNuWhFuTxpqoJ87VKaSOx/Aefca44Etwcj4gHb7LThidw/ky
zysZiWjyrbfX/31RX7QanKiMk2RDtgZaWi/lMfsl5O+6E2lJ1vo4xv9pW8225B5X
eAeXHCfjV/vrrCFqeCprNF6a3Tn/LX6VNy3jbeC+167QagBOaoDA01XPOx7Odhsb
Gd7cJ5VkgyycZgLnT9zrChgwjx59JQosFEG1DsaAgHfpEl/N3YPJh68N7fwN41Cj
zsk39v6iZdfuet/sP7oiP5/gLmA/CIPNhdIYxaojbLjFPkftVjVPn49RqwqzJJPR
N8BOyb94yhQ7KO4F3IcLT/y/dsWitY0ZH4lCnAVV/v2YjWAWS3OWyC8BFx/Jmc3W
DK/yPwECUcPgHIeXiRjHnJt0Zcm23O2Q3RphpU+1SO3XixsXpOVOYP6rJIXW9bMZ
A1gTTlpi7A==
-----END CERTIFICATE-----
</ca>
key-direction 1
<tls-auth>
# 2048 bit OpenVPN static key
-----BEGIN OpenVPN Static key V1-----
6acef03f62675b4b1bbd03e53b187727
423cea742242106cb2916a8a4c829756
3d22c7e5cef430b1103c6f66eb1fc5b3
75a672f158e2e2e936c3faa48b035a6d
e17beaac23b5f03b10b868d53d03521d
8ba115059da777a60cbfd7b2c9c57472
78a15b8f6e68a3ef7fd583ec9f398c8b
d4735dab40cbd1e3c62a822e97489186
c30a0b48c7c38ea32ceb056d3fa5a710
e10ccc7a0ddb363b08c3d2777a3395e1
0c0b6080f56309192ab5aacd4b45f55d
a61fc77af39bd81a19218a79762c3386
2df55785075f37d8c71dc8a42097ee43
344739a0dd48d03025b0450cf1fb5e8c
aeb893d9a96d1f15519bb3c4dcb40ee3
16672ea16c012664f8a9f11255518deb
-----END OpenVPN Static key V1-----
</tls-auth>
A log file is also present, called pvpn-cli.log.
# Manual connection with OpenVPN
openvpn --user _openvpn --group _openvpn \
--config ${protonmail_template} \
--remote {remote_ip}
Without certificate, the credentials used will be available
from protonvpn account (OpenVPN / IKEv2 username section).
If authentication is a success, you should receive routes:
Wed Apr 22 16:06:44 2020 TUN/TAP device /dev/tun0 opened
Wed Apr 22 16:06:44 2020 /sbin/ifconfig tun0 10.8.8.5 10.8.8.1 mtu 1500 netmask 255.255.255.0 up
Wed Apr 22 16:06:44 2020 /sbin/route add -net 10.8.8.0 -netmask 255.255.255.0 10.8.8.1
add net 10.8.8.0: gateway 10.8.8.1
Wed Apr 22 16:06:44 2020 /sbin/route add -net 194.59.249.20 -netmask 255.255.255.255 192.168.0.254
add net 194.59.249.20: gateway 192.168.0.254
Wed Apr 22 16:06:44 2020 /sbin/route add -net 0.0.0.0 -netmask 128.0.0.0 10.8.8.1
add net 0.0.0.0: gateway 10.8.8.1
Wed Apr 22 16:06:44 2020 /sbin/route add -net 128.0.0.0 -netmask 128.0.0.0 10.8.8.1
add net 128.0.0.0: gateway 10.8.8.1
# ProtonVPN API
The main URL is https://api.protonvpn.ch. Here the different end-point from the source code of protonvpn-api.
# /vpn/logicals
Used to retrieve the list of all servers.
curl https://api.protonvpn.ch/vpn/logicals
# /vpn/location
Used to retrieve the public IP address.
curl https://api.protonvpn.ch/vpn/location
# /test/ping
Used to check the connection. Return a 404 not found when tested.
curl https://api.protonvpn.ch/test/ping
# /vpn/config
Return an OpenVPN configuration template. The id param
come from the /vpn/logical end-point and refer the
a server id. This will generate a configuration based
on the IP address(es) of this server.
platform="Linux"
protocol="tcp"
id="BzHqSTaqcpjIY9SncE5s7FpjBrPjiGOucCyJmwA6x4nTNqlElfKvCQFr9xUa2KgQxAiHv4oQQmAkcA56s3ZiGQ=="
curl "https://api.protonvpn.ch/vpn/config?Platform=${platform}&LogicalID=${id}&Protocol=${protocol}"
# ProtonMail System Test - Report
The goal of the test is to setup an automatic failover for HAProxy load balancer using keepalived. It will involve the installation and configuration of a dummy “hello, world!†HTTP server back end, the HAProxy in a VM, replication of the HAProxy to a second VM instance and a setup of the keepalived service.
The VM setup and replication themselves are not directly connected to the failover functionality but are just as important components of the test.
The whole test consists of 3 parts:
Setup a guest virtual machine V1 on T3 using KVM. V1 needs to have the public IP address 46.20.249.165 using bridged networking, see references for some details and guides. Install HAProxy on V1 with the 46.20.249.165 front end. Setup a simple “hello, world!†HTTP back end on T3. Configure HAProxy to use the T3 HTTP back end. Now, you should be able to request http://46.20.249.165 and get the “hello, world!†response served by the T3 back end. If you pause V1, the request should time out.
Replicate V1 to a 2nd virtual machine V2 with the public IP 46.20.249.166. You should now be able to request http://46.20.249.166 and get the same response served by the same back end as if you requested http://46.20.249.165.
Install keepalived on V1 and V2 and configure it for an automated failover from V1 to V2 through the transfer of the virtual IP 46.20.249.167 from V1 to V2. Now, you should be able to request http://46.20.249.167 and get the “hello, world!†response served by the T3 back end through V1. If you pause V1 now, the http://46.20.249.167 request should not time out anymore as in step 1. Instead, it should be served through V2, which automatically takes over the VIP 46.20.249.167. In addition to the failure of the whole V1 machine, the automatic failover should happen also in the cases when either the V1 HAProxy goes down (e.g. we intentionally kill the process) or the V1 network interface goes down.
For the static IPv4 networking of the VM public interfaces (gateway, subnet mask, DNS hosts), use the same settings as are used for T3.
Optionally, as time allows, pick one or more of the following tasks :
Use Vagrant for instantiating the VMs with a base Linux OS
Use scripts, Ansible or Puppet to automate the configuration of the VMs
Put the HTTP back-end in a third VM V3, in a private network
Improve on the security of the systems, according to your preferences
# Tasks
install toolbox on T3 server: done
install qemu on T3 server: done
install nginx on T3 server: done
install curl on T3 server: done
install vagrant on T3 server: done
install ansible on T3 server: done
configure nginx on T3 server: done
disable NetworkManager: done
create a bridge on T3 server: done
create a kvm guest V1 on T3 server: done
configure network on V1: done
install haproxy on V1: done
configure haproxy on V1: done
replicate V1 guest to V2: done
configure network on V2: done
install keepalived on V1: done
install keepalived on V2: done
configure NAT on v3 with iptables: doesn't work
- probably due to firewalld. did not check
create ansible configuration for host: done
create a nginx role: done
create an haproxy role: done
create a keepalived role: done
create other ansible tasks: done
test network: done
test failover: done
# Final Configuration Summary
- 46.20.249.165 (V1): kvm guest
- haproxy
- keepalived
- 46.20.249.166 (V2): kvm guest (V1 replication)
- haproxy
- keepalived
- 46.20.249.168 (T3): host server
- user: icandidate2
- centos
- http backend
- 46.20.249.171 (S1): Dell iDrac access
- 10.0.0.2/24 (V3): backend on private network
- nginx
# Network Map
[46.20.249.161] [46.20.249.168] [10.0.0.2/24]
________ / ____/ _______ ____/
( )/ | | ( ) | |
( internet )---| T3 |---( private )---| V3 |
(________) |____| (_______) |____|
| \
| [10.0.0.1/24]
|
| [46.20.249.165]
__|___ ____/
( ) | |
( bridge )----| V1 |
(______) |____|
| /
| ( [46.20.249.167]
| \ ____
| | |
+---------| V2 |
|____|
\
[46.20.249.166]
# Connexion T3 server
A VPN connexion is required to have the right to connect to this server. OpenVPN is used, here an example on openbsd:
doas openvpn --user _openvpn --group _openvpn --config ${config}
Ensure your SSH public key is well configured too. You should have access to t3 server with icandidate2 user.
ssh 46.20.249.168 -l icandidate2
# T3 Server configuration
uname -a
# output:
# Linux t3 3.10.0-1062.18.1.el7.x86_64 #1 SMP Tue Mar 17 23:49:17
# UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
A quick review of the server configuration before doing important modification on it. The server uses LVM and the toolbox. It could be possible to create LVM snapshot for fast recovery.
pvdisplay
vgdisplay
lvdisplay
It has only one active network interface (em1):
ip a
The raw output of the command:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether d4:ae:52:8e:bb:50 brd ff:ff:ff:ff:ff:ff
inet 46.20.249.168/28 brd 46.20.249.175 scope global noprefixroute em1
valid_lft forever preferred_lft forever
inet6 fe80::42d7:2ac0:328:cf56/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: em2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether d4:ae:52:8e:bb:51 brd ff:ff:ff:ff:ff:ff
4: p1p1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 90:e2:ba:82:4d:70 brd ff:ff:ff:ff:ff:ff
5: p1p2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 90:e2:ba:82:4d:71 brd ff:ff:ff:ff:ff:ff
# Pre-Configuration and Toolbox installation
First thing first, ensuring the server is up to date.
sudo yum update
During the configuration, and because I don't have any other way to test or ensure the connection will be up during all the exercice, I will install tmux.
sudo yum install tmux
T3 Server will need nginx (http backend) and qemu/kvl (for virtualization part). Curl will be used to check http services.
sudo yum install nginx qemu qemu-kvm curl
Ansible was selected to install and configure the different virtual machines installed on T3 server.
sudo yum install ansible
Vagrant will also be used. Hashicorp GPG key can be retrieved on hashicorp security page (opens new window)
curl -Os https://releases.hashicorp.com/vagrant/2.2.7/vagrant_2.2.7_x86_64.rpm
curl -Os https://releases.hashicorp.com/vagrant/2.2.7/vagrant_2.2.7_SHA256SUMS
curl -Os https://releases.hashicorp.com/vagrant/2.2.7/vagrant_2.2.7_SHA256SUMS.sig
Before installing vagrant, a verifification is needed, ensuring the package was correctly downloaded from the website without alteration.
gpg --import hashicorp.asc
gpg --verify vagrant_2.2.7_SHA256SUMS.sig vagrant_2.2.7_SHA256SUMS
sha256sum -c vagrant_2.2.7_SHA256SUMS
If everything fine, vagrant RPM file can be installed
yum localinstall vagrant_2.2.7_x86_64.rpm
vagrant command is now available from the command line
vagrant
# HTTP back-end configuration on T3
The backend on T3 will have nginx configured to listen on all interfaces. The "hello, world!" application will be only a simple text file containing this string.
nginx is configuration is located in /etc/nginx/nginx.conf and files
are stored by default in /usr/share/nginx/html. We can modify the
configuration in many different way, but, because its a default
backend application used only for test, the fastest way is to modify
files in /usr/share/nginx/html:
cd /usr/share/nginx/html
sudo mv index.html index.html.bck
sudo sh -c 'echo "Hello, world!" > index.html'
nginx service can now be enabled and started.
sudo systemctl enable nginx
sudo systemctl start nginx
To check if the service is running, systemctl status command be used
and to test it, curl can be used.
curl localhost
# output:
# Hello, world!
curl 46.20.249.168
# output:
# Hello, world!
nginx service is up and running, it returns the desired value.
# Static Network Configuration and NetworkManager
During the test phase, NetworkManager is only stopped.
sudo systemctl stop NetworkManager
To disable this service completely, it is also possible to reuse
systemctl. It will not be used in this
sudo systemctl disable NetworkManager
# Bridge Configuration
NOTE: this part was tested before on personal virtual machine. Creating bridge and alter network configuration can result in a network loss.
#!/bin/sh
######################################################################
# bridge fallback script for testing
# will migrate the configuration for 15 seconds to ensure
# everything is fine. After that, it will fallback to the previous
# configuration.
######################################################################
WAN=em1
WAN_IP=46.20.249.168/28
BRIDGE=br0
GATEWAY=46.20.249.161
_eval() {
printf "execute: %s\n" "${*}"
eval ${*}
}
_init() {
_eval ip link add ${BRIDGE} type bridge
}
_clean() {
_eval ip link del ${BRIDGE}
}
_do() {
_eval ip link set ${BRIDGE} up
_eval ip link set ${WAN} master ${BRIDGE}
_eval ip addr del dev ${WAN} ${WAN_IP}
_eval ip addr add dev ${BRIDGE} ${WAN_IP}
_eval ip route add default ${GATEWAY}
_eval ping -c1 ${GATEWAY}
_eval ping -c1 185.70.41.32
}
_undo() {
_eval ip addr del dev ${BRIDGE} ${WAN_IP}
_eval ip addr add dev ${WAN} ${WAN_IP}
_eval ip link set ${WAN} nomaster
_eval ip link set ${BRIDGE} down
_eval ip route add default ${GATEWAY}
_eval ping -c1 ${GATEWAY}
_eval ping -c1 185.70.41.32
}
case "${1}" in
init) _init;;
clean) _clean;;
do) do;;
undo) _undo;;
restart) systemctl network restart;;
*) echo "usage ${0}: [init|clean|do|undo|restart]";;
esac
This script is only used to check if bridge configuration works as expected. It will offer different facilities to create and migrate the network configuration to a bridge.
sudo sh script.sh do
# to test:
# sudo sh script.sh do; sleep 15; sudo sh script.sh undo
The main network files configuration are stored in
/etc/sysconfig/network-scripts. To configure a bridge, the file
ifcfg-br0 should be created
DEVICE=br0
TYPE=Bridge
IPADDr="46.20.249.168"
NETMASK="255.255.255.240"
GATEWAY="46.20.249.161"
ONBOOT=yes
BOOTPROTO=none
NM_CONTROLLED=no
DELAY=0
To connect ethernet interface to the bridge,
/etc/sysconfig/network-scripts/ifcg-em1 should be modified too.
DEVICE=em1
TYPE=Ethernet
USERCTL=no
SLAVE=yes
MASTER=br0
BOOTPROTO=none
HWADDR=d4:ae:52:8e:bb:50
NM_CONTROLLED=no
Network configuration can be enabled by using systemctl or by
rebooting the server.
sudo systemctl restart network
# Virtual Machine Configuration
All virtual machines will use vagrant and libvirt.
# Libvirt Configuration
Libvirt is an abstraction layer to help creating and managing virtual machines. All packages are directly available from centos or rhel repositories.
# install requirement for vagrant libvirt plugin
sudo yum install libvirt libvirt-devel
To use libvirt as a manager, libvirt must be enabled and
started. centos use systemd as main init and the command systemctl
must be used.
# ensure libvirt is enabled and started
sudo systemctl enable libvirtd
sudo systemctl start libvirtd
Vagrant runs as default user, to give it access to libvirt, this user
must be in the libvirt group.
# ensure user is in libvirt group
sudo usermod ${USER} -aG libvirt
libvirt can also be manually managed with virsh.
# print the virtual machine
sudo virsh list
# print the stats about VMs
sudo virsh domstats
Eventually, all these steps can be added to an ansible playbook:
- name: install libvirt
become: yes
package:
name: libvirt
state: latest
- name: enable libvirt
become: yes
service:
name: libvirtd
state: started
- name: add current user in libvirt group
become: yes
command: "usermod {{ libvirtd['user'] }} -aG libvirt"
when: libvirtd['user'] is defined
# Vagrant Configuration
Configure virtual machine with vagrant. In this part, libvirt is used because no generic boxes are available for qemu/kvm. The configuration steps can be found on the official vagrant-libvirt (opens new window) plugins on github.
# install git to manage the ansible repository
sudo yum install git ruby-devel gcc
# create the vagrant repository
mkdir vms
cd vms
# init vagrant by creating a Vagrantfile
vagrant init .
# install vagrant-libvirt plugin
vagrant plugin install vagrant-libvirt
# start the VMs
vagrant up
At least the first step (installing package) can be automated easily with an ansible playbook.
- name: install git, ruby and gcc
become: yes
package:
name: [git, ruby-devel, gcc]
state: latest
After the creation of the Vagrantfile present in ${HOME}/vms, we
can configure our three virtual machines, v1, v2 and v3. Both VMs are
connected to one management network, connected on eth0 interface by
default on the guest.
The bridge (br0) previously configured will be connected to each VMs
on eth1 interface on the guest, and vnetX on the host. It is
possible to use the bridge command to control this part.
bridge link
Here the Vagrantfile used. Please read the comment on each part. The
provisioning is made with ansible and this part will be presented
later.
Vagrant.configure("2") do |config|
# v1 virtual machine with 46.20.249.165 ip address
# the provisioning is made with ansible and it uses
# a generic centos7 image
config.vm.define "v1" do |v|
v.vm.box = "generic/centos7"
v.vm.provider :libvirt do |lib|
lib.cpus = 2
end
v.vm.network :public_network,
:dev => "br0",
:mode => "bridge",
:type => "bridge",
:ip => "46.20.249.165",
:netmask => "255.255.255.240"
v.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/site.yml"
end
end
# v2 virtual machine with 46.20.249.166 ip address
# the provisioning is made with ansible and it uses
# a generic centos7 image
config.vm.define "v2" do |v|
v.vm.box = "generic/centos7"
v.vm.provider :libvirt do |lib|
lib.cpus = 2
end
v.vm.network :public_network,
:dev => "br0",
:mode => "bridge",
:type => "bridge",
:ip => "46.20.249.166",
:netmask => "255.255.255.240"
v.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/site.yml"
end
end
# v3 virtual machine is in a private network (10.0.0.0/24)
# but share the same information than the other VMs
config.vm.define "v3" do |v|
v.vm.box = "generic/centos7"
v.vm.provider :libvirt do |lib|
lib.cpus = 2
end
v.vm.network :private_network,
:dev => "br1",
:mode => "bridge",
:type => "bridge",
:ip => "10.0.0.2",
:netmask => "255.255.255.0"
v.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/site.yml"
end
end
end
Vagrant is ready to fetch images and provision the virtual machine. If
ansible playbooks and roles are not defined (yet), vagrant
provisioning can be executed manually with vagrant provision.
vagrant up
# Ansible Repository
In the vagrant root where Vagrantfile is present, an ansible
repository will be made. This tree is based on best practices from
official ansible documentation.
# create the tree
mkdir -p provisioning/host_vars
mkdir -p provisioning/roles
mkdir -p provisioning/inventory
# configure local machine (host)
echo "localhost ansible_connection=local" \
>> provisioning/inventory/hosts
# main playbook used to provisioning virtual machines
touch provisioning/site.yml
# used to initialize the host
touch provisioning/localhost.yml
# dedicated host variables
touch provisioning/host_vars/v1.yml
touch provisioning/host_vars/v2.yml
touch provisioning/host_vars/v3.yml
# this host variable file is dedicated to the host
# can be executed with:
touch provisioning/host_vars/localhost.yaml
Provisionning with ansible on vagrant is done with the provision
subcommand.
vagrant provision ${vm}
# or with a defined provisioner
vagrant provision ${vm} --provision-with ansible
It is also possible to provision manually with ansible, directly from the provisioning directory:
cd provisioning
ansible-playbook -i inventory site.yml
# or with vagrant host file
ansible-playbook -i ../.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory \
site.yml
Each roles will be made manually. Anyway, some of these roles can be found from galaxy like:
- https://galaxy.ansible.com/entercloudsuite/haproxy
- https://galaxy.ansible.com/entercloudsuite/keepalived
- https://galaxy.ansible.com/geerlingguy/nginx
provisionning/site.yml is the main playbook used to provision each
servers.
- hosts:
- v1
- v2
tasks:
- name: update packages
become: yes
yum:
name: '*'
state: latest
roles:
- haproxy
- keepalived
- hosts: v3
roles:
- nginx
- iptables
# nginx configuration
cd roles
# initialize tree
mkdir nginx
mkdir -p nginx/templates
mkdir -p nginx/tasks
mkdir -p nginx/handlers
# create files
touch nginx/README.md
touch nginx/tasks/main.yml
touch nginx/tasks/nginx.yml
touch nginx/templates/nginx.conf.j2
touch nginx/handlers/main.yml
tasks/main.yml will include nginx.yml only if nginx variable is
set.
- name: include nginx
include: nginx.yml
when: nginx is defined
tasks/nginx.yml will install and check nginx, ensure each directory
are present (from the configuration file), generate a new
configuration and check it.
- name: install nginx
become: yes
package:
name: nginx
state: latest
- name: enable nginx
become: yes
service:
name: nginx
enabled: yes
state: started
- name: ensure modules directory is present
become: yes
file:
path: /usr/share/nginx/modules
state: directory
- name: ensure conf directory is present
become: yes
file:
path: /etc/nginx/conf.d
state: directory
- name: configure nginx
become: yes
template:
src: nginx.conf.j2
dest: /tmp/nginx.conf.tmp
owner: root
group: root
mode: '0644'
- name: check nginx configuration
become: yes
command: nginx -t -c /tmp/nginx.conf.tmp
- name: nginx configuration is valid
become: yes
copy:
src: /tmp/nginx.conf.tmp
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
remote_src: yes
notify: reload nginx
- name: clean nginx temporary configuration
become: yes
file:
path: /tmp/nginx/conf/tmp
state: absent
templates/nginx.conf.j2 generate a configuration file based on the
default one offered by RHEL package.
user {{ nginx['user'] | default("nginx") }};
worker_processes {{ nginx['worker_processes'] | default("auto") }};
error_log {{ nginx['error_log'] | default("/var/log/nginx/error.log") }};
pid {{ nginx['pid'] | default("/run/nginx.pid") }};
include /usr/share/nginx/modules/*.conf;
events {
worker_connections {{ nginx['events']['worker_connections'] | default("1024") }};
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile {{ nginx['http']['sendfile'] | default("on") }};
tcp_nopush {{ nginx['http']['tcp_nopush'] | default("on") }};
tcp_nodelay {{ nginx['http']['tcp_nodelay'] | default("on") }};
keepalive_timeout {{ nginx['http']['keepalive_timeout'] | default("65") }};
types_hash_max_size {{ nginx['http']['types_hash_max_size'] | default("2048") }};
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
handlers/main.yml add reload support.
- name: reload nginx
become: yes
service:
name: nginx
state: reloaded
NOTE: the website/application will be deployed manually.
# haproxy configuration
haproxy configuration will be used by a flexible pattern. This role
(like the other I will create) will be enable only if the variable
haproxy is set. This pattern is really interesting, because you can
easily export the configuration of a server or group of server in yaml
format to another tool (like salt or chef). The idea is to create a
high level abstraction with defined data structure, in this case an
object or dictionary in Python. Here a configuration example for the
v1 server haproxy. This file is generaly stored in host_vars/v1.yml.
haproxy:
frontends:
- name: test
listen: "*:80"
use_backend: t3
default_backend: t3
backends:
- name: t3
balance: roundrobin
servers:
- t3 46.20.249.168:80 check
Before creating the role from scratch, the directory and file tree need to be initialized. Again, this is from ansible best practices pages.
cd roles
# initialize tree
mkdir haproxy
mkdir -p haproxy/templates
mkdir -p haproxy/tasks
mkdir -p haproxy/handlers
# create files
touch haproxy/README.md
touch haproxy/tasks/main.yml
touch haproxy/tasks/haproxy.yml
touch haproxy/templates/haproxy.cfg.j2
touch haproxy/handlers/main.yml
README.md will contain a small documentation about the role.
tasks/main.yml will contain the "bootstrap", a small piece of code
used to include the true main file based on haproxy variable.
- name: haproxy enabled
include: haproxy.yml
when: haproxy is defined
tasks/haproxy.yml will contain the main code, install haproxy,
enable and start it. Configure the configuration based on the data
structure, check the configuration and restart the service if this
last one was changed.
- name: install haproxy
become: yes
package:
name: haproxy
state: present
- name: enable haproxy
become: yes
service:
name: haproxy
state: started
enabled: yes
- name: configure haproxy
become: yes
template:
src: haproxy.cfg.j2
dest: /tmp/haproxy.cfg.tmp
owner: root
group: root
mode: '0644'
- name: check haproxy configuration
become: yes
command: haproxy -c -f /tmp/haproxy.cfg.tmp
- name: haproxy configuration is valid, use it
become: yes
copy:
src: /tmp/haproxy.cfg.tmp
dest: /etc/haproxy/haproxy.cfg
owner: root
group: root
mode: '0644'
remote_src: yes
notify: restart haproxy
- name: clean temporary haproxy configuration
become: yes
file:
path: /tmp/haproxy.cfg.tmp
state: absent
templates/haproxy.cfg.j2 is a jinja template containing the main
haproxy configuration file stored in /etc/haproxy/haproxy.cfg. This
template is configured with default working values. Each values can be
set from the haproxy variable present in the ansible hosts variable
namespace.
global
chroot {{ haproxy['global']['chroot'] | default("/var/lib/haproxy") }}
daemon
group {{ haproxy['global']['group'] | default("haproxy") }}
log {{ haproxy['global']['log'] | default("127.0.0.1 local2") }}
maxconn {{ haproxy['global']['maxconn'] | default("4000") }}
pidfile {{ haproxy['global']['pidfile'] | default("/var/run/haproxy.pid") }}
stats socket /var/lib/haproxy/stats
user {{ haproxy['global']['user'] | default("haproxy") }}
defaults
log {{ haproxy['defaults']['log'] | default("global") }}
maxconn {{ haproxy['defaults']['maxconn'] | default("3000") }}
mode {{ haproxy['defaults']['mode'] | default("http") }}
{% if haproxy['defaults']['option'] is defined %}
{% else %}
option dontlognull
option forwardfor except 127.0.0.0/8
option httplog
option http-server-close
option redispatch
{% endif %}
retries {{ haproxy['defaults']['retries'] | default("3") }}
{% if haproxy['defaults']['timeout'] is defined %}
{% else %}
timeout check 10s
timeout client 1m
timeout connect 10s
timeout http-keep-alive 10s
timeout http-request 10s
timeout queue 1m
timeout server 1m
{% endif %}
{% if haproxy['frontends'] is defined %}
{% for frontend in haproxy['frontends'] %}
frontend {{ frontend['name'] }} {{ frontend['listen'] | default("*:5000") }}
{% if frontend['acls'] is defined %}
{% for acl in frontend['acls'] %}
acl {{ acl }}
{% endfor %}
{% endif %}
{% if frontend['use_backend'] is defined %}
use_backend {{ frontend['use_backend'] }}
{% endif %}
{% if frontend['default_backend'] is defined %}
default_backend {{ frontend['default_backend'] }}
{% endif %}
{% endfor %}
{% endif %}
{% if haproxy['backends'] is defined %}
{% for backend in haproxy['backends'] %}
backend {{ backend['name'] }}
balance {{ backend['balance'] | default('roundrobin') }}
{% if backend['servers'] is defined %}
{% for server in backend['servers'] %}
server {{ server }}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
handlers/main.yml contain all actions like, stop, start and restart
(or other events) to control haproxy service.
- name: restart haproxy
become: yes
service:
name: haproxy
state: restarted
With this role, a user can deploy an haproxy server only by editing the configuration from the data-structure stored in ansible.
# keepalived configuration
cd roles
# initialize tree
mkdir keepalived
mkdir -p keepalived/templates
mkdir -p keepalived/tasks
mkdir -p keepalived/handlers
# create files
touch keepalived/README.md
touch keepalived/tasks/main.yml
touch keepalived/tasks/keepalived.yml
touch keepalived/templates/keepalived.conf.j2
touch keepalived/handlers/main.yml
README.md is a documentation file.
tasks/main.yml is like the one create for haproxy, used only to
check if the keepalived variable is set correctly?
- name: include keepalived
include: keepalived.yml
when: keepalived is defined
tasks/keepalived.yml contains the main ansible code. It will
install, enable and configure keepalived. A configuration will be
generated based on the data structure and tested. If this
configuration is working, it will be placed on the default keepalived
configuration path in /etc/keepalived/keepalived.conf.
- name: install keepalived
become: yes
package:
name: keepalived
state: installed
- name: enable keepalived
become: yes
service:
name: keepalived
state: started
enabled: yes
- name: configure keepalived
become: yes
template:
src: keepalived.conf.j2
dest: /tmp/keepalived.conf.tmp
owner: root
group: root
mode: '0644'
- name: check keepalived configuration
become: yes
command: keepalived --check /tmp/keepalived.conf.tmp
- name: install keepalived configuration
become: yes
copy:
remote_src: yes
src: /tmp/keepalived.conf.tmp
dest: /etc/keepalived/keepalived.conf
owner: root
group: root
mode: '0644'
notify: restart keepalived
- name: clean temporary keepalived configuration
become: yes
file:
path: /tmp/keepalived.conf.tmp
state: absent
templates/keepalived.conf.j2 is the template to generate the
keepalived configuration file. This is a small one supported only the
required features.
global_defs {
notification_email {
{{ keepalived['global']['notification_email'] | default("root") }}
}
notification_email_from {{ keepalived['global']['notification_email_from'] | default("root") }}
smtp_server {{ keepalived['global']['smtp_server'] | default("localhost") }}
smtp_connect_timeout {{ keepalived['global']['smtp_connection_timeout'] | default("30") }}
}
{% if keepalived['instances'] is defined %}
{% for instance in keepalived['instances'] %}
vrrp_instance {{ instance['name'] }} {
state {{ instance['state'] | default("MASTER") }}
interface {{ instance['interface'] }}
virtual_router_id {{ instance['virtual_router_id'] }}
priority {{ instance['priority'] | default("1") }}
advert_int {{ instance['advert_int'] | default("1") }}
{% if instance['authentication'] is defined %}
authentication {
auth_type {{ instance['authentication']['type'] | default("PASS") }}
auth_pass {{ instance['authentication']['pass'] }}
}
{% endif %}
{% if instance['virtual_ipaddress'] is defined %}
virtual_ipaddress {
{% for ip in instance['virtual_ipaddress'] %}
{{ ip }}
{% endfor %}
}
{% endif %}
}
{% endfor %}
{% endif %}
handlers/main.yml was made to react to different event from the
other part of the code. Only the restart facility was made.
- name: restart keepalived
become: yes
service:
name: keepalived
state: restarted
# iptables configuration
cd roles
# initialize tree
mkdir iptables
mkdir -p iptabes/tasks
# create files
touch iptables/README.md
touch iptables/tasks/main.yml
README.md is a documentation file.
tasks/main.yml will contain the main iptables configuration. As
usual, it will be set based on the data-structure.
- name: disable firewalld
become: yes
service:
name: firewalld
state: enabled
state: started
- name: install iptables-services
become: yes
package:
name: iptables-services
state: latest
- name: enable iptables service
become: yes
service:
name: iptables
state: started
- name: allow access to http port
become: yes
iptables:
chain: INPUT
protocol: tcp
destination_port: 80
jump: ACCEPT
when: iptables['http'] is defined
- name: allow access to https port
become: yes
iptables:
chain: INPUT
protocol: tcp
destination_port: 443
jump: ACCEPT
when: iptables['https'] is defined
- name: backup iptables rules
become: yes
command: service iptables save
WARNING: it seems firewalld is installed and enabled by default on generic/centos7 image. This can lead to some trouble with iptables compatibility.
# Libvirt role
mkdir -p provisioning/roles/libvirt/tasks
mkdir -p provisioning/roles/libvirt/handlers
touch provisioning/roles/libvirt/tasks/main.yml
touch provisioning/roles/libvirt/tasks/libvirt.yml
touch provisioning/roles/libvirt/handlers/main.yml
tasks/main.yml:
- name: include libvirt configuration
include: libvirt.yml
when: libvirt is defined
tasks/libvirt.yml:
- name: install libvirt
become: yes
package:
name: libvirt
state: latest
- name: enable libvirtd
become: yes
service:
name: libvirtd
state: started
enabled: yes
handlers/main.yml:
- name: reload libvirtd
become: yes
service:
name: libvirtd
state: reloaded
# Firewalld Role
I create a firewalld role. I was not aware that firewalld was present in centos by default now. I am old school and prefere iptables, ip6tables or even nftables. Here the code to fallback to firewalld.
firewalld/tasks/main.yml
- name: include firewalld
include: firewalld.yml
when: firewalld is defined
firewalld/tasks/firewalld.yml
- name: disable iptables
service:
name: iptables
state: stopped
enabled: no
- name: enable firewalld
service:
name: firewalld
state: started
enabled: yes
- name: enable https access
become: yes
firewalld:
service: https
permanent: yes
state: enabled
- name: enable http access
become: yes
firewalld:
service: http
permanent: yes
state: enabled
# Using Ansible Provisioning
Both virtual machines are configured through ansible host variables
stored in provisioning/host_vars and called by the name of the
server.
touch provisioning/host_vars/localhost.yml
touch provisioning/host_vars/v1.yml
touch provisioning/host_vars/v2.yml
# localhost configuration
- hosts: localhost
connection: local
roles:
- nginx
- libvirt
# v1 Configuration
This configuration file is located in provisioning/host_vars/v1.yml
and contains the ansible host variable for v1 virtual machine. The
first part define the required network configuration.
network:
eth1:
ip: 46.20.249.165
netmask: 255.255.255.240
The second part defines HAproxy configuration. A frontend called test and listening to all interface on http port is created. A backend called t3 is created and forward the flow to 46.20.249.168 on port 80.
haproxy:
frontends:
- name: test
listen: "*:80"
use_backend: t3
default_backend: t3
backends:
- name: t3
balance: roundrobin
servers:
- t3 46.20.249.168:80 check
The third part of the configuration is about keepalived. One instance
is created on eth1 interface. VRRP information like router id is
defined. track_interface and unicast_peer are defined explicitely,
due to multicast or broadcast issue. An authentication method is also
defined. Finaly, 46.20.249.167/28 is shared between the two VMs.
keepalived:
instances:
- name: test
interface: eth1
virtual_router_id: 100
priority: 100
track_interface:
- eth1
unicast_peer:
- 46.20.249.166
authentication:
type: PASS
pass: 1234
virtual_ipaddress:
- 46.20.249.167/28
Because HAProxy is listening on port 80 (and probably one day on port 443), iptables should allow the flow.
iptables:
http: yes
https: yes
# v2 Configuration
Same as v1 configuration.
network:
eth1:
ip: 46.20.249.165
netmask: 255.255.255.240
haproxy:
frontends:
- name: test
listen: "*:80"
use_backend: t3
default_backend: t3
backends:
- name: t3
balance: roundrobin
servers:
- t3 46.20.249.168:80 check
keepalived:
instances:
- name: test
interface: eth1
virtual_router_id: 100
priority: 100
track_interface:
- eth1
unicast_peer:
- 46.20.249.166
authentication:
type: PASS
pass: 1234
virtual_ipaddress:
- 46.20.249.167/28
iptables:
http: yes
https: yes
# v3 Configuration
v3 configuration is pretty simple, we need to enable nginx and allow http flow
nginx:
iptables:
http: yes
https: yes
Because it is a server in private network, we need to create a NAT rule. The flow to 46.20.249.168:80 and 46.20.249.168:443 will be forwarded to 10.0.0.2:80 and 10.0.0.2:443 respectivily. This configuration can be done with iptables (on the host). Unfortunately, due to libvirt iptables configuration, I am a bit confuse... Too many rules are present and I don't really know how to alter them without breaking the service. Usually, the rule is pretty simple:
iptables -I FORWARD -d 46.20.249.168 -p tcp -dport 80 -d 10.0.0.2
iptables -t nat -I PREROUTING -p tcp --dport 80 -d 46.20.249.168 \
-j DNAT --to 10.0.0.2:80
Due this strange behaviour, I decided to find an alternative and use
nginx as a forwarder with proxy_pass feature. Here the modification
on the /etc/nginx/nginx.conf file on the host.
--- nginx.conf.old 2020-04-25 10:43:44.244383741 +0000
+++ nginx.conf 2020-04-25 10:42:36.360137288 +0000
@@ -36,6 +36,7 @@
include /etc/nginx/default.d/*.conf;
location / {
+ proxy_pass http://10.0.0.2;
}
error_page 404 /404.html;
To enable the configuration, just reload nginx.
sudo systemctl reload nginx
Maybe it is a better choice than using iptables, nginx can be easily configured dynamically with lua or other script language.
# Testing
Many different testing. Those steps should be automated with a monitoring tool.
# Network and service test
From my laptop to main server
ping -c1 46.20.249.168
# output:
# PING 46.20.249.168 (46.20.249.168): 56 data bytes
# 64 bytes from 46.20.249.168: icmp_seq=0 ttl=45 time=35.297 ms
#
# --- 46.20.249.168 ping statistics ---
# 1 packets transmitted, 1 packets received, 0.0% packet loss
# round-trip min/avg/max/std-dev = 35.297/35.297/35.297/0.000 ms
curl 46.20.249.168
# output:
# Hello, world!
From my laptop to v1
ping -c1 46.20.249.165
# output:
# PING 46.20.249.165 (46.20.249.165): 56 data bytes
# 64 bytes from 46.20.249.165: icmp_seq=0 ttl=45 time=35.419 ms
#
# --- 46.20.249.165 ping statistics ---
# 1 packets transmitted, 1 packets received, 0.0% packet loss
# round-trip min/avg/max/std-dev = 35.419/35.419/35.419/0.000 ms
curl 46.20.249.165
# output:
# Hello, world!
From my laptop to v2
ping -c1 46.20.249.166
# output:
# PING 46.20.249.166 (46.20.249.166): 56 data bytes
# 64 bytes from 46.20.249.166: icmp_seq=0 ttl=45 time=35.253 ms
#
# --- 46.20.249.166 ping statistics ---
# 1 packets transmitted, 1 packets received, 0.0% packet loss
# round-trip min/avg/max/std-dev = 35.253/35.253/35.253/0.000 ms
curl 46.20.249.166
# output:
# Hello, world!
From my laptop to vip
ping -c1 46.20.249.167
# output:
# PING 46.20.249.167 (46.20.249.167): 56 data bytes
# 64 bytes from 46.20.249.167: icmp_seq=0 ttl=45 time=44.936 ms
#
# --- 46.20.249.167 ping statistics ---
# 1 packets transmitted, 1 packets received, 0.0% packet loss
# round-trip min/avg/max/std-dev = 44.936/44.936/44.936/0.000 ms
curl 46.20.249.167
# output:
# Hello, world!
# VIP testing
Checking IP configuration on v1 VM:
ip a s dev eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:20:6b:d4 brd ff:ff:ff:ff:ff:ff
inet 46.20.249.165/28 brd 46.20.249.175 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe20:6bd4/64 scope link
valid_lft forever preferred_lft forever
Checking IP configuration on v2 VM:
ip a s dev eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:79:ae:c2 brd ff:ff:ff:ff:ff:ff
inet 46.20.249.166/28 brd 46.20.249.175 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet 46.20.249.167/28 scope global secondary eth1
valid_lft forever preferred_lft forever
VIP is defined on v2. To test if the VIP will move to v1, we will
suspend v2 server with vagrant suspend v2 on the host. After this
action, we got this output on v1 server:
ip a s eth1
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:20:6b:d4 brd ff:ff:ff:ff:ff:ff
inet 46.20.249.165/28 brd 46.20.249.175 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet 46.20.249.167/28 scope global secondary eth1
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe20:6bd4/64 scope link
valid_lft forever preferred_lft forever
and the service is still available.
curl 46.20.249.167
# output:
# Hello, world!
# Troubleshooting // FAQ
# I lost the connection when I start the bridge
You can use iDrac and get access to the remote console. Or, just modify the configuration without saving your change (like me) if you can't use iDrac.
# I got "no route to host" message on VM
This is probably due to iptables configuration. By default, RHEL
systems are using iptables rules. If you want to connect to a defined
port you need to add it explicitely in the chain IN_public_allow on
the host or other VMs.
iptables -A IN_public_allow -j ACCEPT -p tcp --destination-port 80
#
# Conclusion
Last time I configured a VRRP service on Linux was at Alcatel-Lucent, 10 years ago. It was really nice to recreate from scratch all the configuration.
Anyway, why I decided to create all role by myself? Because roles from ansible galaxy are not trustable for different reasons. The first one is due to the lack of announce in the community, it makes change harder. The second one is mainly because we need to control all the stack, to make that possible, we need to defined standard and convention. Ansible scripts are made by different people for different needs and are sometime not compatible with other one.
The pattern used here was used in different companies, like Afrinet, OVH and Leroy Merlin. I use it on my own project and gives the possibility to switch to another solution easily. For exemple, at OVH, this pattern was created to manage Salt, Ansible and Puppet script.
# Resources
- https://www.centos.org/forums/viewtopic.php?t=53972
# Bridge Configuration
- http://www.itzgeek.com/how-tos/mini-howtos/create-a-network-bridge-on-centos-7-rhel-7.html
- https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/sec-Network_Bridging_Using_the_Command_Line_Interface.html
- https://wiki.linuxfoundation.org/networking/bridge
- https://wiki.libvirt.org/page/Networking#Fedora.2FRHEL_Bridging
- https://wiki.archlinux.org/index.php/Network_bridge
# Secondary IP
- https://ma.ttias.be/how-to-add-secondary-ip-alias-on-network-interface-in-rhel-centos-7/
- https://community.spiceworks.com/topic/545859-add-secondary-ip-to-one-interface-in-centos-7
- https://www.ubiquityhosting.com/blog/configure-ip-ranges-on-centos-7-redhat-7/
# General Information
- http://www.linux-kvm.org/page/Networking
- https://wiki.libvirt.org/page/Networking#Bridged_networking_.28aka_.22shared_physical_device.22.29
- https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Virtualization_Deployment_and_Administration_Guide/sect-bridge-mode.html
# Setup routing with iptables (host routes and guest gateways) as described here:
- http://www.linux-kvm.org/page/Networking#Routing_with_iptables
# Vagrant
- https://www.vagrantup.com
- https://www.hashicorp.com/security.html
- https://www.vagrantup.com/docs/provisioning/ansible_local.html
- https://app.vagrantup.com/generic/boxes/centos7
- https://github.com/vagrant-libvirt/vagrant-libvirt
- https://www.vagrantup.com/docs/synced-folders/basic_usage.html
# Libvirt
- https://libvirt.org/
- https://github.com/vagrant-libvirt/vagrant-libvirt
# Ansible
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html
- https://jinja.palletsprojects.com/en/2.11.x/templates/
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html
- https://docs.ansible.com/ansible/latest/modules/copy_module.html
- https://docs.ansible.com/ansible/latest/modules/service_module.html
- https://docs.ansible.com/ansible/latest/modules/template_module.html
- https://docs.ansible.com/ansible/latest/modules/package_module.html
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_includes.html#includes-vs-imports
- https://docs.ansible.com/ansible/latest/scenario_guides/guide_vagrant.html
# Iptables
- https://linux.die.net/man/8/iptables
- https://docs.ansible.com/ansible/latest/modules/iptables_module.html
- https://wiki.libvirt.org/page/Networking#Forwarding_Incoming_Connections