Commit 4e7ffcd4 authored by stvhu's avatar stvhu
Browse files

Support IAM authentication and access points.

parent 0fd7b833
...@@ -87,27 +87,39 @@ To mount with the recommended default options, simply run: ...@@ -87,27 +87,39 @@ To mount with the recommended default options, simply run:
$ sudo mount -t efs file-system-id efs-mount-point/ $ sudo mount -t efs file-system-id efs-mount-point/
``` ```
To mount automatically with recommended options, add an `/etc/fstab` entry like: To mount over TLS, simply add the `tls` option:
``` ```
file-system-id efs-mount-point efs _netdev 0 0 $ sudo mount -t efs -o tls file-system-id efs-mount-point/
``` ```
To mount over TLS, simply add the `tls` option: To authenticate with EFS using the system’s IAM identity, add the `iam` option. This option requires the `tls` option.
``` ```
$ sudo mount -t efs -o tls file-system-id efs-mount-point/ $ sudo mount -t efs -o tls,iam file-system-id efs-mount-point/
```
To mount using an access point, use the `accesspoint=` option. This option requires the `tls` option.
```
$ sudo mount -t efs -o tls,accesspoint=access-point-id file-system-id efs-mount-point/
```
To mount your file system automatically with any of the options above, you can add entries to `/efs/fstab` like:
```
file-system-id efs-mount-point efs _netdev,tls,iam,accesspoint=access-point-id 0 0
``` ```
To mount over TLS automatically, add an `/etc/fstab` entry like: For more information on mounting with the mount helper, see the manual page:
``` ```
file-system-id efs-mount-point efs _netdev,tls 0 0 man mount.efs
``` ```
For more information on mounting with the mount helper, see the [documentation](https://docs.aws.amazon.com/efs/latest/ug/using-amazon-efs-utils.html). or refer to the [documentation](https://docs.aws.amazon.com/efs/latest/ug/using-amazon-efs-utils.html).
#### amazon-efs-mount-watchdog ### amazon-efs-mount-watchdog
`efs-utils` contains a watchdog process to monitor the health of TLS mounts. This process is managed by either `upstart` or `systemd` depending on your Linux distribution, and is started automatically the first time an EFS file system is mounted over TLS. `efs-utils` contains a watchdog process to monitor the health of TLS mounts. This process is managed by either `upstart` or `systemd` depending on your Linux distribution, and is started automatically the first time an EFS file system is mounted over TLS.
......
...@@ -11,7 +11,7 @@ set -ex ...@@ -11,7 +11,7 @@ set -ex
BASE_DIR=$(pwd) BASE_DIR=$(pwd)
BUILD_ROOT=${BASE_DIR}/build/debbuild BUILD_ROOT=${BASE_DIR}/build/debbuild
VERSION=1.17 VERSION=1.18
echo 'Cleaning deb build workspace' echo 'Cleaning deb build workspace'
rm -rf ${BUILD_ROOT} rm -rf ${BUILD_ROOT}
......
#
# Copyright 2017-2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
#
# Licensed under the MIT License. See the LICENSE accompanying this file
# for the specific language governing permissions and limitations under
# the License.
#
[global]
version=1.18
Package: amazon-efs-utils Package: amazon-efs-utils
Architecture: all Architecture: all
Version: 1.17 Version: 1.18
Section: utils Section: utils
Depends: python|python2, nfs-common, stunnel4 (>= 4.56) Depends: python|python2, nfs-common, stunnel4 (>= 4.56)
Priority: optional Priority: optional
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
%endif %endif
Name : amazon-efs-utils Name : amazon-efs-utils
Version : 1.17 Version : 1.18
Release : 1%{?dist} Release : 1%{?dist}
Summary : This package provides utilities for simplifying the use of EFS file systems Summary : This package provides utilities for simplifying the use of EFS file systems
......
...@@ -10,12 +10,14 @@ ...@@ -10,12 +10,14 @@
logging_level = INFO logging_level = INFO
logging_max_bytes = 1048576 logging_max_bytes = 1048576
logging_file_count = 10 logging_file_count = 10
# mode for /var/run/efs in octal # mode for /var/run/efs and subdirectories in octal
state_file_dir_mode = 750 state_file_dir_mode = 750
[mount] [mount]
dns_name_format = {fs_id}.efs.{region}.amazonaws.com dns_name_format = {fs_id}.efs.{region}.{dns_name_suffix}
dns_name_suffix = amazonaws.com
stunnel_debug_enabled = false stunnel_debug_enabled = false
stunnel_cafile = /etc/amazon/efs/efs-utils.crt
# Validate the certificate hostname on mount. This option is not supported by certain stunnel versions. # Validate the certificate hostname on mount. This option is not supported by certain stunnel versions.
stunnel_check_cert_hostname = true stunnel_check_cert_hostname = true
......
...@@ -81,6 +81,22 @@ more information, see \fBstunnel(8)\fR\&. ...@@ -81,6 +81,22 @@ more information, see \fBstunnel(8)\fR\&.
Selects whether to perform OCSP validation on TLS certificates\&, \ Selects whether to perform OCSP validation on TLS certificates\&, \
overriding /etc/amazon/efs/efs-utils.conf. \ overriding /etc/amazon/efs/efs-utils.conf. \
For more information, see \fBstunnel(8)\fR\&. For more information, see \fBstunnel(8)\fR\&.
.TP
\fBiam\fR
Use the system's IAM identity to authenticate with EFS. The mount helper will try \
to retrieve the required IAM credentials from the following locations: the EC2 instance \
profile, the AWS CLI credentials file (~/.aws/credentials), and the AWS CLI config \
file (~/.aws/config). The first location that has credentials will be used. \
This option requires the \fBtls\fR option\&.
.TP
\fBaccesspoint\fR
Mount the EFS file system using the specified access point. This option requires the \
\fBtls\fR option\&.
.TP
\fBawsprofile\fR
Use the named profile used to lookup IAM credentials in the AWS CLI credentials file \
(~/.aws/credentials) or AWS CLI config file (~/.aws/config). If "awsprofile" is not \
specified, the "default" profile is used\&.
.if n \{\ .if n \{\
.RE .RE
.\} .\}
...@@ -121,6 +137,16 @@ resolve to a fully-qualified EFS DNS name such as \ ...@@ -121,6 +137,16 @@ resolve to a fully-qualified EFS DNS name such as \
"fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com" \ "fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com" \
\(em at mount point "/mnt/efs" using encryption \ \(em at mount point "/mnt/efs" using encryption \
of data in transit\&. of data in transit\&.
.TP
sudo mount -t efs -o tls,iam fs-abcd1234 /mnt/efs
Mount an EFS file system with file system ID "fs-abcd1234" at mount point "/mnt/efs" \
with encryption of data in transit. The mount helper will authenticate with EFS using \
the system's IAM identity\&.
.TP
sudo mount -t efs -o tls,accesspoint=fsap-12345678 fs-abcd1234 /mnt/efs
Mount an EFS file system with file system ID "fs-abcd1234" at mount point "/mnt/efs" \
with encryption of data in transit. The file system is mounted using the access point \
"fsap-12345678"\&.
.SH "FILES" .SH "FILES"
.TP .TP
\fI/sbin/mount.efs\fR \fI/sbin/mount.efs\fR
......
This diff is collapsed.
This diff is collapsed.
#
# Copyright 2017-2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
#
# Licensed under the MIT License. See the LICENSE accompanying this file
# for the specific language governing permissions and limitations under
# the License.
#
#
# Copyright 2017-2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
#
# Licensed under the MIT License. See the LICENSE accompanying this file
# for the specific language governing permissions and limitations under
# the License.
#
import os
try:
import ConfigParser
except ImportError:
from configparser import ConfigParser
FILE_LIST = ['src/watchdog/__init__.py', 'src/mount_efs/__init__.py', 'dist/amazon-efs-utils.spec',
'dist/amazon-efs-utils.control', 'build-deb.sh']
GLOBAL_CONFIG = 'config.ini'
def test_global_version_match():
global_version = get_global_version()
for f in FILE_LIST:
version_in_file = get_version_for_file(f)
assert version_in_file == global_version, \
'version in {} is {}, does not match global version {}'.format(f, version_in_file, global_version)
def get_version_for_file(file_path):
mount_helper_root_folder = uppath(os.path.abspath(__file__), 3)
file_to_check = os.path.join(mount_helper_root_folder, file_path)
with open(file_to_check) as fp:
lines = fp.readlines()
for line in lines:
if line.startswith('VERSION'):
return line.split('=')[1].strip().replace("'", '')
if line.startswith('Version'):
return line.split(':')[1].strip()
return None
def get_global_version():
mount_helper_root_folder = uppath(os.path.abspath(__file__), 3)
config_file = os.path.join(mount_helper_root_folder, GLOBAL_CONFIG)
cp = read_config(config_file)
version = str(cp.get('global', 'version'))
return version
# Given: path : file path
# n : the number of parent level we want to reach
# Returns: parent path of certain level n
# Example: uppath('/usr/lib/java', 1) -> '/usr/lib'
# uppath('/usr/lib/java', 2) -> '/usr'
def uppath(path, n):
return os.sep.join(path.split(os.sep)[:-n])
def read_config(config_file):
try:
p = ConfigParser.SafeConfigParser()
except AttributeError:
p = ConfigParser()
p.read(config_file)
return p
...@@ -11,34 +11,84 @@ import tempfile ...@@ -11,34 +11,84 @@ import tempfile
import pytest import pytest
try:
import ConfigParser
except ImportError:
from configparser import ConfigParser
CAPATH = '/capath' CAPATH = '/capath'
CAFILE = '/cafile.crt' CAFILE = '/cafile.crt'
def create_temp_file(tmpdir, content=''): def _get_config():
try:
config = ConfigParser.SafeConfigParser()
except AttributeError:
config = ConfigParser()
config.add_section(mount_efs.CONFIG_SECTION)
return config
def _create_temp_file(tmpdir, content=''):
temp_file = tmpdir.join(tempfile.mktemp()) temp_file = tmpdir.join(tempfile.mktemp())
temp_file.write(content, ensure=True) temp_file.write(content, ensure=True)
return temp_file return temp_file
def test_use_existing_cafile(tmpdir): def test_use_existing_cafile(tmpdir):
options = {'cafile': str(_create_temp_file(tmpdir))}
efs_config = {} efs_config = {}
stunnel_cafile = str(create_temp_file(tmpdir))
mount_efs.add_stunnel_ca_options(efs_config, stunnel_cafile) mount_efs.add_stunnel_ca_options(efs_config, _get_config(), options)
assert stunnel_cafile == efs_config.get('CAfile') assert options['cafile'] == efs_config.get('CAfile')
assert 'CApath' not in efs_config assert 'CApath' not in efs_config
def test_use_missing_cafile(capsys): def test_use_missing_cafile(capsys):
options = {'cafile': '/missing1'}
efs_config = {} efs_config = {}
stunnel_cafile = '/missing1'
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
mount_efs.add_stunnel_ca_options(efs_config, stunnel_cafile) mount_efs.add_stunnel_ca_options(efs_config, _get_config(), options)
assert 0 != ex.value.code assert 0 != ex.value.code
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert 'Failed to find the EFS certificate authority file for verification' in err assert 'Failed to find certificate authority file for verification' in err
def test_stunnel_cafile_configuration_in_option(mocker):
options = {'cafile': CAFILE}
efs_config = {}
mocker.patch('os.path.exists', return_value=True)
mount_efs.add_stunnel_ca_options(efs_config, _get_config(), options)
assert CAFILE == efs_config.get('CAfile')
def test_stunnel_cafile_configuration_in_config(mocker):
options = {}
efs_config = {}
config = _get_config()
config.set(mount_efs.CONFIG_SECTION, 'stunnel_cafile', CAFILE)
mocker.patch('os.path.exists', return_value=True)
mount_efs.add_stunnel_ca_options(efs_config, config, options)
assert CAFILE == efs_config.get('CAfile')
def test_stunnel_cafile_not_configured(mocker):
options = {}
efs_config = {}
mocker.patch('os.path.exists', return_value=True)
mount_efs.add_stunnel_ca_options(efs_config, _get_config(), options)
assert mount_efs.DEFAULT_STUNNEL_CAFILE == efs_config.get('CAfile')
...@@ -12,9 +12,11 @@ import tempfile ...@@ -12,9 +12,11 @@ import tempfile
from mock import MagicMock from mock import MagicMock
AP_ID = 'fsap-beefdead'
FS_ID = 'fs-deadbeef' FS_ID = 'fs-deadbeef'
DNS_NAME = '%s.efs.us-east-1.amazonaws.com' % FS_ID DNS_NAME = '%s.efs.us-east-1.amazonaws.com' % FS_ID
MOUNT_POINT = '/mnt' MOUNT_POINT = '/mnt'
REGION = 'us-east-1'
DEFAULT_TLS_PORT = 20049 DEFAULT_TLS_PORT = 20049
...@@ -30,7 +32,10 @@ def setup_mocks(mocker): ...@@ -30,7 +32,10 @@ def setup_mocks(mocker):
mocker.patch('mount_efs.start_watchdog') mocker.patch('mount_efs.start_watchdog')
mocker.patch('mount_efs.get_tls_port_range', return_value=(DEFAULT_TLS_PORT, DEFAULT_TLS_PORT + 10)) mocker.patch('mount_efs.get_tls_port_range', return_value=(DEFAULT_TLS_PORT, DEFAULT_TLS_PORT + 10))
mocker.patch('socket.socket', return_value=MagicMock()) mocker.patch('socket.socket', return_value=MagicMock())
mocker.patch('mount_efs.write_tls_tunnel_state_file', return_value="~mocktempfile") mocker.patch('mount_efs.get_dns_name', return_value=DNS_NAME)
mocker.patch('mount_efs.get_region_helper', return_value=REGION)
mocker.patch('mount_efs.write_tls_tunnel_state_file', return_value='~mocktempfile')
mocker.patch('mount_efs.create_certificate')
mocker.patch('os.rename') mocker.patch('os.rename')
mocker.patch('os.kill') mocker.patch('os.kill')
...@@ -43,11 +48,23 @@ def setup_mocks(mocker): ...@@ -43,11 +48,23 @@ def setup_mocks(mocker):
return popen_mock, write_config_mock return popen_mock, write_config_mock
def setup_mocks_without_popen(mocker):
mocker.patch('mount_efs.start_watchdog')
mocker.patch('mount_efs.get_tls_port_range', return_value=(DEFAULT_TLS_PORT, DEFAULT_TLS_PORT + 10))
mocker.patch('socket.gethostname', return_value=DNS_NAME)
mocker.patch('mount_efs.get_dns_name', return_value=DNS_NAME)
mocker.patch('mount_efs.write_tls_tunnel_state_file', return_value='~mocktempfile')
mocker.patch('os.kill')
write_config_mock = mocker.patch('mount_efs.write_stunnel_config_file', return_value=EXPECTED_STUNNEL_CONFIG_FILE)
return write_config_mock
def test_bootstrap_tls_state_file_dir_exists(mocker, tmpdir): def test_bootstrap_tls_state_file_dir_exists(mocker, tmpdir):
popen_mock, _ = setup_mocks(mocker) popen_mock, _ = setup_mocks(mocker)
state_file_dir = str(tmpdir) state_file_dir = str(tmpdir)
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {}, state_file_dir):
pass pass
args, _ = popen_mock.call_args args, _ = popen_mock.call_args
...@@ -64,6 +81,8 @@ def test_bootstrap_tls_state_file_nonexistent_dir(mocker, tmpdir): ...@@ -64,6 +81,8 @@ def test_bootstrap_tls_state_file_nonexistent_dir(mocker, tmpdir):
def config_get_side_effect(section, field): def config_get_side_effect(section, field):
if section == mount_efs.CONFIG_SECTION and field == 'state_file_dir_mode': if section == mount_efs.CONFIG_SECTION and field == 'state_file_dir_mode':
return '0755' return '0755'
elif section == mount_efs.CONFIG_SECTION and field == 'dns_name_format':
return '{fs_id}.efs.{region}.amazonaws.com'
else: else:
raise ValueError('Unexpected arguments') raise ValueError('Unexpected arguments')
...@@ -71,18 +90,84 @@ def test_bootstrap_tls_state_file_nonexistent_dir(mocker, tmpdir): ...@@ -71,18 +90,84 @@ def test_bootstrap_tls_state_file_nonexistent_dir(mocker, tmpdir):
assert not os.path.exists(state_file_dir) assert not os.path.exists(state_file_dir)
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {}, state_file_dir):
pass pass
assert os.path.exists(state_file_dir) assert os.path.exists(state_file_dir)
def test_bootstrap_tls_no_cert_creation(mocker, tmpdir):
setup_mocks_without_popen(mocker)
mocker.patch('mount_efs.get_mount_specific_filename', return_value=DNS_NAME)
state_file_dir = str(tmpdir)
tls_dict = mount_efs.tls_paths_dictionary(DNS_NAME, state_file_dir)
pk_path = os.path.join(str(tmpdir), 'privateKey.pem')
mocker.patch('mount_efs.get_private_key_path', return_value=pk_path)
def config_get_side_effect(section, field):
if section == mount_efs.CONFIG_SECTION and field == 'state_file_dir_mode':
return '0755'
elif section == mount_efs.CONFIG_SECTION and field == 'dns_name_format':
return '{fs_id}.efs.{region}.amazonaws.com'
else:
raise ValueError('Unexpected arguments')
MOCK_CONFIG.get.side_effect = config_get_side_effect
try:
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, None, MOUNT_POINT, {}, state_file_dir):
pass
except OSError as e:
assert '[Errno 2] No such file or directory' in str(e)
assert not os.path.exists(os.path.join(tls_dict['mount_dir'], 'certificate.pem'))
assert not os.path.exists(os.path.join(tls_dict['mount_dir'], 'request.csr'))
assert not os.path.exists(os.path.join(tls_dict['mount_dir'], 'config.conf'))
assert not os.path.exists(pk_path)
def test_bootstrap_tls_cert_created(mocker, tmpdir):
setup_mocks_without_popen(mocker)
mocker.patch('mount_efs.get_mount_specific_filename', return_value=DNS_NAME)
mocker.patch('mount_efs.get_region_helper', return_value=REGION)
state_file_dir = str(tmpdir)
tls_dict = mount_efs.tls_paths_dictionary(DNS_NAME + '+', state_file_dir)
pk_path = os.path.join(str(tmpdir), 'privateKey.pem')
mocker.patch('mount_efs.get_private_key_path', return_value=pk_path)
def config_get_side_effect(section, field):
if section == mount_efs.CONFIG_SECTION and field == 'state_file_dir_mode':
return '0755'
elif section == mount_efs.CONFIG_SECTION and field == 'dns_name_format':
return '{fs_id}.efs.{region}.amazonaws.com'
else:
raise ValueError('Unexpected arguments')
MOCK_CONFIG.get.side_effect = config_get_side_effect
try:
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {},
state_file_dir=state_file_dir):
pass
except OSError as e:
assert '[Errno 2] No such file or directory' in str(e)
assert os.path.exists(os.path.join(tls_dict['mount_dir'], 'certificate.pem'))
assert os.path.exists(os.path.join(tls_dict['mount_dir'], 'request.csr'))
assert os.path.exists(os.path.join(tls_dict['mount_dir'], 'config.conf'))
assert os.path.exists(pk_path)
def test_bootstrap_tls_non_default_port(mocker, tmpdir): def test_bootstrap_tls_non_default_port(mocker, tmpdir):
popen_mock, write_config_mock = setup_mocks(mocker) popen_mock, write_config_mock = setup_mocks(mocker)
mocker.patch('os.rename')
state_file_dir = str(tmpdir) state_file_dir = str(tmpdir)
tls_port = 1000 tls_port = 1000
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {'tlsport': tls_port}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {'tlsport': tls_port},
state_file_dir):
pass pass
popen_args, _ = popen_mock.call_args popen_args, _ = popen_mock.call_args
...@@ -99,7 +184,8 @@ def test_bootstrap_tls_non_default_verify_level(mocker, tmpdir): ...@@ -99,7 +184,8 @@ def test_bootstrap_tls_non_default_verify_level(mocker, tmpdir):
state_file_dir = str(tmpdir) state_file_dir = str(tmpdir)
verify = 0 verify = 0
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {'verify': verify}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {'verify': verify},
state_file_dir):
pass pass
popen_args, _ = popen_mock.call_args popen_args, _ = popen_mock.call_args
...@@ -115,7 +201,7 @@ def test_bootstrap_tls_ocsp_option(mocker, tmpdir): ...@@ -115,7 +201,7 @@ def test_bootstrap_tls_ocsp_option(mocker, tmpdir):
popen_mock, write_config_mock = setup_mocks(mocker) popen_mock, write_config_mock = setup_mocks(mocker)
state_file_dir = str(tmpdir) state_file_dir = str(tmpdir)
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {'ocsp': None}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {'ocsp': None}, state_file_dir):
pass pass
popen_args, _ = popen_mock.call_args popen_args, _ = popen_mock.call_args
...@@ -132,7 +218,7 @@ def test_bootstrap_tls_noocsp_option(mocker, tmpdir): ...@@ -132,7 +218,7 @@ def test_bootstrap_tls_noocsp_option(mocker, tmpdir):
popen_mock, write_config_mock = setup_mocks(mocker) popen_mock, write_config_mock = setup_mocks(mocker)
state_file_dir = str(tmpdir) state_file_dir = str(tmpdir)
with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, MOUNT_POINT, {'noocsp': None}, state_file_dir): with mount_efs.bootstrap_tls(MOCK_CONFIG, INIT_SYSTEM, DNS_NAME, FS_ID, AP_ID, MOUNT_POINT, {'noocsp': None}, state_file_dir):
pass pass
popen_args, _ = popen_mock.call_args popen_args, _ = popen_mock.call_args
...@@ -143,18 +229,3 @@ def test_bootstrap_tls_noocsp_option(mocker, tmpdir): ...@@ -143,18 +229,3 @@ def test_bootstrap_tls_noocsp_option(mocker, tmpdir):
assert EXPECTED_STUNNEL_CONFIG_FILE in popen_args assert EXPECTED_STUNNEL_CONFIG_FILE in popen_args
# positional argument for ocsp_override # positional argument for ocsp_override
assert write_config_args[7] is False