I'm using Ansible only for Configuration Management, the server is up and I want to configure users, install packages and configure them.
For infrastructure provisioning terraform.io is nice!
Currently, my deployment flow includes Drone.io/GitlabCI for CI/CD and Docker Swarm for orchestrating containers.
- Ansible for Configuration Management
- Tasks
- Users, Ansible Vault and Password Stuff
- Use the secrets you stored in Ansible Vault in your Ansible Playbooks
- My user needs to be able to use sudo
- My ansible user needs a sudo password in order to execute priviledged tasks
- Setting password for a unix user
- Create a user and copy the authorized_keys to the new user
- Disallow root SSH access and disallow password authentication
- UFW Basics
- APT
- Styleguide
- Errors & Solutions
Ansible will discover some information about the hosts when running, and you can access like a variable, they are defined by default.
If you run this simple task:
- debug: var=hostvars
# OR
- debug: var=ansible_facts
I won't paste the output here, but you can see here.
Check how you can access the variables.
The variable you're looking for is: {{ inventory_hostname }}
.
"Accessing information about other hosts with magic variables"
Let's say you want to import tasks/users.yml in your tasks/main.yml, you can simply do:
- name: Import user management tasks
include_tasks: users.yml
tags:
- "initial-setup"
But if you run:
ansible-playbook site.yml -i hosts.yml --tags=initial-setup
You'll see that you still need to add the tag initial-setup
to all tasks in tasks/users.yml in order to run only tasks with our chosen tag.
But if you want to import/include a file and apply a tag to all tasks without manually adding tags to several tasks, just use args: apply: tags:
- name: Import user management tasks
include_tasks: users.yml
tags:
- initial-setup
# This let's me apply tags to all tasks imported with `include_tasks`
args:
apply:
tags:
- initial-setup
Now when you run playbooks with --tags=initial-setup
it will execute the task "Import user management tasks" and every task you're including from tasks/users.yml.
Using tags:
More use cases of args:
- https://docs.ansible.com/ansible/latest/modules/script_module.html
- https://docs.ansible.com/ansible/latest/modules/shell_module.html
ansible-playbook site.yml -i hosts.yml --ask-vault-pass
If using encrypted files you can encrypt one of those vars/main.yml, host_vars/hostname.yml in order to have variables automatically available in your tasks.
- https://docs.ansible.com/ansible/latest/user_guide/vault.html#creating-encrypted-files
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout
Single encrypted variables are nice but refer to Error & Solutions below because there's a catch.
- https://docs.ansible.com/ansible/latest/user_guide/vault.html#use-encrypt-string-to-create-encrypted-variables-to-embed-in-yaml
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_vault.html#single-encrypted-variable
Add the user to sudo
group.
- name: "Create user"
user:
name: "myuser"
groups:
- "sudo"
# ...
Note: you'll find online ways to add user to sudoers
file, if you want to do that way.
Check out priviledge escalation: https://docs.ansible.com/ansible/latest/user_guide/become.html
- You could run
ansible-playbook
with--ask-become-pass
flag. - Or define
ansible_become_pass
in your hosts configuration.
https://docs.ansible.com/ansible/latest/user_guide/become.html#passwords-for-enable-mode
hosts.yml
all:
hosts:
host_a:
ansible_connection: ssh
ansible_user: host_a
ansible_become_pass: "{{ host_a_password }}"
ansible_host: 111.11.111.111
ansible_port: 22
host_vars/host_a.yml
host_a_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653063336164663966303231363934653561363964363833313662
6431626536303530376336343832656537303632313433360a626438346336353331386135323734
62656361653630373231613662633962316233633936396165386439616533353965373339616234
3430613539666330390a313736323265656432366236633330313963326365653937323833366536
34623731376664623134383463316265643436343438623266623965636363326136
Note: Don't forget to check ansible-playground --help
there is a lot more you can do with just flags:
Connection Options:
control as whom and how to connect to hosts
-k, --ask-pass ask for connection password
--private-key=PRIVATE_KEY_FILE, --key-file=PRIVATE_KEY_FILE
use this file to authenticate the connection
-u REMOTE_USER, --user=REMOTE_USER
connect as this user (default=None)
-c CONNECTION, --connection=CONNECTION
connection type to use (default=smart)
-T TIMEOUT, --timeout=TIMEOUT
override the connection timeout in seconds
(default=10)
--ssh-common-args=SSH_COMMON_ARGS
specify common arguments to pass to sftp/scp/ssh (e.g.
ProxyCommand)
--sftp-extra-args=SFTP_EXTRA_ARGS
specify extra arguments to pass to sftp only (e.g. -f,
-l)
--scp-extra-args=SCP_EXTRA_ARGS
specify extra arguments to pass to scp only (e.g. -l)
--ssh-extra-args=SSH_EXTRA_ARGS
specify extra arguments to pass to ssh only (e.g. -R)
When creating users you have to provide a hashed password, not the plaintext version. (don't forget to use Ansible Vault to store secrets)
You could use things like mkpasswd
if available, or even Python snnipets you find online like:
python -c "from passlib.hash import sha512_crypt; import getpass; print(sha512_crypt.hash(getpass.getpass()))"
But if you're templating, you can use the jinja filter password_hash
:
{{ password | password_hash('sha512') }}
Let's say user root
is my current user.
NOTE: You can do a lot with the authorized key module, so it's worth to check it out:
- name: "Create main user"
user:
# define user attrs here
- name: "Ensure .ssh dir for our new user is present"
file:
state: directory
path: /home/my_new_user/.ssh/
owner: my_new_user
group: my_new_user
mode: 0700
- name: "Copy authorized_keys from root to new user"
copy:
remote_src: "yes"
# root authorized_keys file
src: "/root/.ssh/authorized_keys"
dest: "/home/my_new_user/.ssh/authorized_keys"
owner: my_new_user
group: my_new_user
mode: 0600
tags:
- initial-setup
- https://docs.ansible.com/ansible/latest/modules/user_module.html
- https://docs.ansible.com/ansible/latest/modules/file_module.html
- https://docs.ansible.com/ansible/latest/modules/copy_module.html
For more secure access only use SSH Key-Based authentication.
- name: "Disallow password authentication"
lineinfile:
dest: "/etc/ssh/sshd_config"
regexp: "^PasswordAuthentication"
line: "PasswordAuthentication no"
state: "present"
notify: "Restart ssh"
- name: "Disallow root SSH access"
lineinfile:
dest: "/etc/ssh/sshd_config"
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
state: "present"
notify: "Restart ssh"
UFW (Uncomplicated Firewall) helps you to not going crazy with iptables
stuff.
Some basic tasks you may find useful:
# Denying all incoming and allowing all outgoing connections.
# So we can specify later what incoming to allow.
- name: "Configure ufw defaults"
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
with_items:
- { direction: 'incoming', policy: 'deny' }
- { direction: 'outgoing', policy: 'allow' }
notify:
- "Restart ufw"
# That's a pretty common task for a developer:
# - allow ssh conections
# - allow incoming traffic on 80 (http)
# - allow incoming traffic on 443 (https)
- name: "Configure access for common ports ssh/http/https"
ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
with_items:
- { rule: 'limit', port: '{{ ssh_port | default("22") }}', proto: 'tcp' }
- { rule: 'allow', port: '80', proto: 'tcp' }
- { rule: 'allow', port: '443', proto: 'tcp' }
notify:
- "Restart ufw"
- name: "Enable ufw logging"
ufw:
logging: on
notify:
- "Restart ufw"
- name: "Start and enable ufw service"
ufw:
state: "enabled"
Define a simple handler for restarting ufw
every time you make changes:
- name: "Restart ufw"
service:
name: "ufw"
state: "restarted"
If you need to configure ufw rules for your Swarm Manager:
- name: "Configure access for Swarm Manager host"
ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
with_items:
- { rule: 'allow', port: '2376', proto: 'tcp' }
- { rule: 'allow', port: '2377', proto: 'tcp' }
- { rule: 'allow', port: '7946', proto: 'tcp' }
- { rule: 'allow', port: '7946', proto: 'udp' }
- { rule: 'allow', port: '4789', proto: 'udp' }
notify:
- "Restart ufw"
If you need to configure ufw rules to allow your Swarm Worker:
- name: "Configure access for Swarm Worker host"
ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
with_items:
- { rule: 'allow', port: '2376', proto: 'tcp' }
- { rule: 'allow', port: '7946', proto: 'tcp' }
- { rule: 'allow', port: '7946', proto: 'udp' }
- { rule: 'allow', port: '4789', proto: 'udp' }
notify:
- "Restart ufw"
IMPORTANT NOTE:
If you don't know already Docker doesn't respect UFW all the time, since Docker can manage iptables
just like UFW does, it can interfere with rules previously applied by ufw
.
If you search online for "ufw docker" you'll discover a lot of discussions.
You may want to try this one, the README is very informative:
Most of the times this happened when upgrading with apt-get upgrade
:
- name: "Upgrade packages to the latest version available"
apt:
upgrade: "safe"
On Google you'll see that there's no definitive answer, so if you have to cancel the task and you get stuck even when doing manually now remember to kill the apt
/aptitude
/dpkg
that are still running:
Check if they are running:
ps aux
# OR
ps aux | grep "aptitude"
Kill them!
pkill aptitude
pkill dpkg
# Define your variable
common_packages:
- htop
- unattended-upgrades
- fail2ban
- ufw
# and then in your tasks:
- name: "Install common packages"
apt:
state: "present"
pkg: "{{ common_packages }}"
You don't need to use loops anymore.
There are some Linus distributions with automatic updates built-in, like CoreOS. If we're using Debian, we can make this happen too, with Unattended Upgrades:
- name: "Install unattended-upgrades package"
apt:
state: "present"
pkg: "unattended-upgrades"
- name: Adjust APT update intervals
copy:
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
dest: /etc/apt/apt.conf.d/10periodic
- Two spaces identation
- Snake case for variables (
variable_name
) - Always quote strings
- Name your hosts with
_
instead of-
(myhost_web
), so you don't face issues when using in variables. - roles/x/vars/main.yml are a nice place for variables, unless you have a lot of global/shared vars then group_vars and host_vars are the way to go
- Multiline strings
- Stick with
true
andfalse
for booleans - DON'T do that one line nonsense:
file: 'path=x state=file mode=0755 owner=me group=me'
, vertical files are better for reading - Don't go crazy on many variables override, cause you can go insane with variable precedence
- You may want to add to your .gitignore:
site.retry
You have an inline encrypted variable, not an encrypted file vault, and you want to transform that value with using some jinja2 filters using the pipe |
.
In my case, I was trying to use the jinja2 filter password_hash
in my_password
variable.
vars/main.yml
mysecret: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653063336164663966303231363934653561363964363833313662
6431626536303530376336343832656537303632313433360a626438346336353331386135323734
62656361653630373231613662633962316233633936396165386439616533353965373339616234
3430613539666330390a313736323265656432366236633330313963326365653937323833366536
34623731376664623134383463316265643436343438623266623965636363326136
When using mysecret
I was simply doing something like {{ devtools_password | password_hash('sha512') }}
.
And the error was something like:
fatal: [remote]: FAILED! => {"msg": "Unexpected templating type error occurred on ({{ mysecret | password_hash('sha512') }}): secret must be unicode or bytes, not ansible.parsing.yaml.objects.AnsibleVaultEncryptedUnicode"}
The solution was to "force" Ansible to decrypt the variable so I can apply filters.
Check my tasks/users.yml:
- name: "Create main user"
user:
name: "myuser"
password: "{{ '%s' | format(mysecret) | password_hash('sha512') }}"
# ...
That probably is some problem with lazy evaluation or stuff like that.
More:
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html
- https://docs.ansible.com/ansible/latest/user_guide/playbooks_vault.html#single-encrypted-variable
- https://docs.ansible.com/ansible/latest/user_guide/vault.html#use-encrypt-string-to-create-encrypted-variables-to-embed-in-yaml
- ansible/ansible#24425
Thank you for the Errors & Solutions section, I was doing the same exact thing with the password_hash and vault.