-
Notifications
You must be signed in to change notification settings - Fork 426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added automatic detection of REALM in SPN needed for Cross Domain authentication. #40
Conversation
Note: Compatibility with IBM JVM could be trivially implemented using: com.ibm.security.jgss.mech.krb5.Krb5RealmUtil.mapHostToRealm( String ) (But I have not the interest nor the IBM JVM to test) |
Note: this patch only work with realms having the same name (uppercased) than the DNS name of machine as I did not find a way to find a easy way to bind a DNS name to a realm. (It is possible to have different configurations in krb5.conf in theory) But this is not a real issue with most configurations (and Active Directory enforces this constraint) The patch has been tested on Oracle and OpenJDk JVMs with Heimdall, MIT Kerberos and Windows implemetation on the following OS:
|
@pierresouchay Thank you for submitting PR. Have you tried adding serverSPN connection property? which in your example in issue #36 should be explicitly written as serverSpn=MSSQLSvc/sqlserver:PORT@MYOTHERTRUSTEDREALM.COM We tried testing your pull request in our test lab, and without specifying a serverSPN it is failing in a cross realm authentication scenario and was unable to get the correct realm. Thank you. |
Hello @v-afrafi Yes, we do not use serverSpn, we call directly setSpn() on the connection, but this code is a port of code we are using in one of our libraries to cross authenticate accross domains. This code works with a /etc/krb5.conf properly configured (or c:\windows\krb5.ini under Windows). In order to make it works, it requires: In the [domain_realm] sections an entry like:
(It can also work if .TXT entries are properly set up in DNS) In my example, I authenticate using a keytab call svc-myservice@MYREALM.com and I have a $HOME/.java.login.config file with the following data:
It works well in our environments (we use up to 6 cross domains realms) with Windows, Linux and Mac Fell free to post your /etc/krb5.conf configuration and your jaas login file |
Note: on our side we cannot set the Spn for historical reasons in the jdbc URL and sometimes, we used to set IPs in the jdbc URL, that is why DNS canonicalization is useful. This code is currently working outside the driver in our code (we use a data source over the SQL server datasource) and we call setSpn computed in the same way as this PR on the datasource of your driver. But I have tested it successfully with this PR applied. The test to ensure everything works: Create a cross domain SQL connection that works with setSpn() then, use my patch, remove the setSpn() and it still works. Also works with an IP instead of FQN DNS name for the server as long as reverse PTR for SQL server is properly set up. |
@pierresouchay Thank you very much for the information you provided. This is the config file we are using: When we don't set the spn at all, what we see is generated as enrichedspn is MSSQLSvc/host.example.com@host.example.com.REALMNAME, however it should be |
Are you sure you are using the right code ? The enriched Spn is built with this code: StringBuilder sb = new StringBuilder("MSSQLSvc/"); sb.append(dnsName).append(":").append(portOrInstance).append("@").append(realm.toUpperCase(Locale.ENGLISH)) So it should have at least one ":" |
hi @pierresouchay, |
int index = 0; | ||
while (index != -1 && index < hostname.length() - 2) { | ||
String realm = hostname.substring(index + 1); | ||
if (realmValidator.isRealmValid(realm)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue in our test lab is that it when it checks for the kdclist in the getRealmValidator
method, it obtains the default kdc instead of remote kdc resulting in producing the following
spn as targetServer.myRealm.com:portNumber@TARGETSERVER.MYREALM.COM:portNumber.OTHERREALM.COM
however, if we add a check to make sure it is not picking up the default kdc it will result in the following spn: targetServer.myRealm.com:portNumber@OTHERREALM.COM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below you'll find changes that we made that fixed this issue on our end.
@pierresouchay , can you confirm that these changes don't cause any regression within your tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, tested on my side, but it does not change anything. The result is still the same, it works.
However, I don't understand your patch :
In realm validator, we are supposed to validate a sub part of the hostname until we find a valid realm, but you propose to test against the default realm each time after testing against the realm we are currently testing ?
This test could be DONE outside the realm validator and if successful, simply return a always false validator.
In order to investigate this more closely, I have this test example:
SQLServerDataSource msSQLdataSource = new SQLServerDataSource();
msSQLdataSource.setURL("jdbc:sqlserver://"+ipOrHostname+":1033;Database=MyDatabase;integratedSecurity=true;authenticationScheme=JavaKerberos");
msSQLdataSource.getConnection().isValid(5);
System.out.println("is ok")
We have 2 DC with realms NY8.AD.MYCOMPANY.COM and PAR.AD.MYCOMPANY.COM
Test with IP address
The result (with a few debug messages) is the following with ipOrHostname = 10.50.4.6
realmValidator: Checking realm=0.50.4.6 ; default_realm=NY8.AD.MYCOMPANY.COM
realmValidator: Checking realm=50.4.6 ; default_realm=NY8.AD.MYCOMPANY.COM
realmValidator: Checking realm=4.6 ; default_realm=NY8.AD.MYCOMPANY.COM
REVERSE DNS FOUND: sql07.par.ad.mycompany.com
realmValidator: Checking realm=sql07.par.ad.mycompany.com ; default_realm=NY8.AD.MYCOMPANY.COM
realmValidator: Checking realm=par.ad.mycompany.com ; default_realm=NY8.AD.MYCOMPANY.COM
SPN before enrichment: MSSQLSvc/10.50.4.6:1033 ; AFTER enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033@PAR.AD.MYCOMPANY.COM
is ok
I saw incidentally during this test that we should not add +1 at line 410 when index is 0... it may cause issue if the SQL server is on the Active Directory machine (Is it you case btw?)
Test with FQDN
The same example with the FQDN name of machine (not its IP address): sql07.par.ad.mycompany.com
Checking realm=sql07.par.ad.mycompany.com ; default_realm=NY8.AD.MYCOMPANY.COM
Checking realm=par.ad.mycompany.com ; default_realm=NY8.AD.MYCOMPANY.COM
SPN before enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033 ; AFTER enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033@PAR.AD.MYCOMPANY.COM
Test with FQDN in same realm
Same 2 examples, but I changed the default realm and my identity to use the same realm as the MS SQL server (and an identity on the same realm):
Checking realm=0.50.4.6 ; default_realm=PAR.AD.MYCOMPANY.COM
Checking realm=50.4.6 ; default_realm=PAR.AD.MYCOMPANY.COM
Checking realm=4.6 ; default_realm=PAR.AD.MYCOMPANY.COM
Checking realm=sql07-par.par.ad.mycompany.com ; default_realm=PAR.AD.MYCOMPANY.COM
Checking realm=par.ad.mycompany.com ; default_realm=PAR.AD.MYCOMPANY.COM
SPN before enrichment: MSSQLSvc/10.50.4.6:1033 ; AFTER enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033@PAR.AD.MYCOMPANY.COM
is ok
Checking realm=ql07-par.mycompany.com ; default_realm=PAR.AD.MYCOMPANY.COM
Checking realm=par.ad.mycompany.com ; default_realm=PAR.AD.MYCOMPANY.COM
SPN before enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033 ; AFTER enrichment: MSSQLSvc/sql07.par.ad.mycompany.com:1033@PAR.AD.MYCOMPANY.COM
is ok
Possible explanation of the behaviour you have in your lab
I don't understand the result. Factually, there is a bug in my implementation in the case where the SQL Server is on the same machine as the realm since it will cut the first char of the domain name. This bug may trigger something tricky if the domain of the SQL Server you are trying to reach is part of a wider realm however.
I imagine you have a domain such as :
DEV.MSFT.COM
and you create another realm with trust called:
MYLAB.DEV.MSFT.COM
and you install an SQL server on it and use mylab.dev.msft.com as server name.
In that case, at in the realm validator, it won't check mylab.dev.msft.com but will check ylab.dev.microsoft.com instead -> no domain found and then will find dev.msft.com to be the correct domain -> realm DEV.MSFT.COM is correct -> it would explain the bug.
Am I correct ?
In that case, the fix is trivial, at line 410, replace
String realm = hostname.substring(index + 1);
by
String realm = hostname.substring(index);
and at line 414, replace:
index = hostname.indexOf(".", index + 1);
by
index = hostname.indexOf(".", index + 1);
if (index != -1)
index++;
If it solves your issue, that's cool, in other case, I still don't understand the issue and I don't get your proposal for patch.
I will soon update the MR with my change.
Regards
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@v-ahibr Ok, I updated the MR and rebased.
I included the bug with index, meaning that if the SQL server is on the same machine as the KDC it should work now.
Does it solves your issue ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@v-ahibr about my previous comment, was I correct about having the SQL server instance on same node as the KDC (see my comment with MYLAB.DEV.MSFT.COM and DEV.MSFT.COM)?
Class<?> clz = Class.forName("sun.security.krb5.Config"); | ||
Method getInstance = clz.getMethod("getInstance", new Class[0]); | ||
final Method getKDCList = clz.getMethod("getKDCList", new Class[] { String.class }); | ||
final Object instance = getInstance.invoke(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changes made to test if RealmValidator is validating with default realm instead of realm we are attempting to access:
final Method getDefaultRealm = clz.getMethod("getDefaultRealm", new Class[0]);
final Object realmDefault = getDefaultRealm.invoke(instance);
@Override | ||
public boolean isRealmValid(String realm) { | ||
try { | ||
Object ret = getKDCList.invoke(instance, realm); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changes made to test if RealmValidator is validating with default realm instead of realm we are attempting to access:
Object defRet = getKDCList.invoke(instance, realmDefault);
if(ret.toString().equalsIgnoreCase(defRet.toString())) {
return false;
}
Hi @pierresouchay. Do you have any updates on this pull request? |
Hello @v-afrafi Actually, I already updated my MR long time ago, see: #40 (comment) I did not understand why the change proposed by @v-ahibr could work. I proposed an explanation and updated my patch. Are my assumptions correct (See my comments on Jan, 5)... ? |
Thanks @pierresouchay for clarification. Still with the updated patch, it is not working. Running a test from host-01.another.ad and connecting to a sql server on: sql-01.example.ad When no spn is set: when serverSpn is set incomplete (without @) I can see your patch is working in your setup. Where are your kdcs located? |
Hi @ v-afrafi, Thank you for the quick answer. My ConfigurationOur configuration is :
Client is located in PAR, has a defaut realm set to PAR.AD... and that's all. KDC list is retrieved using DNS SRV records. (And all our DNS reverse are properly set) My AnalysisAll of this is weird. To me, it means that getKDCList() returns a list when called with sql-01.example.ad in your case, while it should return it only when called with example.ad. While looking at the code of JDK, I found this:
As you can see, implementation greatly differ between JDK6 and JDK7+ ... especially on Windows where some dirty hacks are performed with env variable LOGONSERVER (BTW, are you testing on JDK6?) JDK7+ looks more promising (most notably with getRealmFromDNS() calls within this method) - which is probably why it succeed with my tested configurations. In any case, there are lots of ways for getKDCList() to return a list of KDC even when argument is not a realm:
So, in my conclusion:
So, in my understanding, it works in the following cases :
What about your lab - why is it not working ?I suspect you are running a JVM 8 on Windows... I am right? In that case, it probably fallback to to LOGONSERVER in any case. All of this is really scary. This method cannot be used as it is. My PropositionsStep 1: Decide whether getKDCList() is reliable or notWe can check easily whether getKDCList() return some stupid answers (Windows or JDK6 or java.security.krb5.kdc java property set) by prepending some random hostname as a first check. Which means that in your case, at startup, before even trying to resolve the realm. In your case, it would mean: That would at least disable the feature where my patch would do more harm than good. But on systems where it works reliably (Linux/Mac with JVM 7+ and ([realms] sections properly filled OR DNS lookups kdc/realms activated) OR Windows with JVM7 and DNS well configured). I like the getKDCList() function because it allows the user to use either DNS or specify by himself the REALMS and kdc in the [realms] section of krb5.conf/ini, so I think not using it is not an option. Possible solution 1If getKDCList() is reliable, use it, otherwise, switch to dns resolution of realms. This solution has the advantage of working with other JVMs as the one from Oracle as well (such as IBM). Might be a good compromise. Possible solution 2if getKDCList() is reliable, use it, otherwise, give up and do not enrich with the realm. Possible solution 3if getKDCList() is reliable, use it, otherwise, canonicalize host name and if it works, remove the first part of hostname canonicalized and assume the remaining part is a valid realm name. (so, in you case, if you would use a CNAME such as sql01.msft.com, it would resolve to sql01.example.ad and assume that EXAMPLE.AD is the realm name. Of course, all of this works well if reverse DNS are well configured and if canonical name DOES contains the REALM name, in any case if canonical form of hostname is not the REALM, it would infer a false REALM and fail. What do you think ? Do you want to to propose a modified PR with solution 1? |
Hi @pierresouchay . Thank you for the thorough description! Just wanted to give an update that I tried your patch in Ubuntu and it is working both in the scenarios of not giving a serverSpn name or giving an IP address! |
hi @v-afrafi , Ok, we are making progress then :) I actually made it work on Windows using a c:\windows\krb5.ini with all the realms properly set up + options to use DNS. What do you think about my possible solutions ? Actually, I implemented the first solution. I am confident it will work on Windows as well as with other JVMs this time :) Regards |
…hentication. The original driver only computes the SPN without its REALM. That is why the driver fails in a cross-domain authentication (ex: user@REALM1 try to log on server@REALM2 while REALM2 and REALM1 are in trust). This commit solves this by trying to compute the REALM when REALM has not been provided in the SPN. Which includes both generated SPN and User-Provided SPN. It also enable Kerberos authentication when only IP is provided as long as reverse DNS are present since when SPN is provided, and REALM lookup did fail, it will also try with canonical name and if it works, override the hostname in SPN (feature not activated when user did provide an SPN) Fixed issue when SQL Server is the same machine as kdc
Codecov Report
@@ Coverage Diff @@
## dev #40 +/- ##
============================================
+ Coverage 29.77% 33.38% +3.61%
- Complexity 1251 1493 +242
============================================
Files 97 101 +4
Lines 23303 23573 +270
Branches 3871 3874 +3
============================================
+ Hits 6939 7871 +932
+ Misses 15014 14140 -874
- Partials 1350 1562 +212
Continue to review full report at Codecov.
|
a9a961c
to
bbe8285
Compare
@v-nisidh Great, last amended commits were just cleanups to have cleaner patch |
Hello @v-afrafi and @v-nisidh Any news ? Are you happy with the current state of the patch? Regards |
Hi @pierresouchay. I can confirm with not using setspn I see your changes working on windows too! We consider this as a longer term goal and we still need to do more review and internal testing. We do appreciate your patience and will definitely update you here once we made a decision on it. Thank you again. |
@v-afrafi if you are doing major review of Kerberos, you might also consider this: #163 |
Hello @v-afrafi and @v-nisidh, Still no issues with this patch? Do you feel confident it can be integrated? I'll be pleased to help if you have questions or issues Regards |
return serverName.compareTo(o.serverName); | ||
} | ||
|
||
public int getPriority() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just wondering if user can benefit from this public methods. If so, can you please add the javaDocs for this and the rest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -0,0 +1,37 @@ | |||
package com.microsoft.sqlserver.jdbc.dns; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add the licence header files to the new classes? You can find them in other classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DONE
public int getPriority() { | ||
return priority; | ||
} | ||
|
||
/** | ||
* Get the weight of DNS record from 0 to 65535. | ||
* @return The weight, hi value means higher probability of selecting the given record for a given priority. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo of high instead of hi maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
dnsName = canonicalHostName; | ||
} | ||
catch (UnknownHostException cannotCanonicalize) { | ||
// ignored, but we are in a bad shape |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we log this exception if occurs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Canonicalisation failure is not really an error, it simply means there is no reverse DNS. Most of the time it might not be an error IMHO, but if we are here, it probably means that Kerberos won't work.
It is just an improvement, but not a case of failure IMHO
@@ -0,0 +1,21 @@ | |||
package com.microsoft.sqlserver.jdbc.dns; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test files also need licence header :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DONE
@v-afrafi no other change? ;) |
Thank you @pierresouchay for your great contribution! We really appreciate your work on this feature! And apologies for the delay. |
Great, than you! |
/** | ||
* Find all SRV Record using DNS. | ||
* | ||
* You can then use {@link DNSRecordsSRVCollection#getBestRecord()} to find the best candidate (for instance for Round-Robin calls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello @pierresouchay, thank you very much for your contribution. Just one question and wish you could help us. This line of JavaDoc gives error to us, because Maven cannot find a reference to DNSRecordsSRVCollection#getBestRecord()
, could you please let us know how we can fix it? Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done here: #287
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@v-xiangs simply remove the comment or apply #287
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pierresouchay thank you for the quick response ! I am going to create a PR to fix another broken JavaDocs, I guess I will apply your suggestion of the fix in that PR. Thank you!
Hi @pierresouchay, Looks like |
@ulvii I have no idea and I am not working at all on the subject anymore... At worse, if I did my job correctly it will fallback on On our side, I enforced people having DNS records to avoid overriding DNS default behaviour, so we are probably not impacted. I apologise, but I won't have time to work on this, but there is probably another hack to find on in order to make it work again. Since you are from Microsoft, you might have more ease contacting Oracle to find an alternative way than me. The class seems to be still here in OpenJDK 10: http://hg.openjdk.java.net/jdk10/jdk10/jdk/file/777356696811/src/java.security.jgss/share/classes/sun/security/krb5/Config.java#l1171 So I suspect, reflection is forbidden right? In that case, the DNS implementation should work anyway while less powerful for administrators, it should work for most shops Kind Regards |
The original driver only computes the SPN without its REALM. That is why the driver
fails in a cross-domain authentication (ex: user@REALM1 try to log on server@REALM2
while REALM2 and REALM1 are in trust).
This commit solves this by trying to compute the REALM when REALM has not been
provided in the SPN. Which includes both generated SPN and User-Provided SPN.
It also enable Kerberos authentication when only IP is provided as long as reverse
DNS are present since when SPN is provided, and REALM lookup did fail, it will
also try with canonical name and if it works, override the hostname in SPN (feature
not activated when user did provide an SPN)
See Issue 36: #36
This fixes #36