Skip to content

Commit

Permalink
ftp: add support for (explicit) FTPS doors
Browse files Browse the repository at this point in the history
Motivation:

There are users that wish to use an FTP client and authenticate using
username and password; however, we want those users to send their
credentials encrypted over the network.

Of the two flavours of FTPS, this patch adds support for explicit-FTPS
or FTP(E)S.  This requires the client to explicitly request establishing
a TLS context using a StartTLS-like sequence.

Implicit FTPS is generally considered deprecated, so not supported by
this patch.

Modification:

Update NettyLineBaseDoor to support StartTLS-like protocols.

Introduce a new FTP door type that wraps WeakFtpDoor and adds support
for StartTLS.  Only the AUTH and FEAT commands are supported before the
TLS context is established.

Result:

dCache now supports the standard SSL/TLS protected FTP: FTPS.

Target: master
Requires-notes: yes
Requires-book: yes
Patch: https://rb.dcache.org/r/11608/
Acked-by: Tigran Mkrtchyan
  • Loading branch information
paulmillar committed Apr 1, 2019
1 parent ec34f0c commit fb97274
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 39 deletions.
39 changes: 24 additions & 15 deletions docs/TheBook/src/main/markdown/config-ftp.md
Expand Up @@ -33,15 +33,21 @@ The control channel is the TCP connection established by the client
over which the client issues commands and receives replies indicating
whether those commands were successful.

In general, dCache supports three flavours of control channel:
`plain`, `gsiftp` (also known as GridFTP), and `kerberos`. Each FTP
door supports exactly one of these flavours. These flavours differ in
how the control channel is handled. In plain FTP, the control channel
is unencrypted; in many cases, this is insecure and requires
additional protection. With gsiftp and Kerberos FTP, the control
channel is encrypted, preventing eavesdropping or interfering with
requests. Authentication with gsiftp is based on X.509 credentials,
while Kerberos FTP uses Kerberos.
In general, dCache supports four flavours of control channel: `plain`,
`tls` (also known as FTPS), `gsiftp` (also known as GridFTP), and
`kerberos`. Each FTP door supports exactly one of these flavours.
These flavours differ in how the control channel is handled. In plain
FTP, the control channel is unencrypted; in many cases, this is
insecure and requires additional protection. With tls, gsiftp and
Kerberos FTP, the control channel is encrypted, preventing
eavesdropping or interfering with requests. Authentication with tls
is based on username and password, gsiftp is based on X.509
credentials, while Kerberos FTP uses Kerberos.

Although tls and gsi FTP doors are both X.509 based, they differ in
how the encryption is handled. Support for tls FTP is more common and
is often referred to as FTPS, FTP(E)S, FTPS-explicit or FTPES. Support
for gsi FTP is limited to grid tools.

Limiting access
---------------
Expand Down Expand Up @@ -103,8 +109,8 @@ username (typically 'anonymous'). Although there is no specific
password for these accounts, it is common practice that the client
sends the user's email address as the password, as a courtesy.

dCache supports anonymous FTP for the plain FTP door. This is
disabled by default, but may be enabled using the
dCache supports anonymous FTP for the plain and tls FTP doors. This
is disabled by default, but may be enabled using the
`ftp.enable.anonymous-ftp` configuration property. When enabled,
users may access dCache as user NOBODY; e.g., world-readable files may
be downloaded and world-readable directories may be listed.
Expand All @@ -129,10 +135,11 @@ password is not a valid email address. Note that, Globus transfer
service currently sends "dummy" as the password, which is not a valid
email address.

If the plain ftp door should be used only for anonymous access then
regular username and password access may be disabled by configuring
the `ftp.enable.username-password` property. This is to prevent
normal dCache users from typing in their password unencrypted.
If the plain or tls FTP door should be used only for anonymous access
then regular username and password access may be disabled by
configuring the `ftp.enable.username-password` property. This is
perhaps most useful with plain FTP doors to prevent normal dCache
users from typing in their password unencrypted.


Data transfers
Expand Down Expand Up @@ -260,6 +267,8 @@ without requiring any port configuration:
ftp.authn.protocol = gsi
[<domainName>/nfs]
ftp.authn.protocol = kerberos
[<domainName>/nfs]
ftp.authn.protocol = tls
..


Expand Up @@ -465,7 +465,7 @@ public long getTotal()

public enum ReplyType
{
CLEAR, MIC, ENC, CONF
CLEAR, MIC, ENC, CONF, TLS
}

protected class CommandRequest
Expand Down Expand Up @@ -500,14 +500,14 @@ public CommandRequest(String commandLine, ReplyType replyType, Object commandCon
arg = commandLine.length() > l + 1 ? commandLine.substring(l + 1) : "";
method = _methodDict.get(name);

this.commandLine = name.equals("pass") && !arg.isEmpty()
this.commandLine = (name.equals("adat") || name.equals("pass")) && !arg.isEmpty()
? commandLine.substring(0, 4) + " ..."
: commandLine;

if (replyType == ReplyType.CLEAR) {
commandLineDescription = commandLine;
commandLineDescription = this.commandLine;
} else {
commandLineDescription = replyType.name() + "{" + commandLine + "}";
commandLineDescription = replyType.name() + "{" + this.commandLine + "}";
}
}

