augtool is a program that can be obtained via the augeas package or as part of puppet.
dnf install augeas
OR dnf install puppet
It’s a tool to programmatically edit linux configuration files safely.
Here are some cheatsheet examples :
augtool print /files/etc/ssh/sshd_config
augtool ls /files/etc/ssh/sshd_config
augtool preview /files/etc/ssh/sshd_config
augtool set /files/etc/ssh/sshd_config/PermitRootLogin yes
So for example, looking at the /etc/hosts file :
augtool print /files/etc/hosts | tail
/files/etc/hosts/9
/files/etc/hosts/9/ipaddr = "192.168.1.145"
/files/etc/hosts/9/canonical = "sonic"
/files/etc/hosts/9/alias = "sonic.fabio.org.uk"
/files/etc/hosts/10
/files/etc/hosts/10/ipaddr = "192.168.1.34"
/files/etc/hosts/10/canonical = "foreman3"
/files/etc/hosts/10/alias[1] = "puppetmaster7"
/files/etc/hosts/10/alias[2] = "puppetmaster7.localdomain"
You could write and use an augeas script to add that last line:
#!/usr/bin/augtool -if
ins /files/etc/hosts/*[last()+1] after /files/etc/hosts/*[last()]
set /files/etc/hosts/*[last()]/ipaddr 192.168.1.34
set /files/etc/hosts/*[last()]/canonical foreman3
set /files/etc/hosts/*[last()]/alias[1] puppetmaster7
set /files/etc/hosts/*[last()]/alias[2] puppetmaster7.localdomain
save
quit
And an augtool script to drop a line on the end of rsyslog.conf:
#!/usr/bin/augtool -if
set '/files/etc/rsyslog.conf/entry[last()+1]/selector/facility' authpriv
set '/files/etc/rsyslog.conf/entry[last()]/selector/level' *
set '/files/etc/rsyslog.conf/entry[last()]/action/protocol' @
set '/files/etc/rsyslog.conf/entry[last()]/action/hostname' 192.168.1.32
save
quit
Achieving that using puppet here is the manifest:
augeas { "rsyslog":
context => "/files/etc/rsyslog.conf",
changes => [
"set entry[last()+1]/selector/facility authpriv",
"set entry[last()]/selector/level *",
"set entry[last()]/action/protocol @",
"set entry[last()]/action/hostname '192.168.1.32'",
],
onlyif => "match entry[*]/action/hostname[. = '192.168.1.32' ] size == 0" ,
notify => Service["rsyslog"],
}
service { "rsyslog":
require => Augeas["rsyslog"],
enable => true,
ensure => running,
}
You could also install bolt and run that on a remote host if you can ssh into them:
bolt apply rsyslog.pp --targets mgptmp -u user1 --password-prompt --sudo-password-prompt --run-as root
Please enter your password:
Please enter your privilege escalation password:
Starting: install puppet and gather facts on mgptmp
Finished: install puppet and gather facts with 0 failures in 3.81 sec
Starting: apply catalog on mgptmp
Started on mgptmp...
Finished on mgptmp:
Notice: /Stage[main]/Main/Augeas[rsyslog]/returns: executed successfully
Notice: /Stage[main]/Main/Service[rsyslog]: Triggered 'refresh' from 1 event
changed: 2, failed: 0, unchanged: 0 skipped: 0, noop: 0
Finished: apply catalog with 0 failures in 5.12 sec
Successful on 1 target: mgptmp
Ran on 1 target in 8.98 sec
Here is configuring chrony. We are putting 4 server lines in, and removing the pool line.
#get rid of all the pool* lines
augeas { 'nopool':
context => "/files/etc/chrony.conf",
changes => [
"rm pool[*]",
],
onlyif => "match pool[*] size != 0",
notify => Service["chronyd"],
}
# add 4 server lines
# touch creates the iburst node with no value
augeas { "chronyconf":
context => "/files/etc/chrony.conf",
changes => [
"touch server",
"set server[1] ntp1.herts.ac.uk",
"touch server[1]/iburst",
"set server[2] ntp2.herts.ac.uk",
"touch server[2]/iburst",
"set server[3] ntp3.herts.ac.uk",
"touch server[3]/iburst",
"set server[4] ntp4.herts.ac.uk",
"touch server[4]/iburst",
],
onlyif => "match server[*][. = 'ntp*herts.ac.uk' ] size < 4",
show_diff => true,
notify => Service["chronyd"],
}
# to restart on change
service { "chronyd":
require => Augeas["chronyconf","nopool"],
enable => true,
ensure => running,
}
And we can apply it locally or using bolt:
puppet apply chrony.pp
Notice: Compiled catalog for vuh-lb-foreman03.herts.ac.uk in environment production in 0.02 seconds
Notice: Augeas[nopool](provider=augeas):
--- /etc/chrony.conf 2024-07-16 20:43:48.934393433 +0100
+++ /etc/chrony.conf.augnew 2024-07-16 20:43:55.008444093 +0100
@@ -1,7 +1,6 @@
# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (https://www.pool.ntp.org/join.html).
#
-pool ntp.rocky iburst
# Use NTP servers from DHCP.
sourcedir /run/chrony-dhcp
Notice: /Stage[main]/Main/Augeas[nopool]/returns: executed successfully
Notice: Augeas[chronyconf](provider=augeas):
--- /etc/chrony.conf 2024-07-16 20:43:55.059444796 +0100
+++ /etc/chrony.conf.augnew 2024-07-16 20:43:55.450450186 +0100
@@ -48,3 +48,7 @@
# Select which information is logged.
#log measurements statistics tracking
+server ntp1.herts.ac.uk iburst
+server ntp2.herts.ac.uk iburst
+server ntp3.herts.ac.uk iburst
+server ntp4.herts.ac.uk iburst
Notice: /Stage[main]/Main/Augeas[chronyconf]/returns: executed successfully
Notice: /Stage[main]/Main/Service[chronyd]: Triggered 'refresh' from 2 event
Ok so now lets switch to using the augeas resource in puppet to add a complex group of stanzas in sshd_config . This is the stanza:
# my puppet anchor
PubkeyAuthentication no
PasswordAuthentication no
PermitRootLogin no
AllowGroups grafana programmers root
Match Group "programmers,grafana,root" Address "193.101.30.0/23,193.101.32.0/23,193.101.108.149,193.101.52.0/24"
PubkeyAuthentication yes
PasswordAuthentication yes
How does augeas describe the format of that file. Use augtool to find out:
augtool print /files/etc/ssh/sshd_config | tail
/files/etc/ssh/sshd_config/Match
/files/etc/ssh/sshd_config/Match/Condition
/files/etc/ssh/sshd_config/Match/Condition/Group = "programmers,grafana,root"
/files/etc/ssh/sshd_config/Match/Condition/Address = ""193.101.30.0/23,193.101.32.0/23,193.101.108.149,193.101.52.0/24"
/files/etc/ssh/sshd_config/Match/Settings
/files/etc/ssh/sshd_config/Match/Settings/PubkeyAuthentication = "yes"
/files/etc/ssh/sshd_config/Match/Settings/PasswordAuthentication = "yes"
Using a puppet manifest file (mymanifest.pp) first lets add an anchor comment in the file we can use to group our settings together.
augeas { "mycomment":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins #comment after #comment[last()]",
"set #comment[last()] 'my puppet anchor'",
],
onlyif => "match #comment[. = 'my puppet anchor'] size == 0",
notify => Service["sshd"],
}
service { "sshd":
require => Augeas["mycomment"],
enable => true,
ensure => running,
}
We apply a manifest file like this:
puppet apply mymanifest.pp
Notice: Compiled catalog for puppetmaster in environment production in 0.09 seconds
Notice: /Stage[main]/Main/Augeas[mycomment]/returns: executed successfully
Notice: /Stage[main]/Main/Service[sshd]: Triggered 'refresh' from 1 event
Notice: Applied catalog in 0.75 seconds
Now I want to add three lines if they are not already in the file, below the anchor. Here is one of those:
# if there is no PermitRootLogin then insert it (else do nothing)
augeas { "permitrootlogin":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins PermitRootLogin after #comment[. = 'my puppet anchor']",
"set PermitRootLogin no",
],
onlyif => "match PermitRootLogin size == 0",
notify => Service["sshd"],
}
Ok here’s the whole shabang as its quite involved, maybe you learn by examples like me:
augeas { "mycomment":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins #comment after #comment[last()]",
"set #comment[last()] 'my puppet anchor'",
],
onlyif => "match #comment[. = 'my puppet anchor'] size == 0",
notify => Service["sshd"],
}
# if there is no PermitRootLogin then insert it (else do nothing)
augeas { "permitrootlogin":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins PermitRootLogin after #comment[. = 'my puppet anchor']",
"set PermitRootLogin no",
],
onlyif => "match PermitRootLogin size == 0",
notify => Service["sshd"],
}
# if there is no PasswordAuthentiction then insert it
augeas { "passwordauthentication":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins PasswordAuthentication after #comment[. = 'my puppet anchor']",
"set PasswordAuthentication no",
],
onlyif => "match PasswordAuthentication size == 0",
notify => Service["sshd"],
}
# if there is no PubkeyAuthentication then insert it
augeas { "pubkeyauthentication":
context => "/files/etc/ssh/sshd_config",
changes => [
"ins PubkeyAuthentication after #comment[. = 'my puppet anchor']",
"set PubkeyAuthentication no",
],
onlyif => "match PubkeyAuthentication size == 0",
notify => Service["sshd"],
}
# puppet maintain global PermitRootLogin , PasswordAuthentication, PubkeyAuthentication to "no"
# puppet maintain AllowGroups
# puppet maintain Match Stanza 1
augeas { "sshd_config":
context => "/files/etc/ssh/sshd_config",
changes => [
"set PermitRootLogin no",
"set PasswordAuthentication no",
"set PubkeyAuthentication no",
"set AllowGroups/1 grafana",
"set AllowGroups/2 programmers",
"set AllowGroups/3 root",
"set Match[1]/Condition/Group programmers,grafana,root",
"set Match[1]/Condition/Address 193.101.30.0/23,193.101.32.0/23,193.101.108.149,193.101.52.0/24",
"set Match[1]/Settings/PubkeyAuthentication yes",
"set Match[1]/Settings/PasswordAuthentication yes",
],
notify => Service["sshd"],
}
service { "sshd":
require => Augeas["sshd_config"],
enable => true,
ensure => running,
}
Hopefully there are enough breadcrumbs in that code to inspire or describe what is involved. It certainly seems like a lot of learning for such a simple result.