Headless Ansible

I’m trying to learn Ansible at the same time as setting it up as a power outage shutdown orchestration box. I keep running into issues.

I started by making a user on each machine that’ll need to be controlled named ansible and placed a new ssh public key in each .ssh folder. I wasn’t sure where to put the private key on the control node so I put it in /etc/ansible. Was that a bad idea?
I linked it with ansible_ssh_private_key_file in my hosts file.
I was getting Load Key Permission denied errors when I did this so I tried running Ansible with sudo but then it seems all the host validation was lost. Is the regular way to do host key checking just using ssh to connect to the server first or is there another better way to do that?

Eventually this playbook will be executed by a script called when upsmon needs to shut everything down. Where should I put my inventory, hosts, key, etc. Should I bother encrypting the ssh private key?

Charles

1 Like

Hey @charlespick , this is a good little article I often use when looking at SSH keys - How To Configure SSH Key-Based Authentication on a Linux Server | DigitalOcean

Some other commands you might find helpful

Add hosts to the (ssh) known hosts file

ssh-keyscan -H hostname.domain.local >> ~/.ssh/known_hosts

Add your ssh key to the hosts in your inventory

ansible hostname* -m authorized_key -a “user=‘your-user’ state=‘present’ key=‘{{lookup(‘file’,’~/.ssh/id_rsa.pub’)}}'” -u your-user -i inventory/hosts.yml -k

check that it all worked with an ansible ping

ansible -m ping -u your-user -i inventory/hosts.yml hostname*

I tend to leave the public and private key in the default location for the user i.e. ~/.ssh/ that makes things pretty easy. The SSH keys also are pretty sensitive about permissions on the folder it’s stored in. You can move it as you’ve done but you will need to specify the location of it whenever you use it.

Normally I would always avoid putting my private key into the Inventory unless it was encrypted with something like Ansible Vault - ansible-vault — Ansible Documentation The public key you can share around wherever you need it.

2 Likes

Hi, I have Ansible working just find in user space. The issue is that Ansible will eventually be launched as another user than my personal account. It’s going to be launched by NUT when the UPS runs low. I don’t know if I want to modify the service account to have a home folder with it’s own .ssh etc. Is that just what I need to do?

Charles

Basically you just need to make sure, that the user which will run Ansible is able to reach the target machines via ssh without being prompted. You can test it, if you switch to this user.
Putting the key in the default location would be the easiest way to achieve this but if you choose another method that should be fine.

1 Like

Hey Charles !

I wasn’t sure where to put the private key on the control node so I put it in /etc/ansible. Was that a bad idea?

Sort of. Having your private key outside of “the user who use it”'s home means other non-privileged users might be able to use it as well, or at least access the file. It’s just best practice to limit private keys exposure.
That being said, if you want to keep it there, just ensure the file belongs to your user and not root. Also check ACL, especially if you run sshd in strict mode (it should be 0600 in most cases).

I linked it with ansible_ssh_private_key_file in my hosts file

Note that Ansible relies heavily on your existing ssh config, so you could also define all your hosts ssh config in ~/.ssh/config without having to specify anything in Ansible configuration (be it hosts file, vars or envvars). I find it somewhat easier to manage ssh hosts config this way, as I’m already using it to join my servers anyway, though I have set ANSIBLE_SSH_ARGS envvar to use a Jumpbox and a few other parameters. I’m also more comfortable with pure ssh config.

See the ssh_config man page: man ssh_config for more info.

so I tried running Ansible with sudo but then it seems all the host validation was lost.

When you run command with sudo without specifying a target account, it defaults to use root. In both cases, you run commands as another user who doesn’t have your existing configuration and perhaps can’t access your files. You shouldn’t have to do that. Also see above for ownership and permissions warning.

Is the regular way to do host key checking just using ssh to connect to the server first or is there another better way to do that?

Ssh connection on an unknown node will prompt for hostkeys approval, but you might either / or:

  • Use ssh-keyscan prior to pre-fill your ~/.ssh/known_hosts with hostkeys hashes; here is an example one-liner to do precisely that for every host listed in your inventory files (under ./inventories):

    for host in $(ansible -i ./inventories/ all --list-hosts | tail -n+2 | sed -e 's/\\s\\+//g'); do $(which ssh-keygen) -t rsa -F "${host}" || $(which ssh-keyscan) -t rsa "${host}" >> ~/.ssh/known_hosts; done
    
  • Manually add hostkeys hashes to ~/.ssh/known_hosts

  • Disable hostkeys checking in Ansible configuration

