Commit 311f34fe authored by Tigran Mkrtchyan's avatar Tigran Mkrtchyan
Browse files

exports: add support for secure/insecure export options

Motivation:
In some environments we might want to enforce clients to come from a
privileged port (< 1024), e.g. to be sure that request is send by kernel
process.

Modification:
Add secure/insecure export options (like in linux and solaris servers).
Update PseudoFs to to check for privileged client port, if required.

Result:
nfs clients can be restricted to privileged (root) users.

Acked-by: Lea Morschel
Acked-by: Paul Millar
Target: master
parent c6ffcd94
/* /*
* Copyright (c) 2009 - 2019 Deutsches Elektronen-Synchroton, * Copyright (c) 2009 - 2020 Deutsches Elektronen-Synchroton,
* Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY
* *
* This library is free software; you can redistribute it and/or modify * This library is free software; you can redistribute it and/or modify
...@@ -283,6 +283,16 @@ public class ExportFile implements ExportTable { ...@@ -283,6 +283,16 @@ public class ExportFile implements ExportTable {
continue; continue;
} }
if (option.equals("secure")) {
exportBuilder.withPrivilegedClientPort();
continue;
}
if (option.equals("insecure")) {
exportBuilder.withoutPrivilegedClientPort();
continue;
}
throw new IllegalArgumentException("Unsupported option: " + option); throw new IllegalArgumentException("Unsupported option: " + option);
} }
FsExport export = exportBuilder.build(path); FsExport export = exportBuilder.build(path);
......
...@@ -75,6 +75,7 @@ public class FsExport { ...@@ -75,6 +75,7 @@ public class FsExport {
private final int _index; private final int _index;
private final boolean _withPnfs; private final boolean _withPnfs;
private final List<layouttype4> _layoutTypes; private final List<layouttype4> _layoutTypes;
private final boolean _requirePrivilegedClientPort;
/** /**
* NFS clients may be specified in a number of ways:<br> * NFS clients may be specified in a number of ways:<br>
...@@ -127,6 +128,7 @@ public class FsExport { ...@@ -127,6 +128,7 @@ public class FsExport {
_withPnfs = builder.isWithPnfs(); _withPnfs = builder.isWithPnfs();
_index = getExportIndex(_path); _index = getExportIndex(_path);
_layoutTypes = List.copyOf(builder.getLayoutTypes()); _layoutTypes = List.copyOf(builder.getLayoutTypes());
_requirePrivilegedClientPort = builder.isPrivilegedClientPortRequired();
} }
public static int getExportIndex(String path) { public static int getExportIndex(String path) {
...@@ -156,6 +158,8 @@ public class FsExport { ...@@ -156,6 +158,8 @@ public class FsExport {
.append(_withAcl ? "acl" : "noacl") .append(_withAcl ? "acl" : "noacl")
.append(',') .append(',')
.append("sec=").append(_sec) .append("sec=").append(_sec)
.append(_requirePrivilegedClientPort ? "secure" : "insecure")
.append(",")
.append(',') .append(',')
.append(_withDcap ? "dcap" : "no_dcap") .append(_withDcap ? "dcap" : "no_dcap")
.append(',') .append(',')
...@@ -239,6 +243,10 @@ public class FsExport { ...@@ -239,6 +243,10 @@ public class FsExport {
return _withPnfs; return _withPnfs;
} }
public boolean isPrivilegedClientPortRequired() {
return _requirePrivilegedClientPort;
}
/** /**
* Get an ordered list of layout types to be used by this export entry. * Get an ordered list of layout types to be used by this export entry.
* *
...@@ -350,6 +358,7 @@ public class FsExport { ...@@ -350,6 +358,7 @@ public class FsExport {
private boolean _allRoot = false; private boolean _allRoot = false;
private boolean _withPnfs = true; private boolean _withPnfs = true;
private final List<layouttype4> _layoutTypes = new ArrayList<>(); private final List<layouttype4> _layoutTypes = new ArrayList<>();
private boolean _requirePrivilegedClientPort;
public FsExportBuilder forClient(String client) { public FsExportBuilder forClient(String client) {
checkArgument(isValidHostSpecifier(client), "bad host specifier: " + client); checkArgument(isValidHostSpecifier(client), "bad host specifier: " + client);
...@@ -358,6 +367,20 @@ public class FsExport { ...@@ -358,6 +367,20 @@ public class FsExport {
return this; return this;
} }
public FsExportBuilder withPrivilegedClientPort() {
_requirePrivilegedClientPort = true;
return this;
}
public FsExportBuilder withoutPrivilegedClientPort() {
_requirePrivilegedClientPort = false;
return this;
}
public boolean isPrivilegedClientPortRequired() {
return _requirePrivilegedClientPort;
}
public FsExportBuilder trusted() { public FsExportBuilder trusted() {
_isTrusted = Root.TRUSTED; _isTrusted = Root.TRUSTED;
return this; return this;
......
...@@ -21,7 +21,7 @@ package org.dcache.nfs.vfs; ...@@ -21,7 +21,7 @@ package org.dcache.nfs.vfs;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetSocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
...@@ -62,8 +62,12 @@ import static org.dcache.nfs.vfs.AclCheckable.Access; ...@@ -62,8 +62,12 @@ import static org.dcache.nfs.vfs.AclCheckable.Access;
public class PseudoFs extends ForwardingFileSystem { public class PseudoFs extends ForwardingFileSystem {
private final static Logger _log = LoggerFactory.getLogger(PseudoFs.class); private final static Logger _log = LoggerFactory.getLogger(PseudoFs.class);
/** TCP port range between 0 and 1023 can be used only by privileged (root) user */
public static final int PRIVILEGED_PORT = 1023;
private final Subject _subject; private final Subject _subject;
private final InetAddress _inetAddress; private final InetSocketAddress _inetAddress;
private final VirtualFileSystem _inner; private final VirtualFileSystem _inner;
private final ExportTable _exportTable; private final ExportTable _exportTable;
private final RpcAuth _auth; private final RpcAuth _auth;
...@@ -77,7 +81,7 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -77,7 +81,7 @@ public class PseudoFs extends ForwardingFileSystem {
_inner = inner; _inner = inner;
_subject = call.getCredential().getSubject(); _subject = call.getCredential().getSubject();
_auth = call.getCredential(); _auth = call.getCredential();
_inetAddress = call.getTransport().getRemoteSocketAddress().getAddress(); _inetAddress = call.getTransport().getRemoteSocketAddress();
_exportTable = exportTable; _exportTable = exportTable;
} }
...@@ -189,13 +193,13 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -189,13 +193,13 @@ public class PseudoFs extends ForwardingFileSystem {
/* /*
* reject if there are no exports for this client at all * reject if there are no exports for this client at all
*/ */
if (!_exportTable.exports(_inetAddress).findAny().isPresent()) { if (!_exportTable.exports(_inetAddress.getAddress()).findAny().isPresent()) {
_log.warn("Access denied: (no export) fs root for client {}", _inetAddress); _log.warn("Access denied: (no export) fs root for client {}", _inetAddress);
throw new AccessException("no exports"); throw new AccessException("no exports");
} }
Inode inode = _inner.getRootInode(); Inode inode = _inner.getRootInode();
FsExport export = _exportTable.getExport("/", _inetAddress); FsExport export = _exportTable.getExport("/", _inetAddress.getAddress());
return export == null? realToPseudo(inode) : return export == null? realToPseudo(inode) :
pushExportIndex(inode, export.getIndex()); pushExportIndex(inode, export.getIndex());
} }
...@@ -211,7 +215,7 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -211,7 +215,7 @@ public class PseudoFs extends ForwardingFileSystem {
/* /*
* REVISIT: this is not the best place to do it, but the simples one. * REVISIT: this is not the best place to do it, but the simples one.
*/ */
FsExport export = _exportTable.getExport(parent.exportIndex(), _inetAddress); FsExport export = _exportTable.getExport(parent.exportIndex(), _inetAddress.getAddress());
if (!export.isWithDcap() && ".(get)(cursor)".equals(path)) { if (!export.isWithDcap() && ".(get)(cursor)".equals(path)) {
throw new NoEntException("the dcap magic file is blocked"); throw new NoEntException("the dcap magic file is blocked");
} }
...@@ -427,7 +431,7 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -427,7 +431,7 @@ public class PseudoFs extends ForwardingFileSystem {
if (!inode.isPseudoInode()) { if (!inode.isPseudoInode()) {
int exportIdx = getExportIndex(inode); int exportIdx = getExportIndex(inode);
FsExport export = _exportTable.getExport(exportIdx, _inetAddress); FsExport export = _exportTable.getExport(exportIdx, _inetAddress.getAddress());
if (exportIdx != 0 && export == null) { if (exportIdx != 0 && export == null) {
if (shouldLog) { if (shouldLog) {
_log.warn("Access denied: (no export) to inode {} for client {}", inode, _inetAddress); _log.warn("Access denied: (no export) to inode {} for client {}", inode, _inetAddress);
...@@ -435,6 +439,12 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -435,6 +439,12 @@ public class PseudoFs extends ForwardingFileSystem {
throw new AccessException("permission deny"); throw new AccessException("permission deny");
} }
if (export.isPrivilegedClientPortRequired() && _inetAddress.getPort() > PRIVILEGED_PORT) {
if (shouldLog) {
_log.warn("Access denied: unprivileged client {}", _inetAddress);
}
throw new AccessException("unprivileged client");
}
checkSecurityFlavor(_auth, export.getSec()); checkSecurityFlavor(_auth, export.getSec());
if ( (export.ioMode() == FsExport.IO.RO) && Acls.wantModify(requestedMask)) { if ( (export.ioMode() == FsExport.IO.RO) && Acls.wantModify(requestedMask)) {
...@@ -620,7 +630,7 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -620,7 +630,7 @@ public class PseudoFs extends ForwardingFileSystem {
* This can be wrong, e.g. RO vs. RW. * This can be wrong, e.g. RO vs. RW.
*/ */
if (inode.handleVersion() == 0) { if (inode.handleVersion() == 0) {
FsExport export = _exportTable.exports(_inetAddress) FsExport export = _exportTable.exports(_inetAddress.getAddress())
.findFirst() .findFirst()
.orElse(null); .orElse(null);
return export == null? -1 : export.getIndex(); return export == null? -1 : export.getIndex();
...@@ -679,7 +689,7 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -679,7 +689,7 @@ public class PseudoFs extends ForwardingFileSystem {
Inode rootInode = realToPseudo(_inner.getRootInode()); Inode rootInode = realToPseudo(_inner.getRootInode());
PseudoFsNode root = new PseudoFsNode(rootInode); PseudoFsNode root = new PseudoFsNode(rootInode);
_exportTable.exports(_inetAddress).forEach(e -> pathToPseudoFs(root, nodes, e)); _exportTable.exports(_inetAddress.getAddress()).forEach(e -> pathToPseudoFs(root, nodes, e));
if (nodes.isEmpty()) { if (nodes.isEmpty()) {
_log.warn("No exports found for: {}", _inetAddress); _log.warn("No exports found for: {}", _inetAddress);
...@@ -727,6 +737,6 @@ public class PseudoFs extends ForwardingFileSystem { ...@@ -727,6 +737,6 @@ public class PseudoFs extends ForwardingFileSystem {
} }
private boolean inheritUidGid(Inode inode) { private boolean inheritUidGid(Inode inode) {
return _exportTable.getExport(inode.exportIndex(), _inetAddress).isAllRoot(); return _exportTable.getExport(inode.exportIndex(), _inetAddress.getAddress()).isAllRoot();
} }
} }
...@@ -274,4 +274,25 @@ public class FsExportTest { ...@@ -274,4 +274,25 @@ public class FsExportTest {
assertEquals(FsExport.IO.RO, iomode); assertEquals(FsExport.IO.RO, iomode);
} }
@Test
public void testSecureOption() throws Exception {
FsExport export = _exportFile.getExport("/secure", InetAddress.getByName("192.168.17.1"));
assertTrue("Incorrect secure option", export.isPrivilegedClientPortRequired());
}
@Test
public void testInsecureOption() throws Exception {
FsExport export = _exportFile.getExport("/insecure", InetAddress.getByName("192.168.17.1"));
assertFalse("Incorrect secure option", export.isPrivilegedClientPortRequired());
}
@Test
public void testDefaultSecureOption() throws Exception {
FsExport export = _exportFile.getExport("/secure_default", InetAddress.getByName("192.168.17.1"));
assertFalse("Incorrect default secure option", export.isPrivilegedClientPortRequired());
}
} }
/* /*
* Copyright (c) 2009 - 2019 Deutsches Elektronen-Synchroton, * Copyright (c) 2009 - 2020 Deutsches Elektronen-Synchroton,
* Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY
* *
* This library is free software; you can redistribute it and/or modify * This library is free software; you can redistribute it and/or modify
...@@ -56,7 +56,7 @@ import org.junit.Before; ...@@ -56,7 +56,7 @@ import org.junit.Before;
*/ */
public class PseudoFsTest { public class PseudoFsTest {
private final static InetSocketAddress localAddress = new InetSocketAddress(0); private final static InetSocketAddress localAddress = new InetSocketAddress(31415);
private VirtualFileSystem vfs; private VirtualFileSystem vfs;
private ExportFile mockedExportFile; private ExportFile mockedExportFile;
private FsExport mockedExport; private FsExport mockedExport;
...@@ -699,4 +699,48 @@ public class PseudoFsTest { ...@@ -699,4 +699,48 @@ public class PseudoFsTest {
pseudoFs.setXattr(inode, "xattr1", "value1".getBytes(StandardCharsets.UTF_8), VirtualFileSystem.SetXattrMode.CREATE); pseudoFs.setXattr(inode, "xattr1", "value1".getBytes(StandardCharsets.UTF_8), VirtualFileSystem.SetXattrMode.CREATE);
} }
@Test(expected = AccessException.class)
public void testUnprivilegedClient() throws IOException {
given(mockedTransport.getRemoteSocketAddress()).willReturn(localAddress);
given(mockedAuth.getSubject()).willReturn(Subjects.ROOT);
given(mockedRpc.getTransport()).willReturn(mockedTransport);
given(mockedRpc.getCredential()).willReturn(mockedAuth);
FsExport export = new FsExport.FsExportBuilder()
.rw()
.trusted()
.withoutAcl()
.withSec(FsExport.Sec.NONE)
.withPrivilegedClientPort()
.build("/");
given(mockedExportFile.getExport(fsRoot.exportIndex(), localAddress.getAddress())).willReturn(export);
pseudoFs = new PseudoFs(vfs, mockedRpc, mockedExportFile);
pseudoFs.getattr(fsRoot);
}
@Test
public void testPrivilegedClient() throws IOException {
InetSocketAddress privilegedClient = new InetSocketAddress(314);
given(mockedTransport.getRemoteSocketAddress()).willReturn(privilegedClient);
given(mockedAuth.getSubject()).willReturn(Subjects.ROOT);
given(mockedRpc.getTransport()).willReturn(mockedTransport);
given(mockedRpc.getCredential()).willReturn(mockedAuth);
FsExport export = new FsExport.FsExportBuilder()
.rw()
.trusted()
.withoutAcl()
.withSec(FsExport.Sec.NONE)
.withPrivilegedClientPort()
.build("/");
given(mockedExportFile.getExport(fsRoot.exportIndex(), privilegedClient.getAddress())).willReturn(export);
pseudoFs = new PseudoFs(vfs, mockedRpc, mockedExportFile);
pseudoFs.getattr(fsRoot);
}
} }
...@@ -29,4 +29,7 @@ not/absolute/path ...@@ -29,4 +29,7 @@ not/absolute/path
/export_without_pnfs 192.168.1.1(rw,nopnfs) /export_without_pnfs 192.168.1.1(rw,nopnfs)
/export_default_pnfs 192.168.1.1(rw) /export_default_pnfs 192.168.1.1(rw)
/layouttypes 172.16.1.1(rw,lt=nfsv4_1_files) 172.16.2.1(rw,lt=flex_files:nfsv4_1_files) 172.16.3.1(rw,lt=nfsv4_1_files:flex_files) 172.16.4.1(rw) /layouttypes 172.16.1.1(rw,lt=nfsv4_1_files) 172.16.2.1(rw,lt=flex_files:nfsv4_1_files) 172.16.3.1(rw,lt=nfsv4_1_files:flex_files) 172.16.4.1(rw)
/order_test 192.168.17.0/24(ro) 192.168.17.1(rw) /order_test 192.168.17.0/24(ro) 192.168.17.1(rw)
\ No newline at end of file /secure 192.168.17.0/24(secure)
/insecure 192.168.17.0/24(insecure)
/secure_default 192.168.17.0/24
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment