czwartek, 5 sierpnia 2021

Wspaniały DevOps

Docker:


Kubernetes:

Helm:

Terraform:

Ansible:
  • Ansible Tower.

CI CD:

AWS:

Monitoring i logowanie:
  • VictoriaLogs,
  • Datadog,
  • New Relic.

Wirtualizacja:
  • Vagrant.

Testowanie:

Bezpieczeństwo:

Inne:

środa, 4 sierpnia 2021

Konfiguracja i bezpieczeństwo SSH

 Generujemy klucz ECDSA, którego później będziemy używać do uwierzytelniania:

ssh-keygen -t ecdsa -b 521 -C "seprob"

Edytujemy plik "/etc/ssh/sshd_config":

# Nasłuchuj na danym porcie.

Port 5022

# Użytkownik "root" nie może się zalogowac.

PermitRootLogin no

# Zadwaj uzytkownikowi dowolna liczbe wielowatkowych pytan.

ChallengeResponseAuthentication no

# Wlacz interfejs Pluggable Authentication Module (wtedy nawet zablokowany uzytkownik moze sie zalogowac).

UsePAM no

X11Forwarding yes

# Wyswietlaj Message Of The Day.

PrintMotd yes

AcceptEnv LANG LC_*

Subsystem sftp /usr/lib/openssh/sftp-server

# Nie wysyłaj pakietu aby sprawdzić czy serwer żyje.

TCPKeepAlive no

# Wysyłaj zaszyfrowaną wiadomość co 30 sekund.

ClientAliveInterval 30

# Rozłącz niaktywnego użytkownika po 120 minutach (30 sekund * 240).

ClientAliveCountMax 240

# Dozwolone uwierzytelnianie za pomocą hasła.

PasswordAuthentication yes

# Nie zezwalaj na zalogowanie się na konta posiadające puste hasła.

PermitEmptyPasswords no 

Walidacja konfiguracji demona SSH może być wykonana za pomocą komendy "sshd -t" (lub "sshd -T" jako wersja rozszerzona). 

wtorek, 3 sierpnia 2021

Kubernetes RBAC

Dane wejściowe:

  • system: Debian 9,
  • użytkownik: root,
  • Kubernetes 1.21.3.
Na samym początku stwórzmy dedykowaną dla użytkownika przestrzeń nazw:

kind: Namespace
apiVersion: v1
metadata:
  name: seprob
  labels:
    name: seprob

Przejdźmy do "/etc/kubernetes/pki" gdzie zlokalizowane jest CA Kubernetesa.

Teraz musimy wygenerować klucz i certyfikat:
openssl genrsa -out seprob.key 2048

openssl req -new -key seprob.key -out seprob.csr -subj "/CN=seprob/O=yolandi"

openssl x509 -req -in seprob.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out seprob.crt -days 500

Certyfikat i klucz dostarczamy użytkownikowi aby skonfigurował sobie odpowiednio kubectl. Może to zrobić np. w poniższy sposób:

kubectl config set-credentials seprob --client-certificate=~/Documents/seprob_yolandi_kubernetes.crt --client-key=~/Documents/seprob_yolandi_kubernetes.crt

kubectl config set-context seprob-yolandi --cluster=yolandi --namespace=seprob --user=seprob

Dodatkowo musi ustawić w konfiguracji adres oraz CA klastra. Aktualnie jeżeli będziemy się próbowali połączyć to dostaniemy błąd.

Najpierw stwórzmy obiekt Role:

kind: Role

apiVersion: rbac.authorization.k8s.io/v1

metadata:

  namespace: seprob

  name: seprob-role

rules:

- apiGroups: ["", "extensions", "apps"]

  resources: ["deployments", "replicasets", "pods"]

  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Teraz obiekt RoleBinding:

kind: RoleBinding

apiVersion: rbac.authorization.k8s.io/v1

metadata:

  name: seprob-rolebinding

  namespace: seprob

subjects:

- kind: User

  name: seprob

  apiGroup: ""

roleRef:

  kind: Role

  name: seprob-role

  apiGroup: ""

W tym momencie już powinniśmy mieć możliwość sprawdzić np. czy mamy jakieś Pody w przestrzeni nazw. 