Where should I put my inventory, hosts, key, etc.

It depends on how you plan to use them. You should obviously version your config files (not your private keys !!), but there are no rules for how to organize them on filesystem. Well, there are some rules, for example host_vars and group_vars folders which have to be either next to your inventories or your playbook. Also consider where you put your optional ansible.cfg file.
Have a look to best practices for useful suggestions.

Should I bother encrypting the ssh private key?

It’s up to you. I’d say if you plan to leave your key on a server filesystem, you should do it. If you instead chose to store it in a secrets / creds store (which usually encrypt files and strings), then it’s less necessary. Of course, it also depends on how you access your key. If not sure, do it ! (though I agree using an ssh agent can be cumbersome in some cases).
Here is how I usually manage encrypted keys in Gitlab-CI pipelines:

    - mkdir -p -m=700 ~/.ssh
    - eval $(ssh-agent)
    - chmod 400 "${SSH_PRIVATE_KEY_GIT}"
    - echo "echo ${SSH_PASSPHRASE_GIT@Q}" > ~/.ssh/.ssh_askpass_git.sh
    - chmod ug+x ~/.ssh/.ssh_askpass_git.sh
    - SSH_ASKPASS_REQUIRE=force SSH_ASKPASS=~/.ssh/.ssh_askpass_git.sh ssh-add "${SSH_PRIVATE_KEY_GIT}"

SSH_PRIVATE_KEY_GIT and SSH_PASSPHRASE_GIT being CI vars on my projects.

Which is somewhat needlessly complex, I’ll give you that :confused:

3 Likes

Hi,

So to address the comment about installing Ansible with apt made here WinRM on APT Ansible installation - #2 by ptn I installed Ansible with pipx today. It appears to have installed ansible in my user folder through.

At least while I’m configuring this, I’m going to be working with Ansible in the terminal directly. The NUT Client appears to be running in a service user named nut though. I’m guessing this means that any scripts I have it call will be the equivalent of that logged in user at the terminal. Will it not have access to the ansible installation in my user and also the keys and inventory data in my folder?

I tried logging in as that user but even after setting a password for it, it says that the user is “unavailable.”

What should I do here? Am I making the wrong assumptions?

I’m guessing this means that any scripts I have it call will be the equivalent of that logged in user at the terminal.

I’m not quite sure I understand what you’re saying here. If you tell Ansible to connect as ‘nut’ user on your remote machine, then it will indeed open an ssh session with that user. By default, the user you’re running your playbook with will be the one used to login on remote nodes, except if you defined another user for this host in your ssh client or Ansible config. This user needs to exist on the remote host.

Now if you run tasks (or the whole play) using become, then Ansible will open a session using your current local user, as stated above, and then run modules using sudo as root (by default; these can be changed).

Will it not have access to the ansible installation in my user and also the keys and inventory data in my folder?

It depends where you installed Ansible and put your config files and keys. Your ssh client config and keys are probably owned by the user you created them with, but you can move them and change ownership if needed.
As for Ansible, if you installed it from PyPI (using pip / pipx / …) either with --user param or in a venv in your /home folder, then it would be usable to this user only, and you either have to:

  • Reinstall it in the /home folder of the user who will use it using the same method (recommended)
  • Reinstall it in a system directory
  • Change ownership / ACL of install folder and parent directories so another user can use these binaries, then potentially add this folder to your user $PATH

I tried logging in as that user but even after setting a password for it, it says that the user is “unavailable.”

Can you post the commands you’re using as well as the output ?
On remote machine, could you also verify if the user you’re trying to login with exists and have a shell defined: awk -F: '$1 == "<yourUser>" {print $NF}' /etc/passwd ?

I think there’s a misunderstanding. The Ansible playbook will be called from a script that is run by NUT which is monitoring the UPS. The NUT service is running under a user called nut, which has no home folder (or .ssh folder), password, or interactive login capability.

