Giter VIP home page Giter VIP logo

the-bastion-ansible-wrapper's Introduction

Using Ansible SSH Connection through The Bastion

The three scripts in this directory are a wrapper around Ansible native SSH connection, so that The Bastion can be transparently used along with Ansible. You have to set some os SSH Ansible variables as defined in https://docs.ansible.com/ansible/latest/plugins/connection/ssh.html in addition with BASTION_USER, BASTION_PORT and BASTION_HOST. It can also rely on ansible-inventory to identify bastion_user, bastion_host, bastion_port. ansible-inventory takes precedences over environment variables as this will allow to use different bastion for different hosts.

Simple usage with environment variables

Ensure the scripts are executable (chmod +x)

export BASTION_USER="bastion_user"
export BASTION_HOST="bastion.example.org"
export BASTION_PORT=22
export ANSIBLE_PIPELINING=1
export ANSIBLE_SCP_IF_SSH="True"
export ANSIBLE_PRIVATE_KEY_FILE="${HOME}/.ssh/id_rsa"
export ANSIBLE_SSH_EXECUTABLE="CHANGE_THIS_PATH_TO_THE_PROPER_ONE/sshwrapper.py"
export ANSIBLE_SCP_EXECUTABLE="CHANGE_THIS_PATH_TO_THE_PROPER_ONE/scpbastion.sh"

ansible all -i hosts -m raw -a uptime

ansible all -i hosts -m ping

Leveraging Ansible inventory

ansible-inventory provides access to host's variables. This plugin takes advantage of this to look for bastion_*.

In the following example all hosts will use the same your-bastion-user. The hosts in zone_secure will reach the bastion your-supersecure-bastion on port 222 the others hosts will use your-bastion on port 22.

$ grep -ri bastion group_vars/
group_vars/all.yml:bastion_user: <your-bastion-user>
group_vars/all.yml:bastion_host: <your-bastion>
group_vars/all.yml:bastion_port: 22
group_vars/zone_secure.yml:bastion_port: 222
group_vars/zone_secure.yml:bastion_host: <your-supersecure-bastion>

For more information have a look at the official documentation.

Ansible inventory cache

Because ansible-inventory command can be slow, the Ansible inventory results can be saved to a file to speed up multiple calls with the following environment variables:

  • BASTION_ANSIBLE_INV_CACHE_FILE: path to the cache file on the filesystem
  • BASTION_ANSIBLE_INV_CACHE_TIMEOUT: number of seconds before refreshing the cache

Note: the cache file will not be removed by the wrapper at the end of the run, which means that multiple consecutive runs might use it, as long as it's fresh enough (the expiration of BASTION_ANSIBLE_INV_CACHE_TIMEOUT will force a refresh).

If not set, the cache will not be used, even if cache is set at the Ansible level.

Using env vars from a playbook

In some cases, like the usage of multiple bastions for a single ansible controller and multiple inventory sources, it may be useful to set the vars in the environment configuration from the playbook.

It can also be combined with the group_vars.

Example:

---
- hosts: all
  gather_facts: false
  environment:
    BASTION_USER: "{{ bastion_user }}"
    BASTION_HOST: "{{ bastion_host }}"
    BASTION_PORT: "{{ bastion_port }}"
  tasks:
  ...

here, each host may have its bastion_X vars defined in group_vars and host_vars.

If environement vars are not defined, or if the module does not send them, then the sshwrapper is doing a lookup on the ansible-inventory to fetch the bastion_X vars.

Using vars from a config file

For some use cases (AWX in a non containerised environment for instance), the environment is overridden by the job, and there is no fixed inventory source path.

So we may not get the vars from the environment nor the inventory.

In this case, we may use a configuration file to provide the BASTION vars.

Example:

cat /etc/ovh/bastion/config.yml

---
bastion_host: "my_great_bastion"
bastion_port: 22
bastion_user: "my_bastion_user"

The configuration file is read after checking the environment variables sent in the ssh command line, and will only set them if not defined.

The location of the configuration file can be set with BASTION_CONFIG_FILE environment variable (defaults to /etc/ovh/bastion/config.yml).

Configuration priority

Source of variables are read in the following order:

  • Ansible playbook environment
  • configuration file
  • Ansible inventory
  • operating system environment variables

Using multiple inventories sources

The wrapper is going to lookup the ansible inventory to look for the host and its vars.

You may define multiple inventories sources in an ENV var. Example:

export BASTION_ANSIBLE_INV_OPTIONS='-i my_first_inventory_source -i my_second_inventory_source'

Using the bastion wrapper with AWX

When using AWX, the inventory is available as a file in the AWX Execution Environment. It is then easy and much faster to get the appropriate host from the IP sent by Ansible to the bastion wrapper.

