One place for hosting & domains

      September 2020

      How To Harden OpenSSH Client on Ubuntu 18.04


      The author selected the Electronic Frontier Foundation Inc to receive a donation as part of the Write for DOnations program.

      Introduction

      Linux servers are often administered remotely using SSH by connecting to an OpenSSH server, which is the default SSH server software used within Ubuntu, Debian, CentOS, FreeBSD, and most other Linux/BSD-based systems. Significant effort is put into securing the server-side aspect of SSH, as SSH acts as the entry into your server.

      However, it is also important to consider security on the client-side, such as OpenSSH client.

      OpenSSH client is the “client” side of SSH, also known as the ssh command. You can learn more about the SSH client-server model in SSH Essentials: Working with SSH Servers, Clients, and Keys.

      When hardening SSH at the server side, the primary objective is to make it harder for malicious actors to access your server. However, hardening at the client side is very different, as instead you are working to defend and protect your SSH connection and client from various different threats, including:

      • Attackers on the network, known as “person-in-the-middle” attacks.
      • Compromised or malicious servers sending malformed data packets, nefarious control sequences, or large amounts of data to overload your client.
      • Human error, such as mistyping server addresses or configuration values.

      In this tutorial, you will harden your OpenSSH client in order to help ensure that outgoing SSH connections are as secure as possible.

      Prerequisites

      To complete this tutorial, you will need:

      • A device that you use as an SSH client, for example:

      • An SSH server that you want to connect to, for example:

        • A cloud server
        • A public service such as GitHub or GitLab
        • A third-party device that you are permitted to access

      Once you have these ready, log in to your SSH client device as a non-root user to begin.

      Step 1 — General Hardening

      In this first step, you will implement some initial hardening configurations in order to improve the overall security of your SSH client.

      The exact hardening configuration that is most suitable for your client depends heavily on your own threat model and risk threshold. However, the configuration described in this step is a general, all-round secure configuration that should suit the majority of users.

      Many of the hardening configurations for OpenSSH client are implemented using the global OpenSSH client configuration file, which is located at /etc/ssh/ssh_config. In addition to this, some configurations may also be set using the local SSH configuration file for your user, located at ~/.ssh/config.

      Before continuing with this tutorial, it is recommended to take a backup of both of these files, so that you can restore them in the unlikely event that something goes wrong.

      Take a backup of the files using the following commands:

      • sudo cp /etc/ssh/ssh_config /etc/ssh/ssh_config.bak
      • cp ~/.ssh/config ~/.ssh/config.bak

      This will save a backup copy of the files in their default location, but with the .bak extension added.

      Note that your local SSH configuration file (~/.ssh/config) may not exist if you haven’t used it in the past. If this is the case, it can be safely ignored for now.

      You can now open the global configuration file using your favorite text editor to begin implementing the initial hardening measures:

      • sudo nano /etc/ssh/ssh_config

      Note: The OpenSSH client configuration file includes many default options and configurations. Depending on your existing client setup, some of the recommended hardening options may already have been set.

      When editing your configuration file, some options may be commented out by default using a single hash character (#) at the start of the line. To edit these options, or have the commented option be recognized, you’ll need to uncomment them by removing the hash.

      Firstly, you should disable X11 display forwarding over SSH by setting the following options:

      /etc/ssh/ssh_config

      ForwardX11 no
      ForwardX11Trusted no
      

      X11 forwarding allows for the display of remote graphical applications over an SSH connection, however this is rarely used in practice. By disabling it, you can prevent potentially malicious or compromised servers from attempting to forward an X11 session to your client, which in some cases can allow for filesystem permissions to be bypassed, or for local keystrokes to be monitored.

      Next, you can consider disabling SSH tunneling. SSH tunneling is quite widely used, so you may need to keep it enabled. However, if it isn’t required for your particular setup, you can safely disable it as a further hardening measure:

      /etc/ssh/ssh_config

      Tunnel no
      

      You should also consider disabling SSH agent forwarding if it isn’t required, in order to prevent servers from requesting to use your local SSH agent to authenticate onward SSH connections:

      /etc/ssh/ssh_config

      ForwardAgent no
      

      In the majority of cases, your SSH client will be configured to use password authentication or public-key authentication when connecting to servers. However, OpenSSH client also supports other authentication methods, some of which are enabled by default. If these are not required, they can be disabled to further reduce the potential attack surface of your client:

      /etc/ssh/ssh_config

      GSSAPIAuthentication no
      HostbasedAuthentication no
      

      If you’d like to know more about some of the additional authentication methods available within SSH, you may wish to review these resources:

      OpenSSH client allows you to automatically pass custom environment variables when connecting to servers, for example, to set a language preference or configure terminal settings. However, if this isn’t required in your setup, you can prevent any variables being sent by ensuring that the SendEnv option is commented out or completely removed:

      /etc/ssh/ssh_config

      # SendEnv
      

      Finally, you should ensure that strict host key checking is enabled, to ensure that you are appropriately warned when the host key/fingerprint of a remote server changes, or when connecting to a new server for the first time:

      /etc/ssh/ssh_config

      StrictHostKeyChecking ask
      

      This will prevent you from connecting to a server when the known host key has changed, which could mean that the server has been rebuilt or upgraded, or could be indicative of an ongoing person-in-the-middle attack.

      When connecting to a new server for the first time, your SSH client will ask you whether you want to accept the host key and save it in your ~/.ssh/known_hosts file. It’s important that you verify the host key before accepting it, which usually involves asking the server administrator or browsing the documentation for the service (in the case of GitHub/GitLab and other similar services).

      Save and exit the file.

      Now that you’ve completed your initial configuration file hardening, you should validate the syntax of your new configuration by running SSH in test mode:

      You can substitute the . with any hostname to test/simulate any settings contained within Match or Host blocks.

      If your configuration file has a valid syntax, the options that will apply to that specific connection will be printed out. In the event of a syntax error, there will be an output describing the issue.

      You do not need to restart any system services for your new configuration to take effect, although existing SSH sessions will need to be re-established if you want them to inherit the new settings.

      In this step, you completed some general hardening of your OpenSSH client configuration file. Next, you’ll restrict the ciphers that are available for use in SSH connections.

      Step 2 — Restricting Available Ciphers

      Next, you will configure the cipher suites available within your SSH client to disable support for those that are deprecated/legacy.

      Begin by opening your global configuration file in your text editor:

      • sudo nano /etc/ssh/ssh_config

      Next, ensure that the existing Ciphers configuration line is commented out by prefixing it with a single hash (#).

      Then, add the following to the top of the file:

      /etc/ssh/ssh_config

      Ciphers -arcfour*,-*cbc
      

      This will disable the legacy Arcfour ciphers, as well as all ciphers using Cipher Block Chaining (CBC), which is no longer recommended for use.

      If there is a requirement to connect to systems that only support these legacy ciphers, you can explicitly re-enable the required ciphers for specific hosts by using a Match block. For example, to enable the 3des-cbc cipher for a specific legacy host, the following configuration could be used:

      /etc/ssh/ssh_config

      Match host legacy-server.your-domain
        Ciphers +3des-cbc
      

      Save and exit the file.

      Finally, as you did in Step 1, you may wish to test your SSH client configuration again to check for any potential errors:

      If you have added a Match block to enable legacy ciphers for a specific host, you can also specifically target that configuration during the test by specifying the associated host address:

      • ssh -G legacy-server.your-domain

      You’ve secured the ciphers available to your SSH client. Next, you will review the access permissions for files used by your SSH client.

      Step 3 — Securing Configuration File and Private Key Permissions

      In this step, you’ll lock down the permissions for your SSH client configuration files and private keys to help prevent accidental or malicious changes, or private key disclosure. This is especially useful when using a shared client device between multiple users.

      By default on a fresh installation of Ubuntu, the OpenSSH client configuration file(s) are configured so that each user can only edit their own local configuration file (~/.ssh/config), and sudo/administrative access is required to edit the system-wide configuration (/etc/ssh/ssh_config).

      However, in some cases, especially on systems that have been in existence for a long time, these configuration file permissions may have been accidentally modified or adjusted, so it’s best to reset them to make sure that the configuration is secure.

      You can begin by checking the current permissions value for the system-wide OpenSSH client configuration file using the stat command, which you can use to show the status or files and/or filesystem objects:

      • stat -c "%a %A %U:%G" /etc/ssh/ssh_config

      You use the -c argument to specify a custom output format.

      Note: On some operating systems, such as macOS, you will need to use the -f option to specify a custom format rather than -c.

      In this case, the %A %a %U:%G option will print the permissions for the file in octal and human-readable format, as well as the user/group that owns the file.

      This will output something similar to the following:

      Output

      644 -rw-r--r-- root:root

      In this case, the permissions are correct, root owns the file entirely, and only root has permission to write to/modify it.

      Note: If you’d like to refresh your knowledge on Linux permissions before continuing, you may wish to review An Introduction to Linux Permissions.

      However, if your own output is different, you should reset the permissions back to the default using the following commands:

      • sudo chown root:root /etc/ssh/ssh_config
      • sudo chmod 644 /etc/ssh/ssh_config

      If you repeat the stat command from earlier in this step, you will now receive the correct values for your system-wide configuration file.

      Next, you can carry out the same checks for your own local SSH client configuration file, if you have one:

      • stat -c "%a %A %U:%G" ~/.ssh/config

      This will output something similar to the following:

      Output

      644 -rw--r--r-- user:user

      If the permissions for your own client configuration file permissions are any different, you should reset them using the following commands, similarly to earlier in the step:

      • chown user:user ~/.ssh/config
      • chmod 644 ~/.ssh/config

      Next, you can check the permissions for each of the SSH private keys that you have within your ~/.ssh directory, as these files should only be accessible by yourself, and not any other users on the system.

      Begin by printing the current permission and ownership values for each private key:

      • stat -c "%a %A %U:%G" ~/.ssh/id_rsa

      This will output something similar to the following:

      Output

      600 -rw------- user:user

      It is extremely important that you properly lock down the permissions for your private key files, as failing to do so could allow other users of your device to steal them and access the associated servers or remote user accounts.

      If the permissions aren’t properly configured, use the following commands on each private key file to reset them to the secure defaults:

      • chown user:user ~/.ssh/id_rsa
      • chmod 600 ~/.ssh/id_rsa

      In this step, you assessed and locked down the file permissions for your SSH client configuration files and private keys. Next, you will implement an outbound allowlist to limit which servers your client is able to connect to.

      Step 4 — Restricting Outgoing Connections Using a Host Allowlist

      In this final step, you will implement an outgoing allowlist in order to restrict the hosts that your SSH client is able to connect to. This is especially useful for shared/multi-user systems, as well as SSH jump hosts or bastion hosts.

      This security control is specifically designed to help protect against human error/mistakes, such as mistyped server addresses or hostnames. It can be easily bypassed by the user by editing their local configuration file, and so isn’t designed to act as a defense against malicious users/actors.

      If you want to restrict outbound connections at the network level, the correct way to do this is using firewall rules. This is beyond the scope of this tutorial, but you can check out UFW Essentials: Common Firewall Rules and Commands.

      However, if you want to add some additional fail-safes, then this security control may be of benefit to you.

      It works by using a wildcard rule within your SSH client configuration file to null route all outbound connections, apart from those to specific addresses or hostnames. This means that if you were ever to accidentally mistype a server address, or attempt to connect to a server that you’re not supposed to, the request would be stopped immediately, giving you the opportunity to realize your mistake and take corrective action.

      You can apply this at either the system-level (/etc/ssh/ssh_config) or using your local user configuration file (~/.ssh/config). In this example, we will use the local user configuration file.

      Begin by opening the file, creating it if it doesn’t already exist:

      At the bottom of the file, add the following content, substituting in your own list of allowed IP addresses and hostnames:

      ~/.ssh/config

      Match host !203.0.113.1,!192.0.2.1,!server1.your-domain,!github.com,*
        Hostname localhost
      

      You must prefix IP addresses or hostnames with an exclamation point (!), and use commas to separate each item in the list. The final list item should be a single asterisk (*) without a prefixed exclamation point.

      If you’re running an SSH server on your machine too, you may wish to use a hostname value other than localhost, as this will cause the null routed connections to be sent to your own local SSH server, which could be counterproductive or confusing. Any nullrouted hostname is acceptable, such as null, do-not-use, or disallowed-server.

      Save and close the file once you’ve made your changes.

      You can now test that the configuration is working by attempting to connect to a disallowed destination using your SSH client. For example:

      • ssh disallowed.your-domain

      If the configuration is working properly, you will immediately receive an error similar to the following:

      Output

      Cannot connect to localhost: connection refused

      However, when you attempt to connect to an allowed destination, the connection will succeed as normal.

      In this final step, you implemented some additional fail-safes to help protect against human error and mistakes when using your SSH client.

      Conclusion

      In this article you reviewed your OpenSSH client configuration and implemented various hardening measures.

      This will have improved the security of your outgoing SSH connections, as well as helping to ensure that your local configuration files cannot be accidentally or maliciously modified by other users.

      You may wish to review the manual pages for OpenSSH client and its associated configuration file to identify any potential further tweaks that you want to make:

      Finally, if you want to harden OpenSSH at the server side too, check out How To Harden OpenSSH on Ubuntu 18.04.



      Source link

      How To Install and Secure Redis on CentOS 8


      Not using CentOS 8?


      Choose a different version or distribution.

      Introduction

      Redis is an open-source, in-memory key-value data store which excels at caching. A non-relational database, Redis is known for its flexibility, performance, scalability, and wide language support.

      Redis was designed for use by trusted clients in a trusted environment, and has no robust security features of its own. Redis does, however, have a few security features like a basic unencrypted password as well as command renaming and disabling. This tutorial provides instructions on how to install Redis and configure these security features. It also covers a few other settings that can boost the security of a standalone Redis installation on CentOS 8.

      Note that this guide does not address situations where the Redis server and the client applications are on different hosts or in different data centers. Installations where Redis traffic has to traverse an insecure or untrusted network will require a different set of configurations, such as setting up an SSL proxy or a VPN between the Redis machines.

      Prerequisites

      To complete this tutorial, you will need a server running CentOS 8. This server should have a non-root user with administrative privileges and a firewall configured with firewalld. To set this up, follow our Initial Server Setup guide for CentOS 8.

      Step 1 — Installing and Starting Redis

      You can install Redis with the DNF package manager. The following command will install Redis, its dependencies, and nano, a user-friendly text editor. You don’t have to install nano, but we’ll use it in examples throughout this guide:

      • sudo dnf install redis nano

      This command will prompt you to confirm that you want to install the selected packages. Press y then ENTER to do so:

      Output

      . . . Total download size: 1.5 M Installed size: 5.4 M Is this ok [y/N]: y

      Following this, there is one important configuration change to make in the Redis configuration file, which was generated automatically during the installation.

      Open this file with your preferred text editor. Here we’ll use nano:

      • sudo nano /etc/redis/redis.conf

      Inside the file, find the supervised directive. This directive allows you to declare an init system to manage Redis as a service, providing you with more control over its operation. The supervised directive is set to no by default. Since you are running CentOS, which uses the systemd init system, change this to systemd:

      /etc/redis/redis.conf

      . . .
      
      # If you run Redis from upstart or systemd, Redis can interact with your
      # supervision tree. Options:
      #   supervised no      - no supervision interaction
      #   supervised upstart - signal upstart by putting Redis into SIGSTOP mode
      #   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
      #   supervised auto    - detect upstart or systemd method based on
      #                        UPSTART_JOB or NOTIFY_SOCKET environment variables
      # Note: these supervision methods only signal "process is ready."
      #       They do not enable continuous liveness pings back to your supervisor.
      supervised systemd
      
      . . .
      

      That’s the only change you need to make to the Redis configuration file at this point, so save and close it when you are finished. If you used nano to edit the file, do so by pressing CTRL + X, Y, then ENTER.

      After editing the file, start the Redis service:

      • sudo systemctl start redis.service

      If you’d like Redis to start on boot, you can enable it with the enable command:

      • sudo systemctl enable redis

      Notice that this command doesn’t include the .service suffix after the unit file name. You can usually leave this suffix off of systemctl commands, as it’s typically implied when interacting with systemd.

      You can check Redis’s status by running the following:

      • sudo systemctl status redis

      Output

      ● redis.service - Redis persistent key-value database Loaded: loaded (/usr/lib/systemd/system/redis.service; enabled; vendor preset: disabled) Drop-In: /etc/systemd/system/redis.service.d └─limit.conf Active: active (running) since Wed 2020-09-30 20:05:24 UTC; 13s ago Main PID: 13734 (redis-server) Tasks: 4 (limit: 11489) Memory: 6.6M CGroup: /system.slice/redis.service └─13734 /usr/bin/redis-server 127.0.0.1:6379

      Once you’ve confirmed that Redis is indeed running, you can test its functionality with this command:

      This should print PONG as the response:

      Output

      PONG

      If this is the case, it means you now have Redis running on your server and you can begin configuring it to enhance its security.

      Step 2 — Configuring Redis and Securing it with a Firewall

      An effective way to safeguard Redis is to secure the server it’s running on. You can do this by ensuring that Redis is bound only to either localhost or to a private IP address and also that the server has a firewall up and running.

      However, if you chose to set up Redis using another tutorial, then you may have updated the configuration file to allow connections from anywhere. This is not as secure as binding to localhost or a private IP.

      To remedy this, open the Redis configuration file again with your preferred text editor:

      • sudo nano /etc/redis.conf

      Locate the line beginning with bind and make sure it’s uncommented:

      /etc/redis.conf

      . . .
      bind 127.0.0.1
      

      If you need to bind Redis to another IP address (as in cases where you will be accessing Redis from a separate host) we strongly encourage you to bind it to a private IP address. Binding to a public IP address increases the exposure of your Redis interface to outside parties:

      /etc/redis.conf

      . . .
      bind your_private_ip
      

      After confirming that the bind directive isn’t commented out, you can save and close the file.

      If you’ve followed the prerequisite Initial Server Setup tutorial and installed firewalld on your server, and you do not plan to connect to Redis from another host, then you do not need to add any extra firewall rules for Redis. After all, any incoming traffic will be dropped by default unless explicitly allowed by the firewall rules. Since a default standalone installation of Redis server is listening only on the loopback interface (127.0.0.1 or localhost), there should be no concern for incoming traffic on its default port.

      If, however, you do plan to access Redis from another host, you will need to make some changes to your firewalld configuration using the firewall-cmd command. Again, you should only allow access to your Redis server from your hosts by using their private IP addresses in order to limit the number of hosts your service is exposed to.

      To begin, add a dedicated Redis zone to your firewalld policy:

      • sudo firewall-cmd --permanent --new-zone=redis

      Then specify which port you’d like to have open. Redis uses port 6397 by default:

      • sudo firewall-cmd --permanent --zone=redis --add-port=6379/tcp

      Next, specify any private IP addresses which should be allowed to pass through the firewall and access Redis:

      • sudo firewall-cmd --permanent --zone=redis --add-source=client_server_private_IP

      After running those commands, reload the firewall to implement the new rules:

      • sudo firewall-cmd --reload

      Under this configuration, when the firewall encounters a packet from your client’s IP address, it will apply the rules in the dedicated Redis zone to that connection. All other connections will be processed by the default public zone. The services in the default zone apply to every connection, not just those that don’t match explicitly, so you don’t need to add other services (e.g. SSH) to the Redis zone because those rules will be applied to that connection automatically.

      Keep in mind that using any firewall tool will work, whether you use firewalld, ufw, or iptables. What’s important is that the firewall is up and running so that unknown individuals cannot access your server. In the next step, you will configure Redis to only be accessible with a strong password.

      Step 3 — Configuring a Redis Password

      Configuring a Redis password enables one of its built-in security features — the auth command — which requires clients to authenticate before being allowed access to the database. Like the bind setting, the password is configured directly in Redis’s configuration file, /etc/redis.conf. Reopen that file:

      • sudo nano /etc/redis.conf

      Scroll to the SECURITY section and look for a commented directive that reads:

      /etc/redis.conf

      . . .
      # requirepass foobared
      

      Uncomment it by removing the #, and change foobared to a very strong password of your choosing.

      Note: Rather than make up a password yourself, you may use a tool like apg or pwgen to generate one. If you don’t want to install an application just to generate a password, though, you may use the command below. This command echoes a string value and pipes it into the following sha256sum command, which will display the string’s SHA256 checksum.

      Be aware that entering this command as written will generate the same password every time. To create a unique password, change the string in quotes to any other word or phrase:

      • echo "digital-ocean" | sha256sum

      Though the generated password will not be pronounceable, it will be very strong and long, which is exactly the type of password required for Redis. After copying and pasting the output of that command as the new value for requirepass, it should read:

      /etc/redis.conf

      . . .
      requirepass password_copied_from_output
      

      Alternatively, if you prefer a shorter password, you could instead use the output of the following command. Again, change the word in quotes so it will not generate the same password as this command:

      • echo "digital-ocean" | sha1sum

      After setting the password, save and close the file then restart Redis:

      • sudo systemctl restart redis

      To test that the password works, open the Redis client:

      The following is a sequence of commands used to test whether the Redis password works. The first command tries to set a key to a value before authentication:

      That won’t work as you have not yet authenticated, so Redis returns an error:

      Output

      (error) NOAUTH Authentication required.

      The following command authenticates with the password specified in the Redis configuration file:

      Redis will acknowledge that you have been authenticated:

      Output

      OK

      After that, running the previous command again should be successful:

      Output

      OK

      The get key1 command queries Redis for the value of the new key:

      Output

      "10"

      This last command exits redis-cli. You may also use exit:

      It should now be very difficult for unauthorized users to access your Redis installation. Be aware that if you’re already using the Redis client and then restart Redis, you’ll need to re-authenticate. Also, please note that without SSL or a VPN, the unencrypted password will still be visible to outside parties if you’re connecting to Redis remotely.

      Next, this guide will go over renaming Redis commands to further protect Redis from malicious actors.

      Step 4 — Renaming Dangerous Commands

      The other security feature built into Redis allows you to rename or completely disable certain commands that are considered dangerous. When run by unauthorized users, such commands can be used to reconfigure, destroy, or otherwise wipe your data. Some of the commands that are considered to be dangerous include:

      • FLUSHDB
      • FLUSHALL
      • KEYS
      • PEXPIRE
      • DEL
      • CONFIG
      • SHUTDOWN
      • BGREWRITEAOF
      • BGSAVE
      • SAVE
      • SPOP
      • SREM
      • RENAME
      • DEBUG

      This is not a comprehensive list, but renaming or disabling all of the commands in this list can help to improve your data store’s security. Whether you should disable or rename a given command will depend on your specific needs. If you know you will never use a command that can be abused, then you may disable it. Otherwise, you should rename it instead.

      Like the authentication password, renaming or disabling commands is configured in the SECURITY section of the /etc/redis.conf file. To enable or disable Redis commands, open the configuration file for editing one more time:

      • sudo nano /etc/redis.conf

      NOTE: These are examples. You should choose to disable or rename the commands that make sense for you. You can learn more about Redis’s commands and determine how they might be misused at redis.io/commands.

      To disable or kill a command, rename it to an empty string, like this:

      /etc/redis.conf

      # It is also possible to completely kill a command by renaming it into
      # an empty string:
      #
      rename-command FLUSHDB ""
      rename-command FLUSHALL ""
      rename-command DEBUG ""
      

      To rename a command, give it another name like in the examples below. Renamed commands should be difficult for others to guess, but easy for you to remember:

      /etc/redis.conf

      # It is also possible to completely kill a command by renaming it into
      # an empty string:
      #
      rename-command FLUSHDB ""
      rename-command FLUSHALL ""
      rename-command DEBUG ""
      rename-command SHUTDOWN SHUTDOWN_MENOT
      rename-command CONFIG ASC12_CONFIG
      

      Save your changes and close the file. Then apply the changes by restarting Redis:

      • sudo systemctl restart redis.service

      To test your new commands, enter the Redis command line:

      Authenticate yourself using the password you defined earlier:

      Output

      OK

      Assuming that you renamed the CONFIG command to ASC12_CONFIG, attempting to use the config command will fail:

      Output

      (error) ERR unknown command 'config'

      Calling the renamed command instead will be successful. Note that Redis commands are not case-sensitive:

      • asc12_config get requirepass

      Output

      1) "requirepass" 2) "your_redis_password"

      Finally, you can exit from redis-cli:

      Warning: Regarding renaming commands, there’s a cautionary statement at the end of the SECURITY section in the /etc/redis.conf file, which reads:

      /etc/redis.conf

      . . .
      
      # Please note that changing the name of commands that are logged into the
      # AOF file or transmitted to slaves may cause problems.
      
      . . .
      

      This means if the renamed command is not in the AOF file, or if it is but the AOF file has not been transmitted to replicas, then there should be no problem. Keep that in mind as you’re renaming commands. The best time to rename a command is when you’re not using AOF persistence or right after installation (that is, before your Redis-using application has been deployed).

      When you’re using AOF and dealing with Redis replication, consider this answer from the project’s GitHub issue page. The following is a reply to the author’s question:

      The commands are logged to the AOF and replicated to the slave the same way they are sent, so if you try to replay the AOF on an instance that doesn’t have the same renaming, you may face inconsistencies as the command cannot be executed (same for slaves).

      The best way to handle renaming in cases like that is to make sure that renamed commands are applied to the primary instance as well as every secondary instance in a Redis installation.

      Step 5 — Setting Data Directory Ownership and File Permissions

      This step will go through a couple of ownership and permissions changes you may need to make to improve the security profile of your Redis installation. This involves making sure that only the user that needs to access Redis has permission to read its data. That user is, by default, the redis user.

      You can verify this by grep-ing for the Redis data directory in a long listing of its parent directory. This command and its output are given below:

      • ls -l /var/lib | grep redis

      Output

      drwxr-x---. 2 redis redis 22 Sep 30 20:15 redis

      This output indicates that the Redis data directory is owned by the redis user, with secondary access granted to the redis group. This ownership setting is secure as are the folder’s permissions which, using octal notation, are set to 750.

      If your Redis data directory has insecure permissions (for example, it’s world-readable) you can ensure that only the Redis user and group have access to the folder and its contents by running the chmod command. The following example changes this folder’s the permissions setting to 770:

      • sudo chmod 770 /var/lib/redis

      The other permission you may need to change is that of the Redis configuration file. By default, it has a file permission of 640 and is owned by root, with secondary ownership by the root group:

      Output

      -rw-r-----. 1 redis root 62344 Sep 30 20:14 /etc/redis.conf

      That permission (640) means the Redis configuration file is readable only by the redis user and the root group. Because the configuration file contains the unencrypted password you configured in Step 4, redis.conf should be owned by the redis user, with secondary ownership by the redis group. To set this, run the following command:

      • sudo chown redis:redis /etc/redis.conf

      Then change the permissions so that only the owner of the file can read and write to it:

      • sudo chmod 600 /etc/redis.conf

      You may verify the new ownership and permissions by running the previous ls commands again:

      • ls -l /var/lib | grep redis

      Output

      total 40 drwxrwx---. 2 redis redis 22 Sep 30 20:15 redis

      Output

      total 40 -rw-------. 1 redis redis 62344 Sep 30 20:14 /etc/redis.conf

      Finally, restart Redis to reflect these changes:

      • sudo systemctl restart redis

      With that, your Redis installation has been secured.

      Conclusion

      Keep in mind that once someone is logged in to your server, it’s very easy to circumvent the Redis-specific security features you’ve put in place. This is why the most important security feature covered in this tutorial is the firewall, as that prevents unknown users from logging into your server in the first place.

      If you’re attempting to secure Redis communication across an untrusted network you’ll have to employ an SSL proxy, as recommended by Redis developers in the official Redis security guide.



      Source link

      How To Use unittest to Write a Test Case for a Function in Python


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      The Python standard library includes the unittest module to help you write and run tests for your Python code.

      Tests written using the unittest module can help you find bugs in your programs, and prevent regressions from occurring as you change your code over time. Teams adhering to test-driven development may find unittest useful to ensure all authored code has a corresponding set of tests.

      In this tutorial, you will use Python’s unittest module to write a test for a function.

      Prerequisites

      To get the most out of this tutorial, you’ll need:

      Defining a TestCase Subclass

      One of the most important classes provided by the unittest module is named TestCase. TestCase provides the general scaffolding for testing our functions. Let’s consider an example:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["shark", "tuna"]}
              self.assertEqual(actual, expected)
      

      First we import unittest to make the module available to our code. We then define the function we want to test—here it is add_fish_to_aquarium.

      In this case our add_fish_to_aquarium function accepts a list of fish named fish_list, and raises an error if fish_list has more than 10 elements. The function then returns a dictionary mapping the name of a fish tank "tank_a" to the given fish_list.

      A class named TestAddFishToAquarium is defined as a subclass of unittest.TestCase. A method named test_add_fish_to_aquarium_success is defined on TestAddFishToAquarium. test_add_fish_to_aquarium_success calls the add_fish_to_aquarium function with a specific input and verifies that the actual returned value matches the value we’d expect to be returned.

      Now that we’ve defined a TestCase subclass with a test, let’s review how we can execute that test.

      Executing a TestCase

      In the previous section, we created a TestCase subclass named TestAddFishToAquarium. From the same directory as the test_add_fish_to_aquarium.py file, let’s run that test with the following command:

      • python -m unittest test_add_fish_to_aquarium.py

      We invoked the Python library module named unittest with python -m unittest. Then, we provided the path to our file containing our TestAddFishToAquarium TestCase as an argument.

      After we run this command, we receive output like the following:

      Output

      . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK

      The unittest module ran our test and told us that our test ran OK. The single . on the first line of the output represents our passed test.

      Note: TestCase recognizes test methods as any method that begins with test. For example, def test_add_fish_to_aquarium_success(self) is recognized as a test and will be run as such. def example_test(self), conversely, would not be recognized as a test because it does not begin with test. Only methods beginning with test will be run and reported when you run python -m unittest ....

      Now let’s try a test with a failure.

      We modify the following highlighted line in our test method to introduce a failure:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["rabbit"]}
              self.assertEqual(actual, expected)
      

      The modified test will fail because add_fish_to_aquarium won’t return "rabbit" in its list of fish belonging to "tank_a". Let’s run the test.

      Again, from the same directory as test_add_fish_to_aquarium.py we run:

      • python -m unittest test_add_fish_to_aquarium.py

      When we run this command, we receive output like the following:

      Output

      F ====================================================================== FAIL: test_add_fish_to_aquarium_success (test_add_fish_to_aquarium.TestAddFishToAquarium) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_add_fish_to_aquarium.py", line 13, in test_add_fish_to_aquarium_success self.assertEqual(actual, expected) AssertionError: {'tank_a': ['shark', 'tuna']} != {'tank_a': ['rabbit']} - {'tank_a': ['shark', 'tuna']} + {'tank_a': ['rabbit']} ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)

      The failure output indicates that our test failed. The actual output of {'tank_a': ['shark', 'tuna']} did not match the (incorrect) expectation we added to test_add_fish_to_aquarium.py of: {'tank_a': ['rabbit']}. Notice also that instead of a ., the first line of the output now has an F. Whereas . characters are outputted when tests pass, F is the output when unittest runs a test that fails.

      Now that we’ve written and run a test, let’s try writing another test for a different behavior of the add_fish_to_aquarium function.

      Testing a Function that Raises an Exception

      unittest can also help us verify that the add_fish_to_aquarium function raises a ValueError Exception if given too many fish as input. Let’s expand on our earlier example, and add a new test method named test_add_fish_to_aquarium_exception:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["shark", "tuna"]}
              self.assertEqual(actual, expected)
      
          def test_add_fish_to_aquarium_exception(self):
              too_many_fish = ["shark"] * 25
              with self.assertRaises(ValueError) as exception_context:
                  add_fish_to_aquarium(fish_list=too_many_fish)
              self.assertEqual(
                  str(exception_context.exception),
                  "A maximum of 10 fish can be added to the aquarium"
              )
      

      The new test method test_add_fish_to_aquarium_exception also invokes the add_fish_to_aquarium function, but it does so with a 25 element long list containing the string "shark" repeated 25 times.

      test_add_fish_to_aquarium_exception uses the with self.assertRaises(...) context manager provided by TestCase to check that add_fish_to_aquarium rejects the inputted list as too long. The first argument to self.assertRaises is the Exception class that we expect to be raised—in this case, ValueError. The self.assertRaises context manager is bound to a variable named exception_context. The exception attribute on exception_context contains the underlying ValueError that add_fish_to_aquarium raised. When we call str() on that ValueError to retrieve its message, it returns the correct exception message we expected.

      From the same directory as test_add_fish_to_aquarium.py, let’s run our test:

      • python -m unittest test_add_fish_to_aquarium.py

      When we run this command, we receive output like the following:

      Output

      .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

      Notably, our test would have failed if add_fish_to_aquarium either didn’t raise an Exception, or raised a different Exception (for example TypeError instead of ValueError).

      Note: unittest.TestCase exposes a number of other methods beyond assertEqual and assertRaises that you can use. The full list of assertion methods can be found in the documentation, but a selection are included here:

      Method Assertion
      assertEqual(a, b) a == b
      assertNotEqual(a, b) a != b
      assertTrue(a) bool(a) is True
      assertFalse(a) bool(a) is False
      assertIsNone(a) a is None
      assertIsNotNone(a) a is not None
      assertIn(a, b) a in b
      assertNotIn(a, b) a not in b

      Now that we’ve written some basic tests, let’s see how we can use other tools provided by TestCase to harness whatever code we are testing.

      Using the setUp Method to Create Resources

      TestCase also supports a setUp method to help you create resources on a per-test basis. setUp methods can be helpful when you have a common set of preparation code that you want to run before each and every one of your tests. setUp lets you put all this preparation code in a single place, instead of repeating it over and over for each individual test.

      Let’s take a look at an example:

      test_fish_tank.py

      import unittest
      
      class FishTank:
          def __init__(self):
              self.has_water = False
      
          def fill_with_water(self):
              self.has_water = True
      
      class TestFishTank(unittest.TestCase):
          def setUp(self):
              self.fish_tank = FishTank()
      
          def test_fish_tank_empty_by_default(self):
              self.assertFalse(self.fish_tank.has_water)
      
          def test_fish_tank_can_be_filled(self):
              self.fish_tank.fill_with_water()
              self.assertTrue(self.fish_tank.has_water)
      

      test_fish_tank.py defines a class named FishTank. FishTank.has_water is initially set to False, but can be set to True by calling FishTank.fill_with_water(). The TestCase subclass TestFishTank defines a method named setUp that instantiates a new FishTank instance and assigns that instance to self.fish_tank.

      Since setUp is run before every individual test method, a new FishTank instance is instantiated for both test_fish_tank_empty_by_default and test_fish_tank_can_be_filled. test_fish_tank_empty_by_default verifies that has_water starts off as False. test_fish_tank_can_be_filled verifies that has_water is set to True after calling fill_with_water().

      From the same directory as test_fish_tank.py, we can run:

      • python -m unittest test_fish_tank.py

      If we run the previous command, we will receive the following output:

      Output

      .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

      The final output shows that the two tests both pass.

      setUp allows us to write preparation code that is run for all of our tests in a TestCase subclass.

      Note: If you have multiple test files with TestCase subclasses that you’d like to run, consider using python -m unittest discover to run more than one test file. Run python -m unittest discover --help for more information.

      Using the tearDown Method to Clean Up Resources

      TestCase supports a counterpart to the setUp method named tearDown. tearDown is useful if, for example, we need to clean up connections to a database, or modifications made to a filesystem after each test completes. We’ll review an example that uses tearDown with filesystems:

      test_advanced_fish_tank.py

      import os
      import unittest
      
      class AdvancedFishTank:
          def __init__(self):
              self.fish_tank_file_name = "fish_tank.txt"
              default_contents = "shark, tuna"
              with open(self.fish_tank_file_name, "w") as f:
                  f.write(default_contents)
      
          def empty_tank(self):
              os.remove(self.fish_tank_file_name)
      
      
      class TestAdvancedFishTank(unittest.TestCase):
          def setUp(self):
              self.fish_tank = AdvancedFishTank()
      
          def tearDown(self):
              self.fish_tank.empty_tank()
      
          def test_fish_tank_writes_file(self):
              with open(self.fish_tank.fish_tank_file_name) as f:
                  contents = f.read()
              self.assertEqual(contents, "shark, tuna")
      

      test_advanced_fish_tank.py defines a class named AdvancedFishTank. AdvancedFishTank creates a file named fish_tank.txt and writes the string "shark, tuna" to it. AdvancedFishTank also exposes an empty_tank method that removes the fish_tank.txt file. The TestAdvancedFishTank TestCase subclass defines both a setUp and tearDown method.

      The setUp method creates an AdvancedFishTank instance and assigns it to self.fish_tank. The tearDown method calls the empty_tank method on self.fish_tank: this ensures that the fish_tank.txt file is removed after each test method runs. This way, each test starts with a clean slate. The test_fish_tank_writes_file method verifies that the default contents of "shark, tuna" are written to the fish_tank.txt file.

      From the same directory as test_advanced_fish_tank.py let’s run:

      • python -m unittest test_advanced_fish_tank.py

      We will receive the following output:

      Output

      . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK

      tearDown allows you to write cleanup code that is run for all of your tests in a TestCase subclass.

      Conclusion

      In this tutorial, you have written TestCase classes with different assertions, used the setUp and tearDown methods, and run your tests from the command line.

      The unittest module exposes additional classes and utilities that you did not cover in this tutorial. Now that you have a baseline, you can use the unittest module’s documentation to learn more about other available classes and utilities. You may also be interested in How To Add Unit Testing to Your Django Project.



      Source link