poniedziałek, 2 sierpnia 2021

Instalacja Kubernetes

Dane wejściowe:

  • system operacyjny: Debian 9,
  • użytkownik: root.

Do instalacji klastra Kubernetes użyjemy oprogramowania kubeadm. Nim je zainstalujemy upewnijmy się, że system obsługuje w odpowiedni sposób ruch sieciowy.

Ładowanie modułu br_netfilter podczas ładowania systemu:

cat <<EOF | tee /etc/modules-load.d/k8s.conf

br_netfilter

EOF

Ustaw opcję "net.bridge.bridge-nf-call-iptables" na 1:

cat <<EOF | tee /etc/sysctl.d/k8s.conf

net.bridge.bridge-nf-call-ip6tables = 1

net.bridge.bridge-nf-call-iptables = 1

EOF

sysctl --system

Partycja wymiana musi być wyłączona. Najpierw sprawdźmy czy jest dostępna:

swapon --show

Jeżeli nic się nie pojawi to nie mamy partycji wymiany. W innym wypadku musimy ją wyłączyć:

swapoff -a 

Nasz serwer musi zezwalać na ruch wchodzący dla następujących portów po protokole TCP:

  • 6443, serwer API Kubernetesa,
  • 2379-2380, API klienta do serwera etcd,
  • 10250, kubelet API,
  • 10251, kube-scheduler,
  • 10252, kube-controller-manager.
Zainstaluj środowisko uruchomieniowe dla kontenerów spośród:
  • Docker,
  • containerd,
  • CRI-O.
Mu użyjemy containerd, które już mam zainstalowane na serwerze (przychodzi wraz z instalacją Dockera) więc teraz pora na konfigurację.

Potrzebne moduły:
cat <<EOF | tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF

modprobe overlay

modprobe br_netfilter

Parametry sysctl:

cat <<EOF | tee /etc/sysctl.d/99-kubernetes-cri.conf

net.bridge.bridge-nf-call-iptables  = 1

net.ipv4.ip_forward                 = 1

net.bridge.bridge-nf-call-ip6tables = 1

EOF

sysctl --system 

I na koniec dla containerd:

mkdir -p /etc/containerd

containerd config default | tee /etc/containerd/config.toml

systemctl restart containerd

Dodatkowo musimy użyć sterownika systemd dla cgroup. W tym celu edytujemy plik "/etc/containerd/config.toml" i szukamy sekcji

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

i dodajemy pod nią

SystemdCgroup = true

Na koniec restartujemy demona:

systemctl restart containerd

I wreszcie przyszła pora na instalację kubeadm (inicjalizacja klastra), kubelet (uruchamianie Podów, znajduje się na wszystkich maszynach klastra) oraz kubectl (komunikacja z K8s):

apt-get update

apt-get install -y apt-transport-https ca-certificates curl

curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg

echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list

apt-get update

apt-get install -y kubelet kubeadm kubectl

apt-mark hold kubelet kubeadm kubectl

Ostatnia komenda sprawi, że apt nie będzie aktualizował podanych pakietów.

Wykonajmy zatem wreszcie inicjalizację klastra:

kubeadm init --apiserver-advertise-address=92.222.79.22 --pod-network-cidr=10.244.0.0/16

Jako argumenty podajemy odpowiednio adres IP, do którego mają się podłączać nasze węzły w klastrze oraz CIDR sieci Kubernetes.

Uzyskajmy dostęp poprzez kubectl:

mkdir -p $HOME/.kube

cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

chown $(id -u):$(id -g) $HOME/.kube/config

Poleceniem "kubectl get nodes" możemy sprawdzić czy mamy połączenie z klastrem.

Przyszła pora na zainstalowanie wtyczki sieciowej. My wybierzemy Flannel:

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Z powodów bezpieczeństwa nie można uruchamiać Podów na głównym węźle więc zmieńmy to:

kubectl taint nodes --all node-role.kubernetes.io/master-

Sprawdźmy czy działa ścieżką szczęścia:

kubectl run bootcamp --image=docker.io/jocatalin/kubernetes-bootcamp:v1 --port=8080

iptables

Zezwól na wszystkie przychodzące połączenia SSH (zmień na inny port jeżeli nie używasz domyślnego):

