Merge ~epics-core/epics-base/+git/asLib:as-hostname into ~epics-core/epics-base/+git/epics-base:7.0

Proposed by mdavidsaver
Status: Merged
Approved by: Andrew Johnson
Approved revision: 4dcd6f37c697b1517b5e36ae1b9d130abfcd7699
Merged at revision: c0a04eae219d3f3f8cf9ce9c1507cb31a6149034
Proposed branch: ~epics-core/epics-base/+git/asLib:as-hostname
Merge into: ~epics-core/epics-base/+git/epics-base:7.0
Diff against target: 313 lines (+153/-12) (has conflicts)
8 files modified
documentation/RELEASE_NOTES.html (+45/-0)
modules/database/src/ioc/rsrv/camessage.c (+8/-0)
modules/database/src/ioc/rsrv/caservertask.c (+14/-0)
modules/database/src/ioc/rsrv/server.h (+1/-1)
modules/libcom/src/as/asLib.h (+7/-2)
modules/libcom/src/as/asLibRoutines.c (+34/-6)
modules/libcom/src/iocsh/libComRegister.c (+6/-0)
modules/libcom/test/aslibtest.c (+38/-3)
Conflict in documentation/RELEASE_NOTES.html
Reviewer Review Type Date Requested Status
Andrew Johnson Approve
mdavidsaver Approve
Review via email: mp+358822@code.launchpad.net

Description of the change

Lets give CA Access Security some teeth. At least as far as is possible w/o protocol changes.

Add a flag which replaces use of CA client provided host name strings with the peer IP address, which isn't so trivially forged. This is coupled with a change to asLib to cause a reverse lookup of hostnames found in ACF files. So the "names" stored as all IP addresses. This is done when an ACF file load, so subsequent DNS changes wouldn't be picked up until/unless the ACF file is reloaded.

As a first attempt, the switch to change behavior is a global variable 'asUseIP'. This was quick to implement as I expect ANJ will have other ideas.

To post a comment you must log in.
Revision history for this message
Andrew Johnson (anj) wrote :

Calls for a parallel change to PCAS, once this has been merged.

Keep with a variable for now, consider adding an environment variable for default setting once we've had some experience with using this.

Test what happens if you switch it on/off at runtime.

Documentation – update the chapter of the AppDevGuide, as well as a Release Notes entry ("experimental"?).

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I've pushed a change which addresses reviewer comments. A fresh clone pull it down, but the LP web frontend doesn't show it in the as-hostname branch history. The commit can be directly referenced. Not sure what is going on...

https://git.launchpad.net/~epics-core/epics-base/+git/asLib/commit/?id=73cdea5517d625243ba149abf4a1368fbae8fe81

https://github.com/mdavidsaver/epics-base/commit/73cdea5517d625243ba149abf4a1368fbae8fe81

Revision history for this message
Andrew Johnson (anj) wrote :

Weird, looks like a Launchpad bug. I just changed Launchpad's "default branch" setting for that repo to be refs/heads/as-hostname although doing that didn't trigger any updates. It was set to refs/heads/master but that branch doesn't actually exist, which might be why LP got confused. I'm about to push a change expanding the Release Notes entry in the hopes that a commit to the repo's default branch might trigger Launchpad to wake up; we'll see whether it does shortly.