I installed it with pipx which appears to have put it in my personal home folder.

How can you do this with pipx?

image
:expressionless:

I think there’s a misunderstanding. The Ansible playbook will be called from a script that is run by NUT which is monitoring the UPS. The NUT service is running under a user called nut, which has no home folder (or .ssh folder), password, or interactive login capability.

Ok, then you need to ensure ‘nut’ user on your control node (the machine you’re running ansible from) have access to: Your ssh config and keys, Ansible config (ansible.cfg, inventories, vars, playbooks, …), and Ansible binaries.

You’d also have to ensure your user can open a session to your control node, or login with another user then run commands through sudo (not recommended as it just makes the process more complicated IMO).

Then depending on which account you’d like to connect to your remote nodes (the ones you want to shutdown IIRC) through Ansible, you also have to ensure this account exists on these machines and have the appropriate permission to execute desired tasks, either directly or using sudo through Ansible become directive.

If you think there’s still a misunderstanding, could you draw a quick schema showing machines, users accounts you’re login with, etc… ?

I installed it with pipx which appears to have put it in my personal home folder.

I’m not familiar with pipx specifically; I think it just wraps your installs in venvs under the hood, though I’m probably wrong here. But yeah, it’s best practices for python packages these days to be installed in an isolated way.
What I’d do: Remove packages you have installed for your other user and re-install them while logged in with your ‘nut’ user. You mention it doesn’t have a /home folder, so either create one, or install Ansible packages in a location you’re user can access.

How can you do this with pipx?

Not sure pipx is the way to go here. You can use regular pip (not using --user flag) or install Ansible from your distro repos.

I still think you should isolate your Ansible installation; here’s how to install Ansible using pip in a venv in your logged in user’s /home directory (adapt paths as needed):

sudo apt install python3-venv \
    && mkdir -p ~/.venv \
    && python3 -m venv ~/.venv/ansible \
    && ~/.venv/ansible/bin/python3 -m pip install --upgrade pip \
    && ~/.venv/ansible/bin/python3 -m pip install ansible \
    && [[ ":$PATH:" != *":$HOME/.venv/ansible/bin:"* ]] && export PATH="$PATH:$HOME/.venv/ansible/bin" # To be added to ~/.bashrc

Note you can also run Ansible in a container if you have a container runtime installed on this machine.

image

Yeah, looks like ‘nut’ user can’t open a shell on this machine. You can either change that, sudo / su to run commands as another user or connect with another user altogether.

I’m not sure what “your user” here refers to here.

In this setup I have
My computer
Could be my MacBook, my Windows Desktop, etc
Personal SSH key with encrypted PK

Control server
Runs NUT, Ansible, msmtp, Ubuntu Server
USB connection to UPS, monitored with NUT
nut service account
NUT runs under linux account called nut
nut is a service account and shouldn’t really be logged into
NUT runs a script when the battery is low
Script calls Ansible to shutdown UPS loads and send email with msmtp
Script gets called under nut account
login user account
my personal ssh key is added to authorized_keys
Ideally, all interaction with the server is done through this account
I’d like to be able to test how the ansible script above would work with reasonably predictability from this account for testing

loads
ESXi, Windows Server VMs, Linux VMs, Synology NAS, bare metal linux servers
local ansible user account
separate public key added to authorized_keys with unencrypted private key used by Ansible
Need to shutdown in order - VMs and bare metal servers, then hosts, then SAN/NAS, then UPS

Just curious, why? I think since I want to be able to use Ansible commands when logged in with my personal account for testing, maintenance, building, etc and the service account will need to use it too, it should be installed in /bin or /usr right?

nut is an account that was created when I installed NUT and is what the components run under. I assume it was created nologin just for slight security but changing it’s shell to something useful would not be too big of a deal if I add a password right?

It’s been a while since I ran NUT, but as I recall that account is created by installing those UPS tools. I’d be nervous about changing the assumptions of the package maintainers. It probably wouldn’t be a problem now, but there will be updates some day…

What if the script that NUT runs when the battery is low were suid to some id you control, one which you’ve vetted for running Ansible? I imagine the idea of suid scripts raises as many eyebrows as adding login capabilities to the nut account. But it’s another approach you may want to consider.

1 Like