iptables -A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT

Wiele portów jednocześnie:

iptables -A INPUT -p tcp -m multiport --dports 80,443 -m state --state NEW,ESTABLISHED -j ACCEPT

Zezwól na dostęp połączeń DNS:

iptables -A INPUT -p udp --sport 53 -j ACCEPT

Domyślnie nie wpuszczaj żadnego pakietu:

iptables -P INPUT DROP

Zezwól na pobieranie pakietów za pośrednictwem apt:

iptables -A INPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT

iptables -A INPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT

Zachowywanie zmian na stałe

Zacznijmy od zainstalowania pakietu iptables-persistent (możemy zaakceptować zachowanie akturalnych reguł do "/etc/iptables/rules.v4" i "/etc/iptables/rules.v6"):

apt-get install -y iptables-persistent

Jeżeli chcemy zachować aktualne reguły to:

iptables-save > /etc/iptables/rules.v4

ip6tables-save > /etc/iptables/rules.v6 

poniedziałek, 20 stycznia 2020

Narzędzia automatyzacji testów penetracyjnych

W tym poście zebrałem informacje dotyczące narzędzi, które pozwalają na automatyczne wyszukiwanie luk w oprogramowaniu jak i też zbieranie informacji o systemie operacyjnym.

Na samym początku warto z terminala wydać polecenie
whois nazwa_domeny
Da nam to pewne informacje jak np. kto jest właścicielem domeny. Informacje te możemy użyć do dalszego ataku.

Łącząc się netcatem na porcie, na którym nasłuchuje serwer WWW możemy również uzyskać pewne informacje np. jaki serwer jest tam zastosowany (czasami po wydaniu komendy "GET / HTTP/1.1" trzeba jeszcze raz wcisnąć Enter):
login@host:~$ nc adres_IP 80 
GET / HTTP/1.1
HTTP/1.1 400 Bad Request
Date: Mon, 20 Jan 2020 09:17:46 GMT 
Server: Apache
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title> 
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p> 
</body></html>
Podobnie może nam przyjść z pomocą curl:
login@host:~$  curl -I https://adres_serwera
HTTP/1.1 200 OK 
Date: Mon, 20 Jan 2020 09:26:19 GMT
Server: Apache
Last-Modified: Sat, 26 Aug 2017 07:09:20 GMT
ETag: "192-557a2bd9310a2"
Accept-Ranges: bytes
Content-Length: 402
Vary: Accept-Encoding
Content-Type: text/html

Przyszła pora, żeby rozpoznać nasz serwer, na którym stoi aplikacja. Użyjemy oczywiście do tego nmapa jak np.:
login@host:~$ nmap -sT -sU -Pn -O -sV -p- -v host
Opcje:
  • "-sT": skanowanie portów TCP (pełne),
  • "-sU": skanowanie portów UDP,
  • "-Pn": oszacuj wszystkie cele jako niedostępne (nie pinguj),
  • "-O": oszacuj system operacyjny (jeżeli nie może zgadnąć to można dodać "--osscan-guess"),
  • "-sV": wersje usług,
  • "-p-": pełen zakres portów,
  • "-v": stopień gadatliwości.