Question: I think we said that if a host-name cannot be found, this will cause the IOC to start up with all access disabled. Is this the behavior that we actually want? Wouldn't it be better to allow other hosts that can be resolved to still connect? I suspect our operational ASC files have any number of no-longer-valid host names in them, and here the act of removing a long-disused host name from our DNS is something that could our IT group might do at any time, so it shouldn't prevent our control system from starting up (frankly that would be an operational nightmare). If you change this behavior you should also adjust the Release Note to match (new consequence #2).

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> if a host-name cannot be found, this will cause the IOC to start up with all access disabled. Is this the behavior that we actually want?

That's a fair point. As I think about it, there is no reason to fail the whole load if one host doesn't lookup. It would be easy enough to fill in "unresolved" instead of an IP, print a warning, and continue.

(personally, I'd like to eliminate read access restrictions all together)

> something that could our IT group might do at any time

To some extent, this could be mitigated by including both hostname and IP for some important hosts. eg, hosts which can trigger ACF reload.

Ultimately though, turning this feature on implies an acceptance of this risk.

review: Needs Fixing
Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Name resolution now fails soft.

Revision history for this message
Andrew Johnson (anj) wrote :

Since you changed it to fail soft, the release note needs updating to match. The wording of the second list bullet item is now wrong, and the end of the preceding text paragraph might need tweaking as well, not sure about that.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Release notes updated.

review: Approve
Revision history for this message
Andrew Johnson (anj) wrote :

AI on AJ to review soon.

Revision history for this message
Bruce Hill (bhill) wrote :

Successfully tested this patch w/ pvAccess gwdev branch and pva2pva d7314ea from mdavidsaver.

Tested IOC and ca-gateway instances w/ asCheckClientIP=0 and asCheckClientIP=1.
ca-gateway was based on R2-1-1-0 with inline code to set asCheckClientIP.

No changes were needed to pcas version 4.13.2.

With asCheckClientIP=0, a hacked caput can bypass ASG RULES using hostnames.
With asCheckClientIP=1, caput can only spoof username

For pvput and IOC testing:
With asCheckClientIP=0, All ASG RULES using HAG deny write access.
With asCheckClientIP=1, pvput works same as CA for all UAG and HAG based ASG RULES. (Didn't test variables in RULES or spoofing username in pvput.)

Also tested w/ new p4p gateway as a client while p4p gateway is running it's own access security.

Revision history for this message
Bruce Hill (bhill) wrote :

Re the initial default for asCheckClientIP, I recommend it be set to 1.

Defaulting to 0 only helps those who run w/o DNS yet also want to run EPICS 7 IOCs
but continue to use CA instead of PVA to modify their access secured PVs.
They also have an easy workaround by using IP addr in their acf files.

Meanwhile, every site who tries to support access security w/ EPICS 7 could easily
miss the need to set asCheckClientIP in order to make pvAccess support HAG based rules.

It also has the benefit of closing the CA spoofed hostname vulnerability.

Revision history for this message
Ernest Williams (ernesto-slac) wrote :

I agree

Sent from my iPhone

> On Aug 13, 2019, at 11:10 PM, Bruce Hill via Core-talk <email address hidden> wrote:
>
> Re the initial default for asCheckClientIP, I recommend it be set to 1.
>
> Defaulting to 0 only helps those who run w/o DNS yet also want to run EPICS 7 IOCs
> but continue to use CA instead of PVA to modify their access secured PVs.
> They also have an easy workaround by using IP addr in their acf files.
>
> Meanwhile, every site who tries to support access security w/ EPICS 7 could easily
> miss the need to set asCheckClientIP in order to make pvAccess support HAG based rules.
>
> It also has the benefit of closing the CA spoofed hostname vulnerability.
> --
> https://code.launchpad.net/~epics-core/epics-base/+git/asLib/+merge/358822
> Your team EPICS Core Developers is subscribed to branch epics-base:7.0.

Revision history for this message
Andrew Johnson (anj) wrote :

Minor Q: An IPv4 address represented as an ASCII dotted-quad needs no more than 16 chars including the string terminator. Why are you (consistently) allocating 24? With a port number it would need 22 chars, and 24 chars would be too short for an IPv6 address. Just wondering...

On the default value of asCheckClientIP, my concern with making it 1 is that this could change the behavior for some existing working systems. If the name for a client listed in an IOC's ASCF is the canonical hostname for the client machine but the network path to that IOC comes through a secondary network interface with a different DNS name, a previously working ASCF rule will now deny access to that client. Admittedly this is an unusual situation but it's by no means impossible, and the code currently has no way to detect when it happens (it could, but Michael has better things to do with his time than implement that). If we do change the default the release notes would need changing to match, and they should probably have something in bold or red to get people to notice.

Approved.

review: Approve
Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I'm planning to go ahead and merge with a default of asCheckClientIP=0. Changing this can be a separate discussion.

> Why are you (consistently) allocating 24?

Force of habit? I can't recall.

> default value of asCheckClientIP

Can we agree that the desired final solution is defaulting to asCheckClientIP=1 ?

> Admittedly this is an unusual situation

Yup, given the number of sites not using ACF at all.

Is it worth my arguing that any site bothering with access control
would likely welcome stronger authentication?

> canonical hostname ... a secondary network interface with a different DNS name
> ... it could ...

How could this be done? I can't think of a way.

> has better things to do

Granted. It would still be nice to document a possible course of action in case someone is motivated.

Revision history for this message
Andrew Johnson (anj) wrote :

> Can we agree that the desired final solution is defaulting to asCheckClientIP=1 ?

Yes, I'd just like there to be something like a couple of releases where people can try it out before we flip the default.

> Is it worth my arguing that any site bothering with access control would likely welcome stronger authentication?

We use AS here to prevent people from fiddling with PVs that only the experts should be modifying, such as some of our RF system IOCs where a mis-placed put might be able to destroy one of our dwindling supply of RF cavities. I'm totally guessing whether that might be possible, but I do know that our RF IOCs do use it quite extensively, as do our beamline gateways. I agree that stronger authentication is better, and that this will give us stronger authentication.

> How could this be done?

Instead of making this a binary switch, store both the HAG name and the DNS result. Implement the host comparisons using both methods simultaneously and cross-check the results. When the by-name comparison says Match but the by-DNS says No there's either someone trying to impersonate a different client machine or this could be a multiple-interface name issue. You aren't modifying the actual host comparison code at all here though, so doing this would need more extensive changes to asLib.

Similarly larger changes would be needed to allow the use of IP address ranges in the ASCF (e.g. 192.168.12.16/28 say) which I could see as being useful to some (and would need more than 16 chars to store).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/documentation/RELEASE_NOTES.html b/documentation/RELEASE_NOTES.html
index df78360..39d101b 100644
--- a/documentation/RELEASE_NOTES.html
+++ b/documentation/RELEASE_NOTES.html
@@ -30,6 +30,7 @@ release.</p>
3030
31-->31-->
3232
33<<<<<<< documentation/RELEASE_NOTES.html
33<h1 align="center">EPICS Release 7.0.2.2</h1>34<h1 align="center">EPICS Release 7.0.2.2</h1>
3435
35<h3>Build System changes</h3>36<h3>Build System changes</h3>
@@ -127,6 +128,50 @@ than it used to be.</p>
127128
128129
129<h1 align="center">EPICS Release 7.0.2</h1>130<h1 align="center">EPICS Release 7.0.2</h1>
131=======
132<h3>Channel Access Security: Check Hostname Against DNS</h3>
133
134<p>Host names given in a <tt>HAG</tt> entry of an IOC's Access Security
135Configuration File (ACF) have to date been compared against the hostname
136provided by the CA client at connection time, which may or may not be the actual
137name of that client. This allows rogue clients to pretend to be a different
138host, and the IOC would believe them.</p>
139
140<p>An option is now available to cause an IOC to ask its operating system to
141look up the IP address of any hostnames listed in its ACF (which will normally
142be done using the DNS or the <tt>/etc/hosts</tt> file). The IOC will then
143compare the resulting IP address against the client's actual IP address when
144checking access permissions at connection time. This name resolution is performed
145at ACF file load time, which has a few consequences:</p>
146
147<ol>
148
149<li>If the DNS is slow when the names are resolved this will delay the process
150of loading the ACF file.</li>
151
152<li>If a host name cannot be resolved the IOC will proceed, but this host name
153will never be matched.</li>
154
155<li>Any changes in the hostname to IP address mapping will not be picked up by
156the IOC unless and until the ACF file gets reloaded.</li>
157
158</ol>
159
160<p>Optionally, IP addresses may be added instead of, or in addition to, host names
161in the ACF file.</p>
162
163<p>This feature can be enabled before <tt>iocInit</tt> with</p>
164
165<blockquote><pre>
166var("asCheckClientIP",1)
167</pre></blockquote>
168
169<p>or with the VxWorks target shell use</p>
170
171<blockquote><pre>
172asCheckClientIP = 1
173</pre></blockquote>
174>>>>>>> documentation/RELEASE_NOTES.html
130175
131<h3>Launchpad Bugs</h3>176<h3>Launchpad Bugs</h3>
132177
diff --git a/modules/database/src/ioc/rsrv/camessage.c b/modules/database/src/ioc/rsrv/camessage.c
index 72a4b17..f54bb48 100644
--- a/modules/database/src/ioc/rsrv/camessage.c
+++ b/modules/database/src/ioc/rsrv/camessage.c
@@ -861,6 +861,14 @@ static int host_name_action ( caHdrLargeArray *mp, void *pPayload,
861 return RSRV_ERROR;861 return RSRV_ERROR;
862 }862 }
863863
864 /* after all validation */
865 if(asCheckClientIP) {
866
867 DLOG (2, ( "CAS: host_name_action for \"%s\" ignores client provided host name\n",
868 client->pHostName ) );
869 return RSRV_OK;
870 }
871
864 /*872 /*
865 * user name will not change if there isnt enough memory873 * user name will not change if there isnt enough memory
866 */874 */
diff --git a/modules/database/src/ioc/rsrv/caservertask.c b/modules/database/src/ioc/rsrv/caservertask.c
index f377d83..7a9ae63 100644
--- a/modules/database/src/ioc/rsrv/caservertask.c
+++ b/modules/database/src/ioc/rsrv/caservertask.c
@@ -1421,6 +1421,20 @@ struct client *create_tcp_client (SOCKET sock , const osiSockAddr *peerAddr)
1421 }1421 }
14221422
1423 client->addr = peerAddr->ia;1423 client->addr = peerAddr->ia;
1424 if(asCheckClientIP) {
1425 epicsUInt32 ip = ntohl(client->addr.sin_addr.s_addr);
1426 client->pHostName = malloc(24);
1427 if(!client->pHostName) {
1428 destroy_client ( client );
1429 return NULL;
1430 }
1431 epicsSnprintf(client->pHostName, 24,
1432 "%u.%u.%u.%u",
1433 (ip>>24)&0xff,
1434 (ip>>16)&0xff,
1435 (ip>>8)&0xff,
1436 (ip>>0)&0xff);
1437 }
14241438
1425 /*1439 /*
1426 * see TCP(4P) this seems to make unsolicited single events much1440 * see TCP(4P) this seems to make unsolicited single events much
diff --git a/modules/database/src/ioc/rsrv/server.h b/modules/database/src/ioc/rsrv/server.h
index 4d502f7..6392c69 100644
--- a/modules/database/src/ioc/rsrv/server.h
+++ b/modules/database/src/ioc/rsrv/server.h
@@ -86,7 +86,7 @@ typedef struct client {
86 ELLLIST chanList;86 ELLLIST chanList;
87 ELLLIST chanPendingUpdateARList;87 ELLLIST chanPendingUpdateARList;
88 ELLLIST putNotifyQue;88 ELLLIST putNotifyQue;
89 struct sockaddr_in addr;89 struct sockaddr_in addr; /* peer address, TCP only */
90 epicsTimeStamp time_at_last_send;90 epicsTimeStamp time_at_last_send;
91 epicsTimeStamp time_at_last_recv;91 epicsTimeStamp time_at_last_recv;
92 void *evuser;92 void *evuser;
diff --git a/modules/libcom/src/as/asLib.h b/modules/libcom/src/as/asLib.h
index 261e5ed..528ce6e 100644
--- a/modules/libcom/src/as/asLib.h
+++ b/modules/libcom/src/as/asLib.h
@@ -21,6 +21,11 @@
21extern "C" {21extern "C" {
22#endif22#endif
2323
24/* 0 - Use (unverified) client provided host name string.
25 * 1 - Use actual client IP address. HAG() are resolved to IPs at ACF load time.
26 */
27epicsShareExtern int asCheckClientIP;
28
24typedef struct asgMember *ASMEMBERPVT;29typedef struct asgMember *ASMEMBERPVT;
25typedef struct asgClient *ASCLIENTPVT;30typedef struct asgClient *ASCLIENTPVT;
26typedef int (*ASINPUTFUNCPTR)(char *buf,int max_size);31typedef int (*ASINPUTFUNCPTR)(char *buf,int max_size);
@@ -165,8 +170,8 @@ typedef struct uag{
165} UAG;170} UAG;
166/*Defs for Host Access Groups*/171/*Defs for Host Access Groups*/
167typedef struct{172typedef struct{
168 ELLNODE node;173 ELLNODE node;
169 char *host;174 char host[1];
170} HAGNAME;175} HAGNAME;
171typedef struct hag{176typedef struct hag{
172 ELLNODE node;177 ELLNODE node;
diff --git a/modules/libcom/src/as/asLibRoutines.c b/modules/libcom/src/as/asLibRoutines.c
index 3f5713e..ab0bf50 100644
--- a/modules/libcom/src/as/asLibRoutines.c
+++ b/modules/libcom/src/as/asLibRoutines.c
@@ -15,6 +15,8 @@
15#include <ctype.h>15#include <ctype.h>
1616
17#define epicsExportSharedSymbols17#define epicsExportSharedSymbols
18#include "osiSock.h"
19#include "epicsTypes.h"
18#include "epicsStdio.h"20#include "epicsStdio.h"
19#include "dbDefs.h"21#include "dbDefs.h"
20#include "epicsThread.h"22#include "epicsThread.h"
@@ -27,6 +29,8 @@
27#include "postfix.h"29#include "postfix.h"
28#include "asLib.h"30#include "asLib.h"
2931
32int asCheckClientIP;
33
30static epicsMutexId asLock;34static epicsMutexId asLock;
31#define LOCK epicsMutexMustLock(asLock)35#define LOCK epicsMutexMustLock(asLock)
32#define UNLOCK epicsMutexUnlock(asLock)36#define UNLOCK epicsMutexUnlock(asLock)
@@ -1203,14 +1207,38 @@ static HAG *asHagAdd(const char *hagName)
1203static long asHagAddHost(HAG *phag,const char *host)1207static long asHagAddHost(HAG *phag,const char *host)
1204{1208{
1205 HAGNAME *phagname;1209 HAGNAME *phagname;
1206 int len, i;
12071210
1208 if (!phag) return 0;1211 if (!phag) return 0;
1209 len = strlen(host);1212 if(!asCheckClientIP) {
1210 phagname = asCalloc(1, sizeof(HAGNAME) + len + 1);1213 size_t i, len = strlen(host);
1211 phagname->host = (char *)(phagname + 1);1214 phagname = asCalloc(1, sizeof(HAGNAME) + len);
1212 for (i = 0; i < len; i++) {1215 for (i = 0; i < len; i++) {
1213 phagname->host[i] = (char)tolower((int)host[i]);1216 phagname->host[i] = (char)tolower((int)host[i]);
1217 }
1218
1219 } else {
1220 struct sockaddr_in addr;
1221 epicsUInt32 ip;
1222
1223 if(aToIPAddr(host, 0, &addr)) {
1224 static const char unresolved[] = "unresolved:";
1225
1226 errlogPrintf("ACF: Unable to resolve host '%s'\n", host);
1227
1228 phagname = asCalloc(1, sizeof(HAGNAME) + sizeof(unresolved)-1+strlen(host));
1229 strcpy(phagname->host, unresolved);
1230 strcat(phagname->host, host);
1231
1232 } else {
1233 ip = ntohl(addr.sin_addr.s_addr);
1234 phagname = asCalloc(1, sizeof(HAGNAME) + 24);
1235 epicsSnprintf(phagname->host, 24,
1236 "%u.%u.%u.%u",
1237 (ip>>24)&0xff,
1238 (ip>>16)&0xff,
1239 (ip>>8)&0xff,
1240 (ip>>0)&0xff);
1241 }
1214 }1242 }
1215 ellAdd(&phag->list, &phagname->node);1243 ellAdd(&phag->list, &phagname->node);
1216 return 0;1244 return 0;
diff --git a/modules/libcom/src/iocsh/libComRegister.c b/modules/libcom/src/iocsh/libComRegister.c
index b105ea1..8fd5e79 100644
--- a/modules/libcom/src/iocsh/libComRegister.c
+++ b/modules/libcom/src/iocsh/libComRegister.c
@@ -12,6 +12,7 @@
1212
13#define epicsExportSharedSymbols13#define epicsExportSharedSymbols
14#include "iocsh.h"14#include "iocsh.h"
15#include "asLib.h"
15#include "epicsStdioRedirect.h"16#include "epicsStdioRedirect.h"
16#include "epicsString.h"17#include "epicsString.h"
17#include "epicsTime.h"18#include "epicsTime.h"
@@ -393,6 +394,8 @@ static void installLastResortEventProviderCallFunc(const iocshArgBuf *args)
393 installLastResortEventProvider();394 installLastResortEventProvider();
394}395}
395396
397static iocshVarDef asCheckClientIPDef = {"asCheckClientIP", iocshArgInt, 0};
398
396void epicsShareAPI libComRegister(void)399void epicsShareAPI libComRegister(void)
397{400{
398 iocshRegister(&dateFuncDef, dateCallFunc);401 iocshRegister(&dateFuncDef, dateCallFunc);
@@ -425,4 +428,7 @@ void epicsShareAPI libComRegister(void)
425 428
426 iocshRegister(&generalTimeReportFuncDef,generalTimeReportCallFunc);429 iocshRegister(&generalTimeReportFuncDef,generalTimeReportCallFunc);
427 iocshRegister(&installLastResortEventProviderFuncDef, installLastResortEventProviderCallFunc);430 iocshRegister(&installLastResortEventProviderFuncDef, installLastResortEventProviderCallFunc);
431
432 asCheckClientIPDef.pval = &asCheckClientIP;
433 iocshRegisterVariable(&asCheckClientIPDef);
428}434}
diff --git a/modules/libcom/test/aslibtest.c b/modules/libcom/test/aslibtest.c
index 875aa56..4237faf 100644
--- a/modules/libcom/test/aslibtest.c
+++ b/modules/libcom/test/aslibtest.c
@@ -81,8 +81,8 @@ static const char hostname_config[] = ""
8181
82static void testHostNames(void)82static void testHostNames(void)
83{83{
84
85 testDiag("testHostNames()");84 testDiag("testHostNames()");
85 asCheckClientIP = 0;
8686
87 testOk1(asInitMem(hostname_config, NULL)==0);87 testOk1(asInitMem(hostname_config, NULL)==0);
8888
@@ -102,18 +102,53 @@ static void testHostNames(void)
102 testAccess("ro", 0);102 testAccess("ro", 0);
103 testAccess("rw", 0);103 testAccess("rw", 0);
104104
105 setHost("nosuchhost");105 setHost("guaranteed.invalid.");
106
107 testAccess("invalid", 0);
108 testAccess("DEFAULT", 0);
109 testAccess("ro", 0);
110 testAccess("rw", 0);
111}
112
113static void testUseIP(void)
114{
115 testDiag("testUseIP()");
116 asCheckClientIP = 1;
117
118 /* still host names in .acf */
119 testOk1(asInitMem(hostname_config, NULL)==0);
120 /* now resolved to IPs */
121
122 setUser("testing");
123 setHost("localhost"); /* will not match against resolved IP */
124 asAsl = 0;
125
126 testAccess("invalid", 0);
127 testAccess("DEFAULT", 0);
128 testAccess("ro", 0);
129 testAccess("rw", 0);
130
131 setHost("127.0.0.1");
132
133 testAccess("invalid", 0);
134 testAccess("DEFAULT", 0);
135 testAccess("ro", 1);
136 testAccess("rw", 3);
137
138 setHost("guaranteed.invalid.");
106139
107 testAccess("invalid", 0);140 testAccess("invalid", 0);
108 testAccess("DEFAULT", 0);141 testAccess("DEFAULT", 0);
109 testAccess("ro", 0);142 testAccess("ro", 0);
110 testAccess("rw", 0);143 testAccess("rw", 0);
111}144}
145
112MAIN(aslibtest)146MAIN(aslibtest)
113{147{
114 testPlan(14);148 testPlan(27);
115 testSyntaxErrors();149 testSyntaxErrors();
116 testHostNames();150 testHostNames();
151 testUseIP();
117 errlogFlush();152 errlogFlush();
118 return testDone();153 return testDone();
119}154}

Subscribers

People subscribed via source and target branches