I like this idea. It solves my pipx and ssh problems all at once. Is is not wise to just install ansible and configure all the ssh stuff on a single account that is also used for administraton or should I have my regular login account separate from the account that will have Ansible installed and all the ssh keys?

Also, will suid actually make it as if the script is being executed by the owning user that has just logged in? Or will the script have to set a working directory to the user’s home folder to have access to the inventories, playbooks, and ssh keys in there?

Sorry, @charlespick . My ancient reptilian brain remembers when people thought this was acceptable. Modern OSs don’t honor the suid bit on scripts any more, and for good reasons. I’m embarrassed to have brought it up.

There are better ways to get the effect you want, though. Below is a simple script I called id-a.sh:

#!/usr/bin/bash

cmd="$(realpath "$0")"
ansibleid="ansible-unleashed"

printf "PID $$ starting $cmd as '%s' in '%s'\n" "$(id -un)" "$(/bin/pwd)"
if [ "$(id -un)" != "$ansibleid" ] ; then
  sudo runuser -l "$ansibleid" "$cmd"
  rc=$?
  printf "PID $$ ending run of $cmd as '%s' in '%s'\n" "$(id -un)" "$(/bin/pwd)"
  exit $rc
fi

printf "PID $$ running $cmd as '%s' in '%s'\n" "$(id -un)" "$(/bin/pwd)"
# Put your ansible-playbook command here.
# And comment out all the other stuff; it's just to prove
# the technique works anyway.
printf "whoami: %s\n" "$(whoami)"
printf "id -a:  %s\n" "$(id -a)"
touch /tmp/id-a.test.$$
printf "touch test: %s\n" "$(ls -l /tmp/id-a.test.$$)"
rm -f /tmp/id-a.test.$$

And here’s the output of a run:

PID 1234130 starting /home/utoddl/ansible/id-a.sh as 'utoddl' in '/home/utoddl/ansible'
PID 1234139 starting /home/utoddl/ansible/id-a.sh as 'ansible-unleashed' in '/home/ansible-unleashed'
PID 1234139 running /home/utoddl/ansible/id-a.sh as 'ansible-unleashed' in '/home/ansible-unleashed'
whoami: ansible-unleashed
id -a:  uid=123454321(ansible-unleashed) gid=123454321(ansible-unleashed) groups=123454321(ansible-unleashed) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
touch test: -rw-r--r--. 1 ansible-unleashed ansible-unleashed 0 Oct 17 21:54 /tmp/id-a.test.1234139
PID 1234130 ending run of /home/utoddl/ansible/id-a.sh as 'utoddl' in '/home/utoddl/ansible'

This script checks to see if it’s running as the user ansible-unleashed, and if it isn’t, it re-runs itself with sudo runuser -l ansible-unleashed /home/utoddl/ansible/id-a.sh which runs the command under the ansible-unleashed user with a login shell.

Your problem then is to setup password-less sudo for the nut user for this one command, which is conveniently described here — if you squint a little and substitute a few strings.

This may seem like a lot of fluff to replace a single suid bit, but it avoids (1) some bad practices that (2) no longer work anyway and (3) can be managed without surprises on modern systems.

1 Like

No worries! I actually found this out after a lot of googling last night. I’m considering using Ansible Rulebooks and a watchdog source to monitor a file and using a script to do something to the file. It seems like regular Ansible really wants to be run in an environment similar to an interactive terminal and that nut account is not that. Any workarounds to get the two to play together is going to be an uphill battle.

Another method: We have a bunch of hosts which hit our AWX instance’s API on a once-per-day cron – spread out throughout the day of course. That cron job looks like this:

#!/bin/bash
###################################################################
# Canonical source: linux-admins/roles/admins_bits/files/towerrun #
###################################################################
. /etc/sysconfig/towerrun.env
curl --data "host_config_key=${host_config_key}" \
     https://${awx_vip}:443/api/v2/job_templates/${towerrun_template_id}/callback/ \
     -H "X-Forwarded-For: $(hostname)" > /dev/null 2>&1

Maybe this gives you some other ideas. Good luck.

1 Like

I’m not sure what “your user” here refers to here.

