├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── __main__.py ├── assets └── etc │ ├── motd │ └── pam.d │ ├── password-auth │ ├── su │ └── system-auth ├── docker-compose.yml └── util.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: 5 | image: circleci/classic:edge 6 | 7 | working_directory: ~/cis 8 | 9 | steps: 10 | - checkout 11 | - run: 12 | name: run tests 13 | pwd: ~/cis 14 | command: docker-compose run linux 15 | 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | [*.py] 14 | max_line_length = 79 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Takashi Nozawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repositry is no longer maintained in favor of [CIS hardened AMIs](https://www.cisecurity.org/cis-hardened-images/amazon/).** 2 | 3 | # amazon-linux-cis 4 | 5 | [![CircleCI](https://circleci.com/gh/nozaq/amazon-linux-cis.svg?style=svg)](https://circleci.com/gh/nozaq/amazon-linux-cis) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/34bfe0c895814295a863a09c30437d34)](https://www.codacy.com/app/nozaq/amazon-linux-cis?utm_source=github.com&utm_medium=referral&utm_content=nozaq/amazon-linux-cis&utm_campaign=badger) 7 | 8 | Bootstrap script for Amazon Linux to comply with [CIS Amazon Linux Benchmark v2.0.0](https://www.cisecurity.org/benchmark/amazon_linux/). 9 | 10 | ## Usage 11 | ``` 12 | $ git clone https://github.com/nozaq/amazon-linux-cis.git . 13 | $ python ./amazon-linux-cis 14 | ``` 15 | 16 | ## Available Arguments 17 | Argument (default value) | What it does 18 | ------------ | ------------- 19 | --time (169.254.169.123) | Specify the upstream time server 20 | --chrony *boolean* (true) | Use chrony for time synchronization 21 | --no-backup | Automatic config backup is disabled 22 | --clients *comma seperate list* | Specify a comma separated list of hostnames and host IP addresses 23 | -v --verbose | Enable verbose logging of utility 24 | --disable-tcp-wrappers | Disable installation of TCP Wrappers package 25 | --disable-pam | Disable the hardening of the PAM module 26 | --disable-iptables | Disable the installation of IPtables 27 | --disable-mount-options | Disable replacing the default */etc/fstab* mounting config file 28 | 29 | 30 | ## Amazon Linux 2 Support 31 | Although the differences between Amazon Linux and Amazon Linux 2 are extensive ([listed here](https://aws.amazon.com/amazon-linux-2/faqs/)), the majority of the changes to reach CIS compliance for Amazon Linux 2 are minor. Here's the minimum required command line needed to install the hardening on Amazon Linux 2 instances. 32 | 33 | ``` 34 | python ./amazon-linux-cis --disable-mount-options 35 | ``` 36 | 37 | ## Tested Environments 38 | - Amazon Linux 2017.09 39 | - Amazon Linux AMI 2018.03.0 (HVM) 40 | - Amazon Linux 2 - 2017.12 41 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | """Bootstrap script for Amazon Linux to comply CIS Amazon Linux Benchmark v2.0.0""" 2 | 3 | import argparse 4 | import logging 5 | import os 6 | import re 7 | from subprocess import CalledProcessError 8 | import pkg_resources 9 | 10 | from util import exec_shell, set_backup_enabled, File, Package, Service, PropertyFile 11 | 12 | 13 | def get_string_asset(path): 14 | """Returns the content of the specified asset file""" 15 | return pkg_resources.resource_string(__name__, 'assets/{}'.format(path)) 16 | 17 | 18 | def disable_unused_filesystems(): 19 | """1.1.1 Disable unused filesystems""" 20 | filesystems = [ 21 | 'cramfs', 'freevxfs', 'jffs2', 'hfs', 'hfsplus', 'squashfs', 'udf', 'vfat' 22 | ] 23 | 24 | prop = PropertyFile('/etc/modprobe.d/CIS.conf', ' ') 25 | for filesystem in filesystems: 26 | prop.override({'install {}'.format(filesystem): '/bin/true'}) 27 | prop.write() 28 | 29 | 30 | def set_mount_options(): 31 | """1.1.2 - 1.1.17""" 32 | options = { 33 | '/tmp': 'tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,relatime 0 0', 34 | '/var/tmp': 'tmpfs /var/tmp tmpfs rw,nosuid,nodev,noexec,relatime 0 0', 35 | '/home': '/dev/xvdf1 /home ext4 rw,nodev,relatime,data=ordered 0 0', 36 | '/dev/shm': 'tmpfs /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime 0 0' 37 | } 38 | 39 | with open('/etc/fstab', 'r') as f: 40 | for line in f: 41 | if line.startswith('#'): 42 | continue 43 | partition = line.split()[1] 44 | if partition not in options: 45 | options[partition] = line.strip() 46 | 47 | with open('/etc/fstab', 'w') as f: 48 | for record in options.values(): 49 | f.write('{}\n'.format(record)) 50 | 51 | 52 | def ensure_sticky_bit(): 53 | """1.1.18 Ensure sticky bit is set on all world - writable directories""" 54 | try: 55 | return exec_shell(['df --local -P | awk {\'if (NR!=1) print $6\'} | xargs -I \'{}\' find \'{}\' -xdev -type d -perm -0002 2>/dev/null | xargs chmod a+t']) 56 | except CalledProcessError: 57 | return 1 58 | 59 | 60 | def disable_automounting(): 61 | """1.1.19 Disable Automounting""" 62 | Service('autofs').disable() 63 | 64 | 65 | def enable_aide(): 66 | """1.3 Filesystem Integrity Checking""" 67 | 68 | cron_job = '0 5 * * * /usr/sbin/aide --check' 69 | 70 | Package('aide').install() 71 | 72 | return exec_shell([ 73 | 'aide --init', 74 | 'mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz', 75 | '(crontab -u root -l 2>/dev/null | grep -v /usr/sbin/aide; echo "{}") | crontab -'.format(cron_job) 76 | ]) 77 | 78 | 79 | def secure_boot_settings(): 80 | """1.4 Secure Boot Settings""" 81 | 82 | if os.path.isfile('/boot/grub/menu.lst'): 83 | exec_shell([ 84 | 'chown root:root /boot/grub/menu.lst', 85 | 'chmod og-rwx /boot/grub/menu.lst' 86 | ]) 87 | 88 | PropertyFile('/etc/sysconfig/init', '=').override({ 89 | 'SINGLE': '/sbin/sulogin', 90 | 'PROMPT': 'no' 91 | }).write() 92 | 93 | 94 | def apply_process_hardenings(): 95 | """1.5 Additional Process Hardening""" 96 | # 1.5.1 Ensure core dumps are restricted 97 | PropertyFile('/etc/security/limits.conf', ' ').override({ 98 | '* hard core': '0' 99 | }).write() 100 | 101 | PropertyFile('/etc/sysctl.conf', ' = ').override({ 102 | 'fs.suid_dumpable': '0' 103 | }).write() 104 | 105 | # 1.5.3 Ensure address space layout randomization (ASLR) is enable 106 | PropertyFile('/etc/sysctl.conf', ' = ').override({ 107 | 'kernel.randomize_va_space': '2' 108 | }).write() 109 | 110 | # 1.5.4 Ensure prelink is disabled 111 | Package('prelink').remove() 112 | 113 | 114 | def configure_warning_banners(): 115 | """1.7 Warning Banners""" 116 | 117 | # 1.7.1 Command Line Warning Banners 118 | exec_shell([ 119 | 'update-motd --disable', 120 | 'chown root:root /etc/motd', 121 | 'chmod 644 /etc/motd' 122 | ]) 123 | File('/etc/motd').write(get_string_asset('/etc/motd')) 124 | 125 | exec_shell(['chown root:root /etc/issue', 'chmod 644 /etc/issue']) 126 | File('/etc/issue').write('Authorized uses only. All activity may be monitored and reported.\n') 127 | 128 | exec_shell(['chown root:root /etc/issue.net', 'chmod 644 /etc/issue.net']) 129 | File('/etc/issue.net').write('Authorized uses only. All activity may be monitored and reported.\n') 130 | 131 | 132 | def ensure_updated(): 133 | """1.8 Ensure updates, patches, and additional security software are installed""" 134 | Package.update_all() 135 | 136 | 137 | def disable_inetd_services(): 138 | """2.1 inetd Services""" 139 | services = [ 140 | 'chargen-dgram', 'chargen-stream', 'daytime-dgram', 'daytime-stream', 141 | 'discard-dgram', 'discard-stream', 'echo-dgram', 'echo-stream', 142 | 'time-dgram', 'time-stream', 'rexec', 'rlogin', 'rsh', 'talk', 143 | 'telnet', 'tftp', 'rsync', 'xinetd' 144 | ] 145 | 146 | for srv in services: 147 | Service(srv).disable() 148 | 149 | 150 | def configure_time_synchronization(upstream, chrony=True): 151 | """2.2.1 Time Synchronization""" 152 | if chrony: 153 | configure_chrony(upstream) 154 | else: 155 | configure_ntp(upstream) 156 | 157 | 158 | def configure_ntp(upstream): 159 | """2.2.1 Time Synchronization""" 160 | # 2.2.1.1 Ensure time synchronization is in use 161 | Package('chrony').remove() 162 | Package('ntp').install() 163 | 164 | # 2.2.1.2 Ensure ntp is configured 165 | PropertyFile('/etc/ntp.conf', ' ').override({ 166 | 'restrict default': None, 167 | 'restrict -4 default': 'kod nomodify notrap nopeer noquery', 168 | 'restrict -6 default': 'kod nomodify notrap nopeer noquery', 169 | 'server': upstream 170 | }).write() 171 | 172 | PropertyFile('/etc/sysconfig/ntpd', '=').override({ 173 | 'OPTIONS': '"-u ntp:ntp"' 174 | }).write() 175 | 176 | 177 | def configure_chrony(upstream): 178 | """2.2.1 Time Synchronization""" 179 | 180 | # 2.2.1.1 Ensure time synchronization is in use 181 | Package('ntp').remove() 182 | Package('chrony').install() 183 | 184 | # 2.2.1.3 Ensure chrony is configured 185 | PropertyFile('/etc/chrony.conf', ' ').override({ 186 | 'server': upstream 187 | }).write() 188 | 189 | PropertyFile('/etc/sysconfig/chronyd', '=').override({ 190 | 'OPTIONS': '"-u chrony"' 191 | }).write() 192 | 193 | exec_shell([ 194 | 'chkconfig chronyd on', 195 | ]) 196 | 197 | 198 | def remove_x11_packages(): 199 | """2.2.2 Ensure X Window System is not installed""" 200 | Package('xorg-x11*').remove() 201 | 202 | 203 | def disable_special_services(): 204 | """2.2.3 - 2.2.14, 2.2.16""" 205 | services = [ 206 | 'avahi-daemon', 'cups', 207 | 'dhcpd', 'slapd', 'nfs', 'rpcbind', 'named', 'vsftpd', 208 | 'httpd', 'dovecot', 'smb', 'squid', 'snmpd', 'ypserv' 209 | ] 210 | 211 | for srv in services: 212 | Service(srv).disable() 213 | 214 | 215 | def configure_mta(): 216 | """2.2.15 Ensure mail transfer agent is configured for local - only mode""" 217 | exec_shell([ 218 | 'mkdir -p /etc/postfix', 219 | 'touch /etc/postfix/main.cf' 220 | ]) 221 | PropertyFile('/etc/postfix/main.cf', ' = ').override({ 222 | 'inet_interfaces': 'localhost' 223 | }).write() 224 | 225 | 226 | def remove_insecure_clients(): 227 | """2.3 Service Clients""" 228 | packages = [ 229 | 'ypbind', 'rsh', 'talk', 230 | 'telnet', 'openldap-clients' 231 | ] 232 | 233 | for package in packages: 234 | Package(package).remove() 235 | 236 | 237 | def configure_host_network_params(): 238 | """3.1 Network Parameters(Host Only)""" 239 | PropertyFile('/etc/sysctl.conf', ' = ').override({ 240 | 'net.ipv4.ip_forward': '0', 241 | 'net.ipv4.conf.all.send_redirects': '0', 242 | 'net.ipv4.conf.default.send_redirects': '0', 243 | }).write() 244 | 245 | 246 | def configure_network_params(): 247 | """3.2 Network Parameters(Host and Router)""" 248 | PropertyFile('/etc/sysctl.conf', ' = ').override({ 249 | 'net.ipv4.conf.all.accept_source_route': '0', 250 | 'net.ipv4.conf.default.accept_source_route': '0', 251 | 'net.ipv4.conf.all.accept_redirects': '0', 252 | 'net.ipv4.conf.default.accept_redirects': '0', 253 | 'net.ipv4.conf.all.secure_redirects': '0', 254 | 'net.ipv4.conf.default.secure_redirects': '0', 255 | 'net.ipv4.conf.all.log_martians': '1', 256 | 'net.ipv4.conf.default.log_martians': '1', 257 | 'net.ipv4.icmp_echo_ignore_broadcasts': '1', 258 | 'net.ipv4.icmp_ignore_bogus_error_responses': '1', 259 | 'net.ipv4.conf.all.rp_filter': '1', 260 | 'net.ipv4.conf.default.rp_filter': '1', 261 | 'net.ipv4.tcp_syncookies': '1' 262 | }).write() 263 | 264 | 265 | def configure_ipv6_params(): 266 | """3.3 IPv6""" 267 | PropertyFile('/etc/sysctl.conf', ' = ').override({ 268 | 'net.ipv6.conf.all.accept_ra': '0', 269 | 'net.ipv6.conf.default.accept_ra': '0', 270 | 'net.ipv6.conf.all.accept_redirects': '0', 271 | 'net.ipv6.conf.default.accept_redirects': '0' 272 | }).write() 273 | 274 | # 3.3.3 Ensure IPv6 is disabled 275 | PropertyFile('/etc/modprobe.d/CIS.conf', ' ').override({ 276 | 'options ipv6': 'disable=1' 277 | }).write() 278 | 279 | 280 | def configure_tcp_wrappers(hosts): 281 | """3.4 TCP Wrappers""" 282 | # 3.4.1 Ensure TCP Wrappers is installed 283 | Package('tcp_wrappers').install() 284 | 285 | if hosts: 286 | # 3.4.2 Ensure /etc/hosts.allow is configured 287 | allowed_hosts = ','.join(hosts) 288 | exec_shell('echo "ALL: {}" > /etc/hosts.allow'.format(allowed_hosts)) 289 | 290 | # 3.4.3 Ensure /etc/hosts.deny is configured 291 | exec_shell('echo "ALL: ALL" > /etc/hosts.deny') 292 | 293 | # 3.4.4 Ensure permissions on /etc/hosts.allow are configured 294 | exec_shell([ 295 | 'chown root:root /etc/hosts.allow', 296 | 'chmod 644 /etc/hosts.allow' 297 | ]) 298 | 299 | # 3.4.5 Ensure permissions on /etc/hosts.deny are configured 300 | exec_shell([ 301 | 'chown root:root /etc/hosts.deny', 302 | 'chmod 644 /etc/hosts.deny' 303 | ]) 304 | 305 | 306 | def disable_uncommon_protocols(): 307 | """3.5 Uncommon Network Protocols""" 308 | modules = [ 309 | 'dccp', 'sctp', 'rds', 'tipc' 310 | ] 311 | prop = PropertyFile('/etc/modprobe.d/CIS.conf', ' ') 312 | for mod in modules: 313 | prop.override({'install {}'.format(mod): '/bin/true'}) 314 | prop.write() 315 | 316 | 317 | def configure_iptables(): 318 | """3.6 Firewall Configuration""" 319 | Package('iptables').install() 320 | 321 | exec_shell([ 322 | 'iptables -F', 323 | 'iptables -P INPUT DROP', 324 | 'iptables -P OUTPUT DROP', 325 | 'iptables -P FORWARD DROP', 326 | 'iptables -A INPUT -i lo -j ACCEPT', 327 | 'iptables -A OUTPUT -o lo -j ACCEPT', 328 | 'iptables -A INPUT -s 127.0.0.0/8 -j DROP', 329 | 'iptables -A OUTPUT -p tcp -m state --state NEW,ESTABLISHED -j ACCEPT', 330 | 'iptables -A OUTPUT -p udp -m state --state NEW,ESTABLISHED -j ACCEPT', 331 | 'iptables -A OUTPUT -p icmp -m state --state NEW,ESTABLISHED -j ACCEPT', 332 | 'iptables -A INPUT -p tcp -m state --state ESTABLISHED -j ACCEPT', 333 | 'iptables -A INPUT -p udp -m state --state ESTABLISHED -j ACCEPT', 334 | 'iptables -A INPUT -p icmp -m state --state ESTABLISHED -j ACCEPT', 335 | 'iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT', 336 | 'iptables-save' 337 | ]) 338 | 339 | 340 | def configure_rsyslog(): 341 | """4.2.1 Configure rsyslog""" 342 | Package('rsyslog').install() 343 | 344 | PropertyFile('/etc/rsyslog.conf', ' ').override({ 345 | '*.emerg': ':omusrmsg:*', 346 | 'mail.*': '-/var/log/mail', 347 | 'mail.info': '-/var/log/mail.info', 348 | 'mail.warning': '-/var/log/mail.warn', 349 | 'mail.err': '/var/log/mail.err', 350 | 'news.crit': '-/var/log/news/news.crit', 351 | 'news.err': '-/var/log/news/news.err', 352 | 'news.notice': '-/var/log/news/news.notice', 353 | '*.=warning;*.=err': '-/var/log/warn', 354 | '*.crit': '/var/log/warn', 355 | '*.*;mail.none;news.none': '-/var/log/messages', 356 | 'local0,local1.*': '-/var/log/localmessages', 357 | 'local2,local3.*': '-/var/log/localmessages', 358 | 'local4,local5.*': '-/var/log/localmessages', 359 | 'local6,local7.*': '-/var/log/localmessages ', 360 | '$FileCreateMode': '0640' 361 | }).write() 362 | 363 | 364 | def configure_log_file_permissions(): 365 | """4.2.4 Ensure permissions on all logfiles are configured""" 366 | exec_shell([r'find /var/log -type f -exec chmod g-wx,o-rwx {} +']) 367 | 368 | 369 | def configure_cron(): 370 | """5.1 Configure cron""" 371 | # 5.1.1 Ensure cron daemon is enabled 372 | Service('crond').enable() 373 | 374 | # 5.1.2 - 5.1.8 375 | exec_shell([ 376 | 'chown root:root /etc/crontab', 377 | 'chmod og-rwx /etc/crontab', 378 | 'chown root:root /etc/cron.hourly', 379 | 'chmod og-rwx /etc/cron.hourly', 380 | 'chown root:root /etc/cron.daily', 381 | 'chmod og-rwx /etc/cron.daily', 382 | 'chown root:root /etc/cron.weekly', 383 | 'chmod og-rwx /etc/cron.weekly', 384 | 'chown root:root /etc/cron.monthly', 385 | 'chmod og-rwx /etc/cron.monthly', 386 | 'chown root:root /etc/cron.d', 387 | 'chmod og-rwx /etc/cron.d', 388 | 'rm -f /etc/cron.deny', 389 | 'rm -f /etc/at.deny', 390 | 'touch /etc/cron.allow', 391 | 'touch /etc/at.allow', 392 | 'chmod og-rwx /etc/cron.allow', 393 | 'chmod og-rwx /etc/at.allow', 394 | 'chown root:root /etc/cron.allow', 395 | 'chown root:root /etc/at.allow' 396 | ]) 397 | 398 | 399 | def configure_sshd(): 400 | """5.2 SSH Server Configuration""" 401 | # 5.2.1 Ensure permissions on /etc/ssh/sshd_config are configured 402 | exec_shell([ 403 | 'chown root:root /etc/ssh/sshd_config', 404 | 'chmod og-rwx /etc/ssh/sshd_config' 405 | ]) 406 | 407 | # 5.2.2 - 5.2.16 408 | PropertyFile('/etc/ssh/sshd_config', ' ').override({ 409 | 'Protocol': '2', 410 | 'LogLevel': 'INFO', 411 | 'X11Forwarding': 'no', 412 | 'MaxAuthTries': '4', 413 | 'IgnoreRhosts': 'yes', 414 | 'HostbasedAuthentication': 'no', 415 | 'PermitRootLogin': 'no', 416 | 'PermitEmptyPasswords': 'no', 417 | 'PermitUserEnvironment': 'no', 418 | 'Ciphers': 'aes256-ctr,aes192-ctr,aes128-ctr', 419 | 'MACs': 'hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com', 420 | 'ClientAliveInterval': '300', 421 | 'ClientAliveCountMax': '0', 422 | 'LoginGraceTime': '60', 423 | 'AllowUsers': 'ec2-user', 424 | 'Banner': '/etc/issue.net' 425 | }).write() 426 | 427 | 428 | def configure_pam(): 429 | """5.3 Configure PAM""" 430 | 431 | def convert_password(line): 432 | if password_unix_re.match(line): 433 | if 'remember=5' not in line: 434 | line += ' remember=5' 435 | if 'sha512' not in line: 436 | line += ' sha512' 437 | return line 438 | password_unix_re = re.compile(r'^password\s+sufficient\s+pam_unix.so') 439 | 440 | password_auth_content = get_string_asset('/etc/pam.d/password-auth') 441 | password_auth_content += exec_shell([ 442 | 'cat /etc/pam.d/password-auth | grep -v "^auth"' 443 | ]) 444 | password_auth_content = '\n'.join([ 445 | convert_password(line) for line in password_auth_content.splitlines() 446 | ]) 447 | 448 | with open('/etc/pam.d/password-auth-local', 'w') as f: 449 | f.write(password_auth_content) 450 | 451 | exec_shell(['ln -sf /etc/pam.d/password-auth-local /etc/pam.d/password-auth']) 452 | 453 | system_auth_content = get_string_asset('/etc/pam.d/system-auth') 454 | system_auth_content += exec_shell([ 455 | 'cat /etc/pam.d/system-auth | grep -v "^auth"' 456 | ]) 457 | system_auth_content = '\n'.join([ 458 | convert_password(line) for line in system_auth_content.splitlines() 459 | ]) 460 | with open('/etc/pam.d/system-auth-local', 'w') as f: 461 | f.write(system_auth_content) 462 | 463 | exec_shell( 464 | ['ln -sf /etc/pam.d/system-auth-local /etc/pam.d/system-auth']) 465 | 466 | PropertyFile('/etc/security/pwquality.conf', '=').override({ 467 | 'minlen': '14', 468 | 'dcredit': '-1', 469 | 'ucredit': '-1', 470 | 'ocredit': '-1', 471 | 'lcredit': '-1' 472 | }).write() 473 | 474 | 475 | def configure_password_parmas(): 476 | """5.4.1 Set Shadow Password Suite Parameters""" 477 | PropertyFile('/etc/login.defs', '\t').override({ 478 | 'PASS_MAX_DAYS': '90', 479 | 'PASS_MIN_DAYS': '7', 480 | 'PASS_WARN_AGE': '7' 481 | }).write() 482 | 483 | exec_shell([ 484 | 'useradd -D -f 30' 485 | ]) 486 | 487 | 488 | def configure_umask(): 489 | """5.4.3, 5.4.4""" 490 | umask_reg = r'^(\s*)umask\s+[0-7]+(\s*)$' 491 | 492 | bashrc = exec_shell([ 493 | 'cat /etc/bashrc | sed -E "s/{}/\\1umask 027\\2/g"'.format(umask_reg) 494 | ]) 495 | File('/etc/bashrc').write(bashrc) 496 | 497 | profile = exec_shell([ 498 | 'cat /etc/profile | sed -E "s/{}/\\1umask 027\\2/g"'.format( 499 | umask_reg) 500 | ]) 501 | File('/etc/profile').write(profile) 502 | 503 | 504 | def configure_su(): 505 | """5.5 Ensure access to the su command is restricted""" 506 | File('/etc/pam.d/su').write(get_string_asset('/etc/pam.d/su')) 507 | exec_shell('usermod -aG wheel root') 508 | 509 | 510 | def main(): 511 | parser = argparse.ArgumentParser( 512 | description='A script to harden Amazon Linux instance.') 513 | 514 | # The Amazon Time Sync Service is available through NTP 515 | # at the 169.254.169.123 IP address for any instance running in a VPC. 516 | # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html 517 | parser.add_argument('--time', metavar='