Expand Down Expand Up @@ -1890,6 +1890,7 @@ protected void reply(CommandRequest request, String answer)

switch (_isHello ? ReplyType.CLEAR : request.getReplyType()) {
case CLEAR:
case TLS:
println(answer);
break;
case MIC:
Expand Down Expand Up @@ -1927,18 +1928,7 @@ private void logReply(CommandRequest request, String response)
if (request.getReplyType() != ReplyType.CLEAR) {
response = request.getReplyType().name() + "{" + response + "}";
}

String commandLine = request.getCommandLineDescription();

if (request.getName() != null) {
// For some commands we don't want to log the arguments.
String name = request.getName();
if (name.equals("adat") || name.equals("pass")) {
commandLine = name.toUpperCase() + " ...";
}
}

log.addInQuotes("command", commandLine);
log.addInQuotes("command", request.getCommandLineDescription());
}
if (_subject != null && !_subjectLogged) {
logSubject(log, _subject);
Expand Down Expand Up @@ -1971,6 +1961,13 @@ public void ftp_feat(String arg)
{
StringBuilder builder = new StringBuilder();
builder.append("211-OK\r\n");
buildFeatList(builder);
builder.append("211 End");
reply(builder.toString());
}

protected StringBuilder buildFeatList(StringBuilder builder)
{
for (String feature: FEATURES) {
builder.append(' ').append(feature).append("\r\n");
}
Expand All @@ -1988,9 +1985,7 @@ public void ftp_feat(String arg)
builder.append(';');
}
builder.append("\r\n");

builder.append("211 End");
reply(builder.toString());
return builder;
}

public void opts_retr(String opt) throws FTPCommandException
Expand Down
142 changes: 142 additions & 0 deletions modules/dcache-ftp/src/main/java/org/dcache/ftp/door/TlsFtpDoor.java
@@ -0,0 +1,142 @@
/*
* dCache - http://www.dcache.org/
*
* Copyright (C) 2019 Deutsches Elektronen-Synchrotron
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.dcache.ftp.door;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLEngine;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import diskCacheV111.doors.TlsStarter;
import diskCacheV111.util.FsPath;

import dmg.util.CommandExitException;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Objects.requireNonNull;

/**
* Support for ftp(e)s: an FTP door that allows the client to switch the
* control channel to be TLS-encrypted.
*/
public class TlsFtpDoor extends WeakFtpDoorV1 implements TlsStarter
{
private static final Logger LOGGER = LoggerFactory.getLogger(TlsFtpDoor.class);
private Consumer<SSLEngine> startTls;
private final SSLEngine ssl;
private final Set<String> _plaintextCommands = new HashSet<>();
private boolean isChannelSecure;

/**
* Commands that are annotated @Plaintext are allowed to be sent before the
* TLS context is established.
*/
@Retention(RUNTIME)
@Target(METHOD)
@interface Plaintext
{
}

public TlsFtpDoor(SSLEngine ssl, boolean allowUsernamePassword,
Optional<String> anonymousUser, FsPath anonymousRoot,
boolean requireAnonPasswordEmail)
{
super("FTPS", allowUsernamePassword, anonymousUser, anonymousRoot,
requireAnonPasswordEmail);
this.ssl = ssl;

visitFtpCommands((m, cmd) -> {
if (m.getAnnotation(Plaintext.class) != null) {
_plaintextCommands.add(cmd);
}
});
}

@Override
public void setTlsStarter(Consumer<SSLEngine> startTls)
{
this.startTls = requireNonNull(startTls);
}

@Override
public void execute(String command) throws CommandExitException
{
ftpcommand(command, null, isChannelSecure ? ReplyType.TLS : ReplyType.CLEAR);
}


@Override
protected void checkCommandAllowed(CommandRequest command, Object commandContext)
throws FTPCommandException
{
boolean isPlaintextAllowed = _plaintextCommands.contains(command.getName());

checkFTPCommand(isChannelSecure || isPlaintextAllowed,
530, "Command not allowed until TLS is established");

super.checkCommandAllowed(command, commandContext);
}

/* Promote the FEAT command to be available in plain-text. This is so
* clients can see that AUTH is supported and learn which schemes are
* supported. See RFC 4217.
*/
@Plaintext
@Override
public void ftp_feat(String arg)
{
super.ftp_feat(arg);
}

@Override
protected StringBuilder buildFeatList(StringBuilder builder)
{
return super.buildFeatList(builder)
.append(' ').append("AUTH SSL TLS").append("\r\n");
}


@Help("AUTH <SP> <arg> - Initiate secure context negotiation.")
@Plaintext
public void ftp_auth(String arg) throws FTPCommandException
{
LOGGER.info("going to authorize");

/* From RFC 2228 Section 3. New FTP Commands, AUTH:
*
* If the server does not understand the named security
* mechanism, it should respond with reply code 504.
*/
checkFTPCommand(arg.equals("TLS") || arg.equals("SSL"),
504, "Authenticating method not supported");
checkFTPCommand(!isChannelSecure, 534, "TLS context already established");

startTls.accept(ssl);
isChannelSecure = true;
reply("234 Ready for " + arg + " handshake");
}
}

0 comments on commit fb97274

Please sign in to comment.