The user you’re connecting with on your control node, so ‘nut’ if I got you right. This sentence is a continuation of the previous one, stating this user also need to be able to open a shell through ssh on your control node as well as having access to all the configuration it needs to run your playbook.

login user account
Ideally, all interaction with the server is done through this account

Ok, so I suggest you don’t manage any ssh or ansible config with your ‘nut’ account (not even changing its shell so you can login or run others remote commands with it), and do everything with this account (connecting and running your playbook), adding a task to your playbook you’ll delegate to nut account, which would run the low battery script and send the email.
You mention an ansible account in next paragraph, so I guess it would be this one.

Just curious, why?

Security best practice, so only specific accounts can run binaries on your machine. Also avoiding conflicts when you update your dependencies as programs running in venvs packs their own ones.
Not a big deal though, especially if you don’t install lots of python packages.

I think since I want to be able to use Ansible commands when logged in with my personal account for testing, maintenance, building, etc and the service account will need to use it too, it should be installed in /bin or /usr right?

Yeah, or you could install your tools in a venv accessible by multiple users. It’s not like it would change much, so go for the simplest route.
Note that for testing, you could also run your playbooks using sudo, su, or any similar tools as your nut user.

I assume it was created nologin just for slight security but changing it’s shell to something useful would not be too big of a deal if I add a password right?

Adding a password won’t be enough for you to login with this user, you’d also need to change its shell, using chsh or editing /etc/passwd directly.
It’s probably not big of a deal, though you’d have to ensure no system would change that setting back. As I understand, NUT created this user once fire and forget style, and doesn’t do anything else on this account, so assuming you don’t manage it neither with a mgmt config tool, you should be find doing this change.

Now as I was suggesting up above, you could just use another account to run your stuff and simply delegate specific tasks to nut account, so no need to enable it to login on this machine as well.

Things are getting a little mixed up :smile: , so don’t hesitate to ask for clarification if needed.

I installed ansible, ansible-runner, ansible-rulebook with the following commands

sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible-runner 
sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible-rulebook
sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible --include-deps

But when I run ansible-rulebook --rulebook rulebook.yml I get the error:

Traceback (most recent call last):
  File "/usr/local/bin/ansible-rulebook", line 5, in <module>
    from ansible_rulebook.cli import main
  File "/opt/pipx/venvs/ansible-rulebook/lib/python3.10/site-packages/ansible_rulebook/cli.py", line 35, in <module>
    from ansible_rulebook import app  # noqa: E402
  File "/opt/pipx/venvs/ansible-rulebook/lib/python3.10/site-packages/ansible_rulebook/app.py", line 32, in <module>
    from ansible_rulebook.engine import run_rulesets, start_source
  File "/opt/pipx/venvs/ansible-rulebook/lib/python3.10/site-packages/ansible_rulebook/engine.py", line 25, in <module>
    from watchdog.events import FileSystemEventHandler
ModuleNotFoundError: No module named 'watchdog'

Is this a fault of using pipx and venvs? I notice not much of the documentation mentions using either even though people recommend them in forums all the time.
Not sure what to do.

Hi Charles,

ModuleNotFoundError: No module named ‘watchdog’

To fix this, just install missing ‘watchdog’ module in your ansible-rulebook venv.

Now I’m not sure why it hasn’t been installed as dependency; install documentation doesn’t mention it, but it is listed as so in multiple files used for others build methods: https://github.com/ansible/ansible-rulebook/blob/main/Dockerfile#L17, https://github.com/ansible/ansible-rulebook/blob/5095c9338d8439f9bb31a336d46bf5d26d8b6886/minimal-decision-environment.yml#L16, https://github.com/ansible/ansible-rulebook/blob/5095c9338d8439f9bb31a336d46bf5d26d8b6886/requirements_test.txt#L1

I don’t know enough about pip packages to give you a more precise answer, but you should be able to check dependencies for an installed package running pip show <packageName> (to run in the venv your package has been installed).

1 Like

You need to install all the packages into the same pipx venv:

pipx install --include-deps ansible
pipx inject --include-apps --include-deps ansible ansible-runner ansible-rulebook
2 Likes

Thanks guys! I guess I just didn’t really consider that it wouldn’t be installed by pip as a dependency. Looks like it’s working now tho.

2 Likes