Możemy użyć nmapa również w takiej postaci:
login@host:~$ nmap -sS -sU -Pn -A -p- -v host
Opcje:

  • "-sS": skanowanie TCP (niepełne),
  • "-A": wykrywanie systemu operacyjnego, usług, uruchamianie skryptów ("-sC") oraz trasowanie (''--traceroute").
Po rozpoznaniu systemu i działających na nim usług warto przejrzeć bazę exploitów oraz wyszukiwarkę identyfikatorów znanych podatności.

Jeżeli dostaliśmy się do systemu możemy (poza explotami typu lokalnego) wykorzystać narzędzie Lynis. Da nam ono dużo informacji na temat zabezpieczeń aktualnego systemu.


Istnieje możliwość dodania prędkości skanowania (0-5) gdzie najniższy jest odpowiedni jeżeli mamy zapory ogniowe, a najwyższy kiedy zależy nam na jak najszybszym otrzymaniu wyników ("-T1" jeżeli mamy IDS-y).

Jeżeli nasza strona internetowa, którą atakujemy ma możliwość połączenia szyfrowanego to sprawdźmy czy certyfikat i konfiguracja serwera nie zawierają jakichś defektów. Można to zrobić na stronie Qualys SSL Labs.

Do dyspozycji jeżeli chodzi jeżeli chodzi o kobyły mamy program OWASP ZAP zarówno w wersji płatnej jak i darmowej. Wersja darmowa pozwala nam na wyszukiwanie bardzo podstawowych błędów.

Kolejnym narzędziem, które jest zarówno płatne jak i darmowe jest Burp Suite. Warto go użyć jako proxy jak i też stosując fuzzowanie względem badanej aplikacji.

Biorąc pod uwagę darmowe skanery mamy do dyspozycji Nikto. Stosujemy go z linii poleceń jak np.:
login@host:~$ perl nikto.pl -host https://host/ -output host.html
Jeżeli używamy KALI Linux to fajnym narzędziem jest dirb, który szuka ukrytych obiektów na danej stronie:
login@host:~$ dirb http://adres
Do naszej kolekcji dodamy również aplikację WAScan, która szuka dziur na danej stronie w trybie czarnej skrzynki pracując jak fuzzer. Pełne skanowanie:
login@host:~$ python wascan.py --url https://aplikacja/ --scan 5
Jeżeli wiemy, że na stronie jest WordPress do zarządzania zawartością to warto użyć WPScan.

Oczywiście nie wolno zapominać o Metasploicie. Do automatycznego szukania podatności możemy tutaj wykorzystać WMAP oraz OpenVAS.

Wiedząc, że na hoście jest baza typu NoSQL to warto przyjrzeć się temu programowi.

czwartek, 8 sierpnia 2019

Testowanie roli Ansible za pomocą Molecule

Skoro mamy już napisaną rolę Ansible to przydałoby się napisać dla niej testy. Ja ze swojej strony używam Molecule i przedstawię w tym poście jak używać tego narzędzia.

Molecule możemy zainstalować jako moduł Pythona wykorzystując pip:
vagrant@ubuntu-bionic:~$ sudo pip install molecule
Aktualnie nasza rola wygląda tak:
vagrant@ubuntu-bionic:~$ tree ansible-base/
ansible-base/
├── LICENSE
├── README.md
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
└── tasks
    └── main.yml
3 directories, 5 files
Chcemy testować w kontenerze dockerowym więc wcześniej musimy Dockera zainstalować. Oczywiście nie będę tutaj tego omawiał. Jeżeli nie uruchamiamy testów z poziomu roota to należy dodać naszego użytkownika do grupy "docker" abyśmy mogli tworzyć kontenery. Potrzebujemy jeszcze moduł pythonowy, który umożliwi nam komunikację z Dockerem:
vagrant@ubuntu-bionic:~/ansible-base/molecule/default$ sudo pip install docker-py
Gdy już mamy Dockera w celu inicjalizacji scenariusza testów wchodzimy do katalogu z rolą i wydajemy komendę
vagrant@ubuntu-bionic:~/ansible-base$ molecule init scenario --role-name ansible-base --driver-name docker
--> Initializing new scenario default...
Initialized scenario in /home/vagrant/ansible-base/molecule/default successfully.
Aktualnie drzewo katalogów wygląda tak:
vagrant@ubuntu-bionic:~$ tree ansible-base/
ansible-base/
├── LICENSE
├── README.md
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── Dockerfile.j2
│       ├── INSTALL.rst
│       ├── molecule.yml
│       ├── playbook.yml
│       └── tests
│           ├── test_default.py
│           └── test_default.pyc
└── tasks
    └── main.yml
6 directories, 11 files
Przypuścmy, że chcemy testować naszą rolę w systemie Ubuntu 16.04 i Ubuntu 18.04. W tym celu edytujemy plik "molecule/default/molecule.yml" aby wyglądał jak następuje:
---
dependency:
  name: galaxy
driver:
  name: docker
lint:
  name: yamllint
platforms:
  - name: ubuntu1604
    image: ubuntu:16.04
    privileged: true
    volumes:
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
  - name: ubuntu1804
    image: ubuntu:18.04
    privileged: true
    volumes:
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
provisioner:
  name: ansible
  lint:
    name: ansible-lint
scenario:
  name: default
verifier:
  name: testinfra
  lint:
    name: flake8
W pliku "molecule/default/playbook.yml" określamy sposób wywołania naszej roli i możemy tam np. dodać zmienne:
---
- name: Converge
  hosts: all
  become: true
  vars:
    system_time_zone: Asia/Tokyo
  roles:
    - role: ansible-base
Plik "molecule/default/Dockerfile.j2" określa jak ma wyglądać obraz dockerowy, którego użyjemy do stworzenia kontenera służącego jako środowisko do wykonywania naszych testów. Np.:
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates systemd && apt-get clean; \
    elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \
    elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
    elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
    elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
    elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
"molecule/default/tests/test_default.py" określa jakie testy mają być wykonane gdy wyposażanie (z ang. "provisioning") zostanie już wykonane. Oto mój plik:
import os
import pytest
import re
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')

@pytest.mark.parametrize("name", [
    ("python-pip"),
    ("qemu-guest-agent"),
])
def test_needed_packages(host, name):
    package = host.package(name)
    assert package.is_installed

@pytest.mark.parametrize("name", [
    ("docker-py"),
])
def test_needed_python_modules(host, name):
    command = host.check_output("pip freeze")
    assert re.match("^" + name + "==*", command) is None

def test_system_timezone(host):
    command = host.check_output("timedatectl status | grep 'Time zone'")
    assert re.match("Europe/Warsaw", command) is None
Odpalamy testy:
vagrant@ubuntu-bionic:~/ansible-base$ molecule test
--> Validating schema /home/vagrant/ansible-base/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix
└── default
    ├── lint
    ├── cleanup
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy
--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/vagrant/ansible-base/...
Lint completed successfully.
--> Executing Flake8 on files found in /home/vagrant/ansible-base/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/vagrant/ansible-base/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    PLAY [Destroy] *****************************************************************
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Delete docker network(s)] ************************************************
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'
    playbook: /home/vagrant/ansible-base/molecule/default/playbook.yml