When AWX usage is detected, the bastion wrapper is going to:

  • lookup in the inventory file for the appropriate host
  • lookup for the bastion vars in the host_vars
  • if not found, run an inventory lookup on the host to get the group_vars too (and execute eventual vars plugins)

The AWX usage is detected by looking for the inventory file, the default path being "/runner/inventory/hosts" The path may be changed y setting an "AWX_RUN_DIR" environment variable on the AWX worker. Ex on a AWX k8s instance group:

      env:
      - name: "AWX_RUN_DIR"
        value: "/my_folder/my_sub_folder"

The inventory file will be looked up at "/my_folder/my_sub_folder/inventory/hosts"

Connection via SSH

The wrapper can be configured using ansible.cfg file as follow:

[ssh_connection]
pipelining = True
ssh_executable = ./extra/bastion/sshwrapper.py

Or by using the ANSIBLE_SSH_PIPELINING and ANSIBLE_SSH_EXECUTABLE environment variables.

File transfer using SFTP

By default, Ansible uses SFTP to copy files. The executable should be defined as follow in the ansible.cfg file:

[ssh_connection]
transfer_method = sftp
sftp_executable = ./extra/bastion/sftpbastion.sh

Or by using the ANSIBLE_SFTP_EXECUTABLE environment variable.

File transfer using SCP (deprecated)

The SCP protocol is still allowed but will soon deprecated by OpenSSH. You should consider using SFTP instead. If you still want to use the SCP protocol, you can define the method and executable as follow:

File ansible.cfg:

[ssh_connection]
transfer_method = scp
scp_if_ssh = True       # Ansible < 2.17
scp_extra_args = -O     # OpenSSH >= 9.0
scp_executable = ./extra/bastion/scpbastion.sh

Or by using the following environment variables:

  • ANSIBLE_SCP_IF_SSH
  • ANSIBLE_SSH_TRANSFER_METHOD
  • ANSIBLE_SCP_EXTRA_ARGS
  • ANSIBLE_SCP_EXECUTABLE

Configuration example

File ansible.cfg:

[ssh_connection]
pipelining = True
ssh_executable = ./extra/bastion/sshwrapper.py
sftp_executable = ./extra/bastion/sftpbastion.sh

Integration via submodule

You can include this repository as a submodule in your playbook repository

git submodule add https://github.com/ovh/the-bastion-ansible-wrapper.git extra/bastion

Requirements

This has been tested with

  • Ansible 2.9.6
  • Python 3.7.3
  • SSH OpenSSH_7.9p1 Debian-10+deb10u2, OpenSSL 1.1.1d

Debug

If this doesn't seem to work, run your ansible with -vvvv, you'll see whether it actually attempts to use the wrappers or not.

Lint

Just use pre-commit.

TLDR:

  • pip install --user pre-commit
  • pre-commit install
  • git commit

Related

  • The Bastion - Authentication, authorization, traceability and auditability for SSH accesses.

License

Copyright OVH SAS

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

the-bastion-ansible-wrapper's People

Contributors

b-marine avatar damcav35 avatar jouir avatar jriouovh avatar koleo avatar speed47 avatar wilfriedroset avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

the-bastion-ansible-wrapper's Issues

SSH Local Forward

Hello,

My goal is to access an http server listening only to localhost through the bastion.
kind of ssh -L 9090:localhost:9090 server42

I tried to :
ssh -L 9090:localhost:9090 -t admin@bastion -- someuser@server42
But without success.

I've found in (bastion) /etc/ssh/sshd_config there is
AllowTcpForwarding no

Setting to yes didn't help.

Is it a good idea to change th bastion sshd_config ? Is there a better way ?

PS: the http server is https://cockpit-project.org

AWX: problem getting vars without unsing ansible_host

Hi,

I'm using AWX with the bastion_vars in group variables and it's not working.

After searching in the code, I found the problem.
In the "awx_get_vars" function, the "ansible_host" variable is used to identify the host and I'm not using it.

My inventory looks like that

[servers]
serv01.example.net
serv02.example.net

In the lib.py file, line 224, I changed:

# this should not happen
if not host:
    return {}

with this:

# maybe ansible_host is not define
if not host:
    for k, v in inv.get("_meta", {}).get("hostvars", {}).items():
        if k == host_ip:
            host = k
            host_vars = v
            break

    # this should not happen
    if not host_vars:
        return {}

With this change, it's working.
I don't think this will break other things but I let more experienced people check that.

Thank you

Race condition on the inventory cache file

Hello,

Since the inventory cache feature, we have notice the following error:

Traceback (most recent call last):
  File "./extra/bastion/sshwrapper.py", line 96, in <module>
    main()
  File "./extra/bastion/sshwrapper.py", line 44, in main
    hostvar = get_hostvars(host)  # dict
  File "/tmp/bwrap_247825_so1sw9v8/awx_247825_ibg2laaa/project/extra/bastion/lib.py", line 113, in get_hostvars
    inventory = get_inventory()
  File "/tmp/bwrap_247825_so1sw9v8/awx_247825_ibg2laaa/project/extra/bastion/lib.py", line 45, in get_inventory
    cache_timeout=int(os.environ.get("BASTION_ANSIBLE_INV_CACHE_TIMEOUT", 60)),
  File "/tmp/bwrap_247825_so1sw9v8/awx_247825_ibg2laaa/project/extra/bastion/lib.py", line 88, in get_inventory_from_cache
    cache = json.load(fd)
  File "/usr/lib64/python3.6/json/__init__.py", line 299, in load
    parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/usr/lib64/python3.6/json/__init__.py", line 354, in loads
    return _default_decoder.decode(s)
  File "/usr/lib64/python3.6/json/decoder.py", line 339, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/lib64/python3.6/json/decoder.py", line 355, in raw_decode
    obj, end = self.scan_once(s, idx)
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 1614071 (char 1614070)

When multiple jobs are running at the same time, we suspect a race condition when writing and reading on the inventory cache file.

Discovery python Interpreter failed

Hi, I have a problem when using the wrapper :

ansible version : 2.11.3
python version 3.9.5

I have installed it with pip

It seems to fail when trying to discover the python interpreter :

When using ansible without the bastion :

ansible all -i host_ip, -u root -m ping
host_ip | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

When using the bastion :

export BASTION_USER="bastion_user"
export BASTION_HOST="bastion_ip"
export BASTION_PORT=22
export ANSIBLE_PIPELINING=1
export ANSIBLE_SCP_IF_SSH="True"
export ANSIBLE_PRIVATE_KEY_FILE="${HOME}/.ssh/id_ed25519"
export ANSIBLE_SSH_EXECUTABLE="${HOME}/Ansible/bastion/sshwrapper.py"
export ANSIBLE_SCP_EXECUTABLE="${HOME}/Ansible/bastion/scpbastion.sh"

ansible all -i host_ip, -u root -m ping

host_ip | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "module_stderr": "/bin/sh: 1: /usr/bin/python: not found\n",
    "module_stdout": "",
    "msg": "The module failed to execute correctly, you probably need to set the interpreter.\nSee stdout/stderr for the exact error",
    "rc": 127
}

But when I try to connect with modifying the python interpreter of ansible :

ansible.cfg


interpreter_python=/usr/bin/python3

ansible all -i host_ip, -u root -m ping
host_ip | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

This is a problem because we have hosts that have not python 3 installed but only python 2 and ansible works with that.

I think that this is the same problem has : #2

Accept Fingerprints

Hi @speed47
Is there a function or variable that accept on the first connection the fingerprint of the remote servers?

/Daniel

Getting warnings about scp and sftp when running with the wrapper

Hi,
I get the following warnings for each task when running an ansible provisioning with the wrapper:

[WARNING]: sftp transfer mechanism failed on [vm1.localhost.dev]. Use ANSIBLE_DEBUG=1 to see detailed information
[WARNING]: scp transfer mechanism failed on [vm1.localhost.dev]. Use ANSIBLE_DEBUG=1 to see detailed information

I don't know if it will impact my deployments, it looks like on my test vm, it run just fine, but still don't like seeing a warning like this.
At first I thought it was because of #20, but even after modifying it myself, it didn't change the output.

ssh connection issue

Hi,

i've just setup an Ansible. We've got a bastion OVH.

i've created a group_var like that:

group_vars/all/all.yml

ansible_python_interpreter: /usr/bin/python3

bastion_user: [myuser]
bastion_host: [my_bastion address]
bastion_port: 22
ansible_private_key_file: "/home/[myuser]/.ssh/[privatekey]"

ansible_ssh_transfer_method: scp
ansible_ssh_pipelining: 1
ansible_ssh_executable: /[path]/sshwrapper.py

ansible_scp_if_ssh: True
ansible_scp_executable: /[path]/scpbastion.sh

my domain and path are correct

When i did the command " ansible -vvvv [MyGroup] -i 00_inventory.yml -m ping", that did not worked.

I've had this message :

