Link to home
Avatar of Leo
LeoFlag for United States of America

asked on

Ansible | How to add a domain user to sudoers (CentOS / Ubuntu)

We are trying to get a domain user added to sudoers (CentOS / Ubuntu) using an ansible playbook. Could someone please help with that? 


I am trying to edit the sudoers file with somehting like this:


- name: "run command"
  become: true
  gather_facts: no
  tasks:
    - name: add user to sudo group
      shell: usermod -aG sudo <username>
      args:
        executable: /bin/bash

Open in new window

but doesn't work. 


Would it be possible to modify the playbook to add the following to /etc/sudoers EOF 


mydomain\\username ALL=(ALL) NOPASSWD: ALL


Ideally the username will come from a variable call reqUser so something like this (not sure if I need to get " " around.  


mydomain\\{{ reqUser }}  ALL=(ALL) NOPASSWD: ALL


Thank you. 



Avatar of simon3270
simon3270
Flag of United Kingdom of Great Britain and Northern Ireland image

"doesn't work" - needs explanation!

If it doesn't allow the use to run sudo commands (it tells you that it is forbidden for that user, and the action has been reported), then the "sudo" group might not be the right one. It might, for example, be "wheel".

If it does allow for sudo, but requires a password, then the NOPASSWD entry is the right one. In this case, you have 2 options:

    Remove the password requirement for all users. You need to find the line which allows users in a specific group (line starts "%group_name") to run sudo commands, and add NOPASSWD: (the colon is important) as you already have in your example.

      If you just want it for one user, you could add it to the /etc/sudoers file, but that is quite dangerous. I would instead create a file in /etc/sudoers.d, whci will automatically be included. I would still use lineinfile, because that supports the "validate" option to check syntax, and that is very important for sudo. It happens automatically if you run the "visudo" command to edit sudoers interactively - it checks the syntax when you exit the editor, and only applies it f the check succeeds. I would also remove the user form the 'sudo' or 'wheel' group - sudo stops on the first line which matches the request, so if they are in the relevant sudo group it will use the generic entry, and won't reach the user-specific one.

      To add an extra file, have something like:
      - name: "Allow {{ reqUser }} to sudo, and validate the sudoers file before saving"
        ansible.builtin.lineinfile:
          path: "/etc/sudoers.d/{{ reqUser }}.sudo"
          state: present
          regexp: '^mydomain\\{{ reqUser }}'
          line: 'mydomain\\{{ reqUser }}  ALL=(ALL) NOPASSWD: ALL'
          validate: /usr/sbin/visudo -cf %s
      

      Open in new window

      Avatar of Leo

      ASKER

      Thanks Simon!

      So I have CentOS and Ubuntu, I am testing first with CentOS, I have tried using the following in CentOS:

      usermod -aG wheel domain\\username

      But that's not allowing the domain\\username to sudo su.

      The only thing that seems to work is when I manually add the following to sudoers

      domainname\\username  ALL=(ALL) NOPASSWD: ALL

      If I could get the above to work is definitely better, I prefer not to modify sudoers.

      I also tested the following:

      --- 
      - hosts: all
        tasks:
         - name: "Allow {{ reqUser }} to sudo, and validate the sudoers file before saving"
           ansible.builtin.lineinfile:
             path: "/etc/sudoers.d/{{ reqUser }}.sudo"
             state: present
             regexp: '^dom\\{{ reqUser }}'
             line: 'dom\\{{ reqUser }}  ALL=(ALL) NOPASSWD: ALL'
             validate: /usr/sbin/visudo -cf %s
      
      
      

      Open in new window


      Runs but getting the following

      centostest                : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0


      TASK [Allow {{ reqUser }} to sudo, and validate the sudoers file before saving] *********************************************
      fatal: [centostest01]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'reqUser' is undefined\n\nThe error appears to be in '/home/ansible/git-setup.yml': line 4, column 6, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n   - name: \"Allow {{ reqUser }} to sudo, and validate the sudoers file before saving\"\n     ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes. Always quote template expression brackets when they\nstart a value. For instance:\n\n    with_items:\n      - {{ foo }}\n\nShould be written as:\n\n    with_items:\n      - \"{{ foo }}\"\n"}
      fatal: [centostest2]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'reqUser' is undefined\n\nThe error appears to be in '/home/ansible/git-setup.yml': line 4, column 6, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n  tasks:\n   - name: \"Allow {{ reqUser }} to sudo, and validate the sudoers file before saving\"\n     ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes. Always quote template expression brackets when they\nstart a value. For instance:\n\n    with_items:\n      - {{ foo }}\n\nShould be written as:\n\n    with_items:\n      - \"{{ foo }}\"\n"}

      Is this the same as the problem we had in the last playbook? Didn't we add a "set_fact" to set reqUser?
      Avatar of Leo

      ASKER

      Looks like I was updating my answer at the same time! :)

      Yes, that's true, let me re-enter it. However, I agree with probably trying to do this without having to modify sudoers.

      I can test any other safer method that can be deployed via Ansible.

      Thanks! 
      Modifying sudoers is fine, as long as you are careful! Adding a file to /etc/sudoers.d is definitely safer, because you won't mess up the main /etc/sudoers file, but if you are sure that know what the original file looks like, you can set up an Ansible change that keeps a valid sudoers file.

      I do something similar on my home network but using Puppet (that was the system I was using at work when I set it up). I may one day rewrite the system in Ansible, but perhaps keeping Puppet going is good for my CV :-)

      If there aren't many users to add to sudo, I think this /etc/sudoers.d solution is probably the best. The alternative would be to do what you originally did to add the user to the sudoers file.. On at least one distro I use (Ubuntu?), there is both a "NOPASSWD entry, and an entry without NOPASSWD for the same group, with one of them commented out. The fix then would be to remove the comment marker on one line, and add it to the other.

      One other thing - have you tried making the changes manually first? Many times I have spent a long time getting automation exactly as I thought it should be, only to find that the commands I was now running didn't actually do what I thought they did!
      Avatar of noci
      noci

      The best way is to have a group access defined in sudoers  and add users to that group as needed.
      This is done on most systems. Only the group users should be added to can vary....
      some use wheel group (rhel derived), others user sudo (debian derived)  group or even other groups (less common).

      So adding a user to the sudo group on a Centos system will not help a lot.
      Alternatively you can use FreeIPA  to run a central server where you can define group members.



      Avatar of Leo

      ASKER

      Simon,

      Looks like there is an ansible module to modify sudoers.d.

      https://galaxy.ansible.com/jonellis/sudoers#:~:text=This%20is%20a%20basic%20Ansible,seem%20to%20show%20any%20content.

      Any thoughts on how I could use this module with the user variable I have used before in something similar to the code that you've provided above? basically I just want to add the user in the variable to sudoers.d with the domain like we've discussed above.

      This one also sounds like a good option, but I don't know how to use the user reqUser within the code.

      https://unix.stackexchange.com/questions/625470/give-sudoers-permission-to-the-user-form-ansible/629419#629419

      Any help would be welcome :)  

      thank you!
      To use a variable, just use it wherever the example code has the fixed text that the variable is replacing, and add double quotes round the entire entry, so
      - sudoers:
          name: "allow-{{ reqUser}}" 
          user: "{{ reqUser }}" 
          command: ALL
      

      Open in new window

      The "name" field has to be unique, as it is the name of the file in /etc/sudoers.d.
      Avatar of Leo

      ASKER

      Thanks Simon,

      As suggested I've manually tested the configuration that works for the env so now I know they way to go.

      I've manually created 2 files (/etc/sudoers.d/) one named "users" the other one "administrators"

      [root@centos sudoers.d]# ls
      users  administrators

      "users" has the following:

      # User rules for centos
      centos ALL=(ALL) NOPASSWD:ALL
      # User rules for myuser (this will need to come from the variable requested by {{ reqUser}} )
      myuser  ALL=(ALL) NOPASSWD:ALL

      "administrator"
      %CDOM\\Domain^Admins ALL=(ALL:ALL) ALL
      %DDOM\\Domain^Admins ALL=(ALL:ALL) ALL

      --------
      So I've manually tested this config and works well, so now I know for sure this is the way to go...

      I am wondering if you could help creating the playbook to mimic the the following steps:
      -------------------------------------------
      create "users" in /etc/sudoers.d/
      with the following content
      # User rules for centos
      centos ALL=(ALL) NOPASSWD:ALL
      # User rules for myuser (this will need to come from the variable requested by {{ reqUser}} )
      jsmith  ALL=(ALL) NOPASSWD:ALL

      create "administrators" in /etc/sudoers.d/ with the following content
      %CDOM\\Domain^Admins ALL=(ALL:ALL) ALL
      %DDOM\\Domain^Admins ALL=(ALL:ALL) ALL
      ------------------------------------------

      The above should do it, thanks so much!

      Avatar of Leo

      ASKER

      I've also been testing this without the variable (with a static user) and getting the following error when running the playbook. This pretty close to what I am trying to get done, just missing the "administrator" with the 2 domain names. Maybe it will work with everything in a single file including the 2 domain names as well. 

      --- 
      - hosts: all
        tasks:
         - name: "Allow {{ reqUser }} to sudo, and validate the sudoers file before saving"
           ansible.builtin.lineinfile:
             path: "/etc/sudoers.d/usera.sudo"
             state: present
             regexp: '^cdom\\usera'
             line: 'dom\\usera  ALL=(ALL) NOPASSWD: ALL'
             validate: /usr/sbin/visudo -cf %s
      

      Open in new window


      This is error when executing the playbook: 

      TASK [Allow {{ reqUser }} to sudo, and validate the sudoers file before saving] *********************************************************
      fatal: [centostest2]: FAILED! => {"changed": false, "msg": "Destination /etc/sudoers.d/usera.sudo does not exist !", "rc": 257}
      fatal: [centostest01]: FAILED! => {"changed": false, "msg": "Destination /etc/sudoers.d/usera.sudo does not exist !", "rc": 257}

      If this one works, then I could get into using the variable and hopefully that will do it.

      Thank you!
      Avatar of Leo

      ASKER

      Update:

      I've added the following to the code above:

      mode: 0440
      create: yes
      
      
      

      Open in new window


      *
      An exception occurred during task execution. To see the full traceback, use -vvv. The error was: OSError: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpaLLmj6usera.sudo'
      fatal: [centostest2]: FAILED! => {"changed": false, "msg": "The destination directory (/etc/sudoers.d) is not writable by the current user. Error was: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpaLLmj6llara.sudo'"}
      An exception occurred during task execution. To see the full traceback, use -vvv. The error was: OSError: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpTzNuIIusera.sudo'
      fatal: [centostest01]: FAILED! => {"changed": false, "msg": "The destination directory (/etc/sudoers.d) is not writable by the current user. Error was: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpTzNuIIllara.sudo'"}

      Not sure why permissions now... I am executing from the controller directly to a workstation machine. Every other playbook seems to execute, like creating a regular folder, etc. 




      The "permission denied" is odd. I think from previous playbooks that you are running commands as root. Have you changed your /etc/ansible/ansible.cfg to remove a become or become_user line?
      Avatar of Leo

      ASKER

      no changes on the ansible.cfg. Actually I've created an ansible user and use key authentication with the clients so user/pass is required. But other playbooks work well, the above isn't. Any suggestions on how to debug this?
      You could check that it is running as root by using  Ansible to create a file in /etc, and seeing which user owns the file.
      Have you maybe taken ansible out of the sudoers file, or changed the entry which ansible uses? It assumes that the ansible user can sudo to root without password
      Avatar of Leo

      ASKER

      Hi,

      So I've reviewed the ansible.cfg and this is the output for "become"

      [privilege_escalation]
      #become=True
      #become_method=sudo
      #become_user=root
      #become_ask_pass=False

      I didn't know I had modify any of this, that being said, can you please let me know what needs to be changed and the reason why?

      Also please note that I am able to SSH to the client/workstation/target using the "ansible" user and run sudo su without being prompted for a password. Using the "ansible" user in the workstation/target I can't create a file (test) in /etc/

      "test"
      "test" E212: Can't open file for writing
      Press ENTER or type command to continue

      Finally "ansible" is part of the sudoers on the "workstation/clients" --- ansible  ALL=(ALL)   NOPASSWD: ALL

      
      ##
      ##      user    MACHINE=COMMANDS
      ##
      ## The COMMANDS section may have other options added to it.
      ##
      ## Allow root to run any commands anywhere
      root    ALL=(ALL)       ALL
      
      ## Allows members of the 'sys' group to run networking, software,
      ## service management apps and more.
      # %sys ALL = NETWORKING, SOFTWARE, SERVICES, STORAGE, DELEGATING, PROCESSES, LOCATE, DRIVERS
      
      ## Allows people in group wheel to run all commands
      %wheel  ALL=(ALL)       ALL
      
      ## Same thing without a password
      # %wheel        ALL=(ALL)       NOPASSWD: ALL
      
      ## Allows members of the users group to mount and unmount the
      ## cdrom as root
      # %users  ALL=/sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom
      
      ## Allows members of the users group to shutdown this system
      # %users  localhost=/sbin/shutdown -h now
      
      ## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment)
      #includedir /etc/sudoers.d
      ansible  ALL=(ALL)   NOPASSWD: ALL
      
      
      

      Open in new window



      Thank you



      Hi Leo,

      You need "become=True" (so remove the "#"). If you don't, it will just run the remote commands as the "ansible" user.
      If this a fairly large setup please consider using sssd to use a more sophisticated access to authentication info from a central repository (AD, FreeIPA, IPAM).
      FreeIPA is either a Lookalike of AD (functional) with an implementation meant to be used in Unix/Linux envs.  Or it can work as an intermediate to translate AD Windows-ismsinit Unix/Linux centric constructs.
      AD has no knowledge of UID, GID etc. Groups are fairly different .. so FreeIPA can make this translation or manage them.
      FreeIPA can also centraly manage hostgroups (systems belonging together), sudo rules (fine grained on command level if needed),.  (Sudo more or less compare to policies.. )
      Avatar of Leo

      ASKER

      Thanks Noci for the info.

      Hi Simon,

      I've removed the comment for "become=True"

      I am getting the following now: 

      localhost | FAILED! => {
          "changed": false,
          "module_stderr": "sudo: a password is required\n",
          "module_stdout": "",
          "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
          "rc": 1
      }
      [2022-03-20T21:03:30.358Z] Failed to run Ansible Ad hoc command -  ansible localhost -m ini_file -a "dest=/home/ansible/inventory option=centos07 allow_no_value=true section=lin state=present" --connection=local.
      Have you set ansible_user=ansible in your cfg file, so that it logs in with ssh as the "ansible" user? First make sure that "ssh ansible@remote_host doesn't prompt for a password.
      Avatar of Leo

      ASKER

      Yes, from the controller I can SSH@remote_host and I am not prompted for password. I get right on. 
      We still need to check that the right user is being called. Easiest way might be to comment out the "become=True" then
      ansible remote_host -a id
      

      Open in new window

      This should return the "id" output fior the "ansible" user on the remote host. If it doesn't, make sure that "ansible_user=ansible" is in your ansible.cfg file and try again. When it is reporting ansible's id, uncomment "become=True" and run the command again. It should now return root's id output.
      Avatar of Leo

      ASKER

      Ok so include the below in the ansible.cfg after the commenting the "become=True"
      ansible remote_host -a id
      

      Open in new window

      I've checked ansible.cfg and I haven't seen an entry for "ansible_user=ansible",other playbooks were working as expected but this particular one does not, perhaps there is something on this one that requires this. So I should add an entry anywhere in the ansible.cfg with "ansible_user=ansible" ?

      As I've mentioned before I am using VMware Realize Automation to run Ansible, I had to get this done in to get the integration working, everything in the article seems ok. 
      https://docs.vmware.com/en/vRealize-Automation/8.6/Using-and-Managing-Cloud-Assembly/GUID-9244FFDE-2039-48F6-9CB1-93508FCAFA75.html

      When I uncomment the become and I run the playbook directly from the controller it works but the same playbook isn't working while run from the VMware Realize Automation, that's when I get the error:

      localhost | FAILED! => {
          "changed": false,
          "module_stderr": "sudo: a password is required\n",
          "module_stdout": "",
          "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
          "rc": 1
      }
      [2022-03-20T22:00:05.972Z] Failed to run Ansible Ad hoc command -  ansible localhost -m ini_file -a "dest=/home/ansible/inventory option=centos8 allow_no_value=true section=lin state=present" --connection=local.

      If I remove the the "become" then the error is the same when running directly from the controller or via VMware/Ansible:

      An exception occurred during task execution. To see the full traceback, use -vvv. The error was: OSError: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpsSxYmsllara.sudo',fatal: [centos25]: FAILED! => {"changed": false, "msg": "The destination directory (/etc/sudoers.d) is not writable by the current user. Error was: [Errno 13] Permission denied: '/etc/sudoers.d/.ansible_tmpsSxYmsllara.sudo'"},,PLAY RECAP

      The article says that by default "Initially, Ansible integration uses the user/password or user/key credentials provided in the integration to connect to the Ansible Control Machine." i think it is connecting with the wrong user. Please run a playbook, using the VMWare system, which gives the output of the "id" command, and let me know what it says.
      Avatar of Leo

      ASKER

      Sure, testing that right now... so I've added the following to the ansible.cfg

      [privilege_escalation]
      #become=True
      #become_method=sudo
      #become_user=root
      #become_ask_pass=False
      ansible remote_host -a id

      Correct?

      Avatar of Leo

      ASKER

      With the change above I get the following error:

      Unable to validate syntax for Playbook(s). Failed to execute script on host 172.28.97.154. Error: Error reading config file (/etc/ansible/ansible.cfg): Source contains parsing errors: '' [line 347]: 'ansible remote_host -a id\n'

      That wasn't a line to add to config, it was a command to run. Take the line out, (line 347 in your config) and please try again.
      Avatar of Leo

      ASKER

      Just to make sure I am running this correctly.

      You would like me to run this command from the controller

      ansible remote_host -a id

      the remote_host (is that reading from the inventory) or should I specify the remote host IP/name?

      Thanks 
      I did ssk you to run that command, but I now think that it won't help. VMWare is controlling Ansible accesss, so we need to run tests using VMWare.
      Avatar of Leo

      ASKER

      I see...

      --- 
      - hosts: all
        become: yes
        tasks:
         - name: Allow {{ reqUser }} to sudo, and validate the sudoers file before saving
          #- set_fact: reqUser="{{ lookup('env','requestedBy') }}"
           ansible.builtin.lineinfile:
             path: "/etc/sudoers.d/usera.sudo"
             state: present
             mode: 0440
             create: yes
             regexp: '^co\\usera'
             line: 'co\\usera  ALL=(ALL) NOPASSWD: ALL'
             validate: /usr/sbin/visudo -cf %s
      
      
      

      Open in new window


      I've added "become: yes" to the playbook and it's running fine now! :)

      Double checking and moving into using the variable next... I will keep you in informed.

      Thanks Simon! 
      Excellent!! That was going to be my next suggestion. Adding "become=True" to ansible.cfg means that alll scripts run as root - this may not be what you want. Adding "become: yes" to the playbook is more targetted.

      Good luck!
      Avatar of Leo

      ASKER

      Ok, I am fighting the " " requirements with the use of the variable.

      --- 
      - hosts: all
        become: yes
        tasks:
           - name: Allow {{ reqUser }} to sudo, and validate the sudoers file before saving
            #- set_fact: reqUser="{{ lookup('env','requestedBy') }}"
             ansible.builtin.lineinfile:
                 path: "/etc/sudoers.d/90-cloud-init-users"
                 state: present
                 mode: 0440
                 create: yes
                 regexp: '^dom\\{{ reqUser }}'
                 line: 'dom\\{{ reqUser }} ALL=(ALL) NOPASSWD: ALL'
                 validate: /usr/sbin/visudo -cf %s
      
      
      
      

      Open in new window

      "The task includes an option with an undefined variable. The error was: 'reqUser' is undefined\n\nThe error appears to be in '/home/ansible/setup.yml': line 8, column 8, but may\nbe elsewhere in the file depending on the exact syntax problem. - name: Allow {{ reqUser }} to sudo, and validate the sudoers file before saving\n ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes. Always quote template expression brackets when they\nstart a value. For instance:\n\n with_items:\n - {{ foo }}\n\nShould be written as:\n\n with_items:\n - \"{{ foo }}\"\n"},,PLAY RECAP  

      I've used the code above, keeps complaining about the use of " " , i've tried several options but nothing seems to work. Any suggestions?

      Thanks! 
      Essentially, put double quotes round any parameter with a variable specified with {{}}. This doesn't apply in "when:" clauses, as they don't use curly braces, but does apply in "name:" clauses. Also, your "set_fact" line is not indented properly - even comments (in Ansible, at least) must be properly indented.

      The problem is more that reqUser isn't defined. Didn't we solve this before? You could uncomment the "set_fact", and make it a separate task:
      -- 
      - hosts: all
        become: yes
        tasks:
           - name: set user we are processing 
             set_fact: reqUser="{{ lookup('env','requestedBy') }}"
           - name: "Allow {{ reqUser }} to sudo, and validate the sudoers file before saving" 
             ansible.builtin.lineinfile:
                 path: "/etc/sudoers.d/90-cloud-init-users"
                 state: present
                 mode: 0440
                 create: yes
                 regexp: "^dom\\{{ reqUser }}"
                 line: "dom\\{{ reqUser }} ALL=(ALL) NOPASSWD: ALL"
                 validate: /usr/sbin/visudo -cf %s
      

      Open in new window

      As always, you can always add "debug:" tasks to see what is happening.
      Avatar of Leo

      ASKER

      Understood. What would be the best way to add a debug for this one?

      ASKER CERTIFIED SOLUTION
      Avatar of simon3270
      simon3270
      Flag of United Kingdom of Great Britain and Northern Ireland image

      Blurred text
      THIS SOLUTION IS ONLY AVAILABLE TO MEMBERS.
      View this solution by signing up for a free trial.
      Members can start a 7-Day free trial and enjoy unlimited access to the platform.
      See Pricing Options
      Start Free Trial
      Avatar of Leo

      ASKER

      Yes, the variable was not coming down, it's fixed now. Thanks Simon so much for your help!