Merge ~epics-core/epics-base/+git/asLib:as-hostname into ~epics-core/epics-base/+git/epics-base:7.0
- Git
- lp:~epics-core/epics-base/+git/asLib
- as-hostname
- Merge into 7.0
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrew Johnson | Approve | ||
mdavidsaver | Approve | ||
Review via email: mp+358822@code.launchpad.net |
Commit message
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.
Andrew Johnson (anj) wrote : | # |
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:/
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/
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).
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.
mdavidsaver (mdavidsaver) wrote : | # |
Name resolution now fails soft.
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.
mdavidsaver (mdavidsaver) wrote : | # |
Release notes updated.
Andrew Johnson (anj) wrote : | # |
AI on AJ to review soon.
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.
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.
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:/
> Your team EPICS Core Developers is subscribed to branch epics-base:7.0.
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.
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.
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
1 | diff --git a/documentation/RELEASE_NOTES.html b/documentation/RELEASE_NOTES.html |
2 | index df78360..39d101b 100644 |
3 | --- a/documentation/RELEASE_NOTES.html |
4 | +++ b/documentation/RELEASE_NOTES.html |
5 | @@ -30,6 +30,7 @@ release.</p> |
6 | |
7 | --> |
8 | |
9 | +<<<<<<< documentation/RELEASE_NOTES.html |
10 | <h1 align="center">EPICS Release 7.0.2.2</h1> |
11 | |
12 | <h3>Build System changes</h3> |
13 | @@ -127,6 +128,50 @@ than it used to be.</p> |
14 | |
15 | |
16 | <h1 align="center">EPICS Release 7.0.2</h1> |
17 | +======= |
18 | +<h3>Channel Access Security: Check Hostname Against DNS</h3> |
19 | + |
20 | +<p>Host names given in a <tt>HAG</tt> entry of an IOC's Access Security |
21 | +Configuration File (ACF) have to date been compared against the hostname |
22 | +provided by the CA client at connection time, which may or may not be the actual |
23 | +name of that client. This allows rogue clients to pretend to be a different |
24 | +host, and the IOC would believe them.</p> |
25 | + |
26 | +<p>An option is now available to cause an IOC to ask its operating system to |
27 | +look up the IP address of any hostnames listed in its ACF (which will normally |
28 | +be done using the DNS or the <tt>/etc/hosts</tt> file). The IOC will then |
29 | +compare the resulting IP address against the client's actual IP address when |
30 | +checking access permissions at connection time. This name resolution is performed |
31 | +at ACF file load time, which has a few consequences:</p> |
32 | + |
33 | +<ol> |
34 | + |
35 | +<li>If the DNS is slow when the names are resolved this will delay the process |
36 | +of loading the ACF file.</li> |
37 | + |
38 | +<li>If a host name cannot be resolved the IOC will proceed, but this host name |
39 | +will never be matched.</li> |
40 | + |
41 | +<li>Any changes in the hostname to IP address mapping will not be picked up by |
42 | +the IOC unless and until the ACF file gets reloaded.</li> |
43 | + |
44 | +</ol> |
45 | + |
46 | +<p>Optionally, IP addresses may be added instead of, or in addition to, host names |
47 | +in the ACF file.</p> |
48 | + |
49 | +<p>This feature can be enabled before <tt>iocInit</tt> with</p> |
50 | + |
51 | +<blockquote><pre> |
52 | +var("asCheckClientIP",1) |
53 | +</pre></blockquote> |
54 | + |
55 | +<p>or with the VxWorks target shell use</p> |
56 | + |
57 | +<blockquote><pre> |
58 | +asCheckClientIP = 1 |
59 | +</pre></blockquote> |
60 | +>>>>>>> documentation/RELEASE_NOTES.html |
61 | |
62 | <h3>Launchpad Bugs</h3> |
63 | |
64 | diff --git a/modules/database/src/ioc/rsrv/camessage.c b/modules/database/src/ioc/rsrv/camessage.c |
65 | index 72a4b17..f54bb48 100644 |
66 | --- a/modules/database/src/ioc/rsrv/camessage.c |
67 | +++ b/modules/database/src/ioc/rsrv/camessage.c |
68 | @@ -861,6 +861,14 @@ static int host_name_action ( caHdrLargeArray *mp, void *pPayload, |
69 | return RSRV_ERROR; |
70 | } |
71 | |
72 | + /* after all validation */ |
73 | + if(asCheckClientIP) { |
74 | + |
75 | + DLOG (2, ( "CAS: host_name_action for \"%s\" ignores client provided host name\n", |
76 | + client->pHostName ) ); |
77 | + return RSRV_OK; |
78 | + } |
79 | + |
80 | /* |
81 | * user name will not change if there isnt enough memory |
82 | */ |
83 | diff --git a/modules/database/src/ioc/rsrv/caservertask.c b/modules/database/src/ioc/rsrv/caservertask.c |
84 | index f377d83..7a9ae63 100644 |
85 | --- a/modules/database/src/ioc/rsrv/caservertask.c |
86 | +++ b/modules/database/src/ioc/rsrv/caservertask.c |
87 | @@ -1421,6 +1421,20 @@ struct client *create_tcp_client (SOCKET sock , const osiSockAddr *peerAddr) |
88 | } |
89 | |
90 | client->addr = peerAddr->ia; |
91 | + if(asCheckClientIP) { |
92 | + epicsUInt32 ip = ntohl(client->addr.sin_addr.s_addr); |
93 | + client->pHostName = malloc(24); |
94 | + if(!client->pHostName) { |
95 | + destroy_client ( client ); |
96 | + return NULL; |
97 | + } |
98 | + epicsSnprintf(client->pHostName, 24, |
99 | + "%u.%u.%u.%u", |
100 | + (ip>>24)&0xff, |
101 | + (ip>>16)&0xff, |
102 | + (ip>>8)&0xff, |
103 | + (ip>>0)&0xff); |
104 | + } |
105 | |
106 | /* |
107 | * see TCP(4P) this seems to make unsolicited single events much |
108 | diff --git a/modules/database/src/ioc/rsrv/server.h b/modules/database/src/ioc/rsrv/server.h |
109 | index 4d502f7..6392c69 100644 |
110 | --- a/modules/database/src/ioc/rsrv/server.h |
111 | +++ b/modules/database/src/ioc/rsrv/server.h |
112 | @@ -86,7 +86,7 @@ typedef struct client { |
113 | ELLLIST chanList; |
114 | ELLLIST chanPendingUpdateARList; |
115 | ELLLIST putNotifyQue; |
116 | - struct sockaddr_in addr; |
117 | + struct sockaddr_in addr; /* peer address, TCP only */ |
118 | epicsTimeStamp time_at_last_send; |
119 | epicsTimeStamp time_at_last_recv; |
120 | void *evuser; |
121 | diff --git a/modules/libcom/src/as/asLib.h b/modules/libcom/src/as/asLib.h |
122 | index 261e5ed..528ce6e 100644 |
123 | --- a/modules/libcom/src/as/asLib.h |
124 | +++ b/modules/libcom/src/as/asLib.h |
125 | @@ -21,6 +21,11 @@ |
126 | extern "C" { |
127 | #endif |
128 | |
129 | +/* 0 - Use (unverified) client provided host name string. |
130 | + * 1 - Use actual client IP address. HAG() are resolved to IPs at ACF load time. |
131 | + */ |
132 | +epicsShareExtern int asCheckClientIP; |
133 | + |
134 | typedef struct asgMember *ASMEMBERPVT; |
135 | typedef struct asgClient *ASCLIENTPVT; |
136 | typedef int (*ASINPUTFUNCPTR)(char *buf,int max_size); |
137 | @@ -165,8 +170,8 @@ typedef struct uag{ |
138 | } UAG; |
139 | /*Defs for Host Access Groups*/ |
140 | typedef struct{ |
141 | - ELLNODE node; |
142 | - char *host; |
143 | + ELLNODE node; |
144 | + char host[1]; |
145 | } HAGNAME; |
146 | typedef struct hag{ |
147 | ELLNODE node; |
148 | diff --git a/modules/libcom/src/as/asLibRoutines.c b/modules/libcom/src/as/asLibRoutines.c |
149 | index 3f5713e..ab0bf50 100644 |
150 | --- a/modules/libcom/src/as/asLibRoutines.c |
151 | +++ b/modules/libcom/src/as/asLibRoutines.c |
152 | @@ -15,6 +15,8 @@ |
153 | #include <ctype.h> |
154 | |
155 | #define epicsExportSharedSymbols |
156 | +#include "osiSock.h" |
157 | +#include "epicsTypes.h" |
158 | #include "epicsStdio.h" |
159 | #include "dbDefs.h" |
160 | #include "epicsThread.h" |
161 | @@ -27,6 +29,8 @@ |
162 | #include "postfix.h" |
163 | #include "asLib.h" |
164 | |
165 | +int asCheckClientIP; |
166 | + |
167 | static epicsMutexId asLock; |
168 | #define LOCK epicsMutexMustLock(asLock) |
169 | #define UNLOCK epicsMutexUnlock(asLock) |
170 | @@ -1203,14 +1207,38 @@ static HAG *asHagAdd(const char *hagName) |
171 | static long asHagAddHost(HAG *phag,const char *host) |
172 | { |
173 | HAGNAME *phagname; |
174 | - int len, i; |
175 | |
176 | if (!phag) return 0; |
177 | - len = strlen(host); |
178 | - phagname = asCalloc(1, sizeof(HAGNAME) + len + 1); |
179 | - phagname->host = (char *)(phagname + 1); |
180 | - for (i = 0; i < len; i++) { |
181 | - phagname->host[i] = (char)tolower((int)host[i]); |
182 | + if(!asCheckClientIP) { |
183 | + size_t i, len = strlen(host); |
184 | + phagname = asCalloc(1, sizeof(HAGNAME) + len); |
185 | + for (i = 0; i < len; i++) { |
186 | + phagname->host[i] = (char)tolower((int)host[i]); |
187 | + } |
188 | + |
189 | + } else { |
190 | + struct sockaddr_in addr; |
191 | + epicsUInt32 ip; |
192 | + |
193 | + if(aToIPAddr(host, 0, &addr)) { |
194 | + static const char unresolved[] = "unresolved:"; |
195 | + |
196 | + errlogPrintf("ACF: Unable to resolve host '%s'\n", host); |
197 | + |
198 | + phagname = asCalloc(1, sizeof(HAGNAME) + sizeof(unresolved)-1+strlen(host)); |
199 | + strcpy(phagname->host, unresolved); |
200 | + strcat(phagname->host, host); |
201 | + |
202 | + } else { |
203 | + ip = ntohl(addr.sin_addr.s_addr); |
204 | + phagname = asCalloc(1, sizeof(HAGNAME) + 24); |
205 | + epicsSnprintf(phagname->host, 24, |
206 | + "%u.%u.%u.%u", |
207 | + (ip>>24)&0xff, |
208 | + (ip>>16)&0xff, |
209 | + (ip>>8)&0xff, |
210 | + (ip>>0)&0xff); |
211 | + } |
212 | } |
213 | ellAdd(&phag->list, &phagname->node); |
214 | return 0; |
215 | diff --git a/modules/libcom/src/iocsh/libComRegister.c b/modules/libcom/src/iocsh/libComRegister.c |
216 | index b105ea1..8fd5e79 100644 |
217 | --- a/modules/libcom/src/iocsh/libComRegister.c |
218 | +++ b/modules/libcom/src/iocsh/libComRegister.c |
219 | @@ -12,6 +12,7 @@ |
220 | |
221 | #define epicsExportSharedSymbols |
222 | #include "iocsh.h" |
223 | +#include "asLib.h" |
224 | #include "epicsStdioRedirect.h" |
225 | #include "epicsString.h" |
226 | #include "epicsTime.h" |
227 | @@ -393,6 +394,8 @@ static void installLastResortEventProviderCallFunc(const iocshArgBuf *args) |
228 | installLastResortEventProvider(); |
229 | } |
230 | |
231 | +static iocshVarDef asCheckClientIPDef = {"asCheckClientIP", iocshArgInt, 0}; |
232 | + |
233 | void epicsShareAPI libComRegister(void) |
234 | { |
235 | iocshRegister(&dateFuncDef, dateCallFunc); |
236 | @@ -425,4 +428,7 @@ void epicsShareAPI libComRegister(void) |
237 | |
238 | iocshRegister(&generalTimeReportFuncDef,generalTimeReportCallFunc); |
239 | iocshRegister(&installLastResortEventProviderFuncDef, installLastResortEventProviderCallFunc); |
240 | + |
241 | + asCheckClientIPDef.pval = &asCheckClientIP; |
242 | + iocshRegisterVariable(&asCheckClientIPDef); |
243 | } |
244 | diff --git a/modules/libcom/test/aslibtest.c b/modules/libcom/test/aslibtest.c |
245 | index 875aa56..4237faf 100644 |
246 | --- a/modules/libcom/test/aslibtest.c |
247 | +++ b/modules/libcom/test/aslibtest.c |
248 | @@ -81,8 +81,8 @@ static const char hostname_config[] = "" |
249 | |
250 | static void testHostNames(void) |
251 | { |
252 | - |
253 | testDiag("testHostNames()"); |
254 | + asCheckClientIP = 0; |
255 | |
256 | testOk1(asInitMem(hostname_config, NULL)==0); |
257 | |
258 | @@ -102,18 +102,53 @@ static void testHostNames(void) |
259 | testAccess("ro", 0); |
260 | testAccess("rw", 0); |
261 | |
262 | - setHost("nosuchhost"); |
263 | + setHost("guaranteed.invalid."); |
264 | + |
265 | + testAccess("invalid", 0); |
266 | + testAccess("DEFAULT", 0); |
267 | + testAccess("ro", 0); |
268 | + testAccess("rw", 0); |
269 | +} |
270 | + |
271 | +static void testUseIP(void) |
272 | +{ |
273 | + testDiag("testUseIP()"); |
274 | + asCheckClientIP = 1; |
275 | + |
276 | + /* still host names in .acf */ |
277 | + testOk1(asInitMem(hostname_config, NULL)==0); |
278 | + /* now resolved to IPs */ |
279 | + |
280 | + setUser("testing"); |
281 | + setHost("localhost"); /* will not match against resolved IP */ |
282 | + asAsl = 0; |
283 | + |
284 | + testAccess("invalid", 0); |
285 | + testAccess("DEFAULT", 0); |
286 | + testAccess("ro", 0); |
287 | + testAccess("rw", 0); |
288 | + |
289 | + setHost("127.0.0.1"); |
290 | + |
291 | + testAccess("invalid", 0); |
292 | + testAccess("DEFAULT", 0); |
293 | + testAccess("ro", 1); |
294 | + testAccess("rw", 3); |
295 | + |
296 | + setHost("guaranteed.invalid."); |
297 | |
298 | testAccess("invalid", 0); |
299 | testAccess("DEFAULT", 0); |
300 | testAccess("ro", 0); |
301 | testAccess("rw", 0); |
302 | } |
303 | + |
304 | MAIN(aslibtest) |
305 | { |
306 | - testPlan(14); |
307 | + testPlan(27); |
308 | testSyntaxErrors(); |
309 | testHostNames(); |
310 | + testUseIP(); |
311 | errlogFlush(); |
312 | return testDone(); |
313 | } |
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"?).