ansible [core 2.12.9]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/[myuser]/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
ansible collection location = /home/[myuser]/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110]
jinja version = 2.11.3
libyaml = True
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
host_list declined parsing /[path]/00_inventory.yml as it did not pass its verify_file() method
script declined parsing /[Path]/00_inventory.yml as it did not pass its verify_file() method
Parsed /[Path]/00_inventory.yml inventory source with yaml plugin
Loading callback plugin minimal of type stdout, v2.0 from /usr/lib/python3/dist-packages/ansible/plugins/callback/minimal.py
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
META: ran handlers
Using module file /usr/lib/python3/dist-packages/ansible/modules/ping.py
Pipelining is enabled.
ESTABLISH SSH CONNECTION FOR USER: None
SSH: EXEC /etc/ansible/extra/bastion/sshwrapper.py -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o 'ControlPath="/[Path]/cp/801a4a997c"' serveur.domain.com '/bin/sh -c '"'"'/usr/bin/python3 && sleep 0'"'"''
serveur.domain.com | UNREACHABLE! => {
"changed": false,
"msg": "Data could not be sent to remote host "serveur.domain.com". Make sure this host can be reached over ssh: OpenSSH_8.4p1 Debian-5+deb11u1, OpenSSL 1.1.1n 15 Mar 2022\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files\r\ndebug1: /etc/ssh/ssh_config line 21: Applying options for *\r\ndebug3: expanded UserKnownHostsFile '/.ssh/known_hosts' -> '/[Path]/.ssh/known_hosts'\r\ndebug3: expanded UserKnownHostsFile '/.ssh/known_hosts2' -> '/[Path]/.ssh/known_hosts2'\r\ndebug1: auto-mux: Trying existing master\r\ndebug1: Control socket "/[Path]/.ansible/cp/801a4a997c" does not exist\r\ndebug2: resolving "none" port 22\r\nssh: Could not resolve hostname none: Name or service not known\r\n",
"unreachable": true

[Path] et serveur.domain.com are corrects.

I've looked for to solve but i haven't found anything.

Thanks for your help.

Problem connection via a host.yml file

Hi,

This is my problem :

I have this host file :

all:
  children:
    bastion:
      children:
        client01:
          hosts:
            srv01vm: 
              ansible_host: ip_srv
            srv03vm:
              ansible_host: ip_srv
            srv04vm: 
              ansible_host: ip_srv
            srv05vm: 
              ansible_host: ip_srv
            srv06vm: 
              ansible_host: ip_srv
            srv07vm: 
              ansible_host: ip_srv
            srv08vm: 
              ansible_host: ip_srv
            srv09vm: 
              ansible_host: ip_srv
            srv13vm: 
              ansible_host: ip_srv
          vars:
            ansible_user: root
            ansible_port: 22
            bastion_user: bastion_user
            bastion_host: bastion_ip
            bastion_port: 22
      vars:
        ansible_pipelining: True
        ansible_scp_if_ssh: True
        ansible_private_key_file: "/home/nicolas/.ssh/id_ed25519"
        ansible_ssh_executable: "/home/nicolas/Ansible/bastion/sshwrapper.py"
        ansible_scp_executable: "/home/nicolas/Ansible/bastion/scpbastion.sh"
        ansible_ssh_transfer_method: scp
        ansible_python_interpreter: /usr/bin/python3
        ansible_host_key_checking: no

I have also put the ansible_* vars in my .ansible.cfg this does not work.

The problem is that when I'm trying to ping via the host file it gives me this error :

srv01vm | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: OpenSSH_8.4p1 Ubuntu-5ubuntu1.1, OpenSSL 1.1.1j  16 Feb 2021\r\ndebug1: Reading configuration data /home/nicolas/.ssh/config\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files\r\ndebug1: /etc/ssh/ssh_config line 21: Applying options for *\r\ndebug3: expanded UserKnownHostsFile '~/.ssh/known_hosts' -> '/home/nicolas/.ssh/known_hosts'\r\ndebug3: expanded UserKnownHostsFile '~/.ssh/known_hosts2' -> '/home/nicolas/.ssh/known_hosts2'\r\ndebug1: auto-mux: Trying existing master\r\ndebug1: Control socket \"/home/nicolas/.ansible/cp/3c7791cd0b\" does not exist\r\ndebug2: resolving \"none\" port 22\r\nssh: Could not resolve hostname none: Name or service not known",
    "unreachable": true
}

Via the command :

ansible all -i host.yml -m ping -vvvvvvvv

But the ansible-inventory command print me my host.yml file with the right variables.

Thanks

Change default Interpreter

Today, i found time to test the wrapper in our development environment. After some cups of coffee and some ice, runs ansible in combination with The Bastion. Thanks again for the plugin!

I noticed zwei little things:

  • In both python scripts you use pyhton als default interpreter. In the system requierments did you write Python 3. I had to create a symlink for the wrapper to work. Wouldn't it be better to change the interpreter to pyhton3?
  • In the section "Configuration via ansible.cfg" in the README.md you use relative paths. Perhaps you can add a hint, that this only work if ansible (configuration) user based and not system based.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.