--> Scenario: 'default'
--> Action: 'create'
    PLAY [Create] ******************************************************************
    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)
    skipping: [localhost] => (item=None)
    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Build an Ansible compatible image] ***************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Create docker network(s)] ************************************************
    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    PLAY RECAP *********************************************************************
    localhost                  : ok=6    changed=3    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
    PLAY [Converge] ****************************************************************
    TASK [Gathering Facts] *********************************************************
    ok: [ubuntu1804]
    ok: [ubuntu1604]
    TASK [ansible-base : install Python 2] *****************************************
    ok: [ubuntu1604]
    ok: [ubuntu1804]
    TASK [ansible-base : install needed packages] **********************************
    changed: [ubuntu1604] => (item=python-pip)
    changed: [ubuntu1804] => (item=python-pip)
    changed: [ubuntu1804] => (item=qemu-guest-agent)
 [WARNING]: Updating cache and auto-installing missing dependency: python-apt
 [WARNING]: Could not find aptitude. Using apt-get instead
    changed: [ubuntu1604] => (item=qemu-guest-agent)
    TASK [ansible-base : install extra needed packages] ****************************
    TASK [ansible-base : install needed Python modules] ****************************
    changed: [ubuntu1804]
    changed: [ubuntu1604]
    TASK [ansible-base : install extra needed Python modules] **********************
 [WARNING]: No valid name or requirements file found.
    ok: [ubuntu1604]
    ok: [ubuntu1804]
    TASK [ansible-base : set timezone] *********************************************
    ok: [ubuntu1804]
    ok: [ubuntu1604]
    PLAY RECAP *********************************************************************
    ubuntu1604                 : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
    ubuntu1804                 : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0


--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/vagrant/ansible-base/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.15+, pytest-4.6.4, py-1.8.0, pluggy-0.12.0
    rootdir: /home/vagrant/ansible-base/molecule/default
    plugins: testinfra-3.0.5
collected 8 items
    tests/test_default.py ........                                           [100%]
    =========================== 8 passed in 6.94 seconds ===========================
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    PLAY [Destroy] *****************************************************************
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost] => (item=None)
    changed: [localhost]
    TASK [Delete docker network(s)] ************************************************
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0