[Freeipa-devel] [PATCH] 353 Added initial vault implementation.

Endi Sukma Dewata edewata at redhat.com
Tue Nov 4 06:27:28 UTC 2014


On 10/28/2014 5:34 PM, Endi Sukma Dewata wrote:
>>>> The NSSConnection class has to be modified not to shutdown existing
>>>> database because some of the vault clients (e.g. vault-archive and
>>>> vault-retrieve) also use a database to encrypt/decrypt the secret.
>>>
>>> The problem is described in more detail in this ticket:
>>> https://fedorahosted.org/freeipa/ticket/4638
>>>
>>> The changes to the NSSConnection in the first patch caused the
>>> installation to fail. Attached is a new patch that uses the solution
>>> proposed by jdennis.
>>
>> New patch attached. It's now using the correct OID's for the schema. It
>> also has been rebased on top of #352-1 and #354.
>
> New patch attached to fix the ticket URL. It depends on #352-2 and #354-1.

New patch attached to fix vault/container ID problems and for some cleanups.

-- 
Endi S. Dewata
-------------- next part --------------
>From 71c91726e1c442baa243210bbbb00556a3c9ce8e Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata at redhat.com>
Date: Tue, 21 Oct 2014 10:57:08 -0400
Subject: [PATCH] Added initial vault implementation.

This patch provides the initial vault implementation which allows
the admin to create a vault, archive data, and retrieve data using
a standard vault. It also includes the initial LDAP schema.

It currently has limitations including:
 - The vault only supports the standard vault type.
 - The vault can only be used by the admin user.
 - The transport certificate has to be installed manually.

These limitations, other vault features, schema and ACL changes
will be addressed in subsequent patches.

The DN class has been enhanched to accept a list of RDNs.

https://fedorahosted.org/freeipa/ticket/3872
---
 API.txt                            | 167 +++++++
 VERSION                            |   4 +-
 install/share/60basev4.ldif        |   3 +
 install/share/Makefile.am          |   1 +
 install/share/copy-schema-to-ca.py |   1 +
 install/updates/40-vault.update    |  27 ++
 install/updates/Makefile.am        |   1 +
 ipa-client/man/default.conf.5      |   1 +
 ipalib/constants.py                |   1 +
 ipalib/plugins/user.py             |   9 +
 ipalib/plugins/vault.py            | 865 +++++++++++++++++++++++++++++++++++++
 ipapython/dn.py                    |   2 +
 ipaserver/install/dsinstance.py    |   1 +
 13 files changed, 1081 insertions(+), 2 deletions(-)
 create mode 100644 install/share/60basev4.ldif
 create mode 100644 install/updates/40-vault.update
 create mode 100644 ipalib/plugins/vault.py

diff --git a/API.txt b/API.txt
index 0000491d7a76fd1d2d50208d314d1600839ce295..b73da0af55a3c514de73ae4e1b2a4d13c01c903d 100644
--- a/API.txt
+++ b/API.txt
@@ -4475,6 +4475,173 @@ option: Str('version?', exclude='webui')
 output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: vault_add
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Bytes('data?', cli_name='data')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Str('in?', cli_name='in')
+option: Str('parent', attribute=False, cli_name='parent', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('text?', cli_name='text')
+option: Str('vault_id', attribute=False, cli_name='vault_id', multivalue=False, required=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_archive
+args: 1,11,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Bytes('data?', cli_name='data')
+option: Bytes('encrypted_data?', cli_name='encrypted_data')
+option: Str('in?', cli_name='in')
+option: Bytes('nonce?', cli_name='nonce')
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('text?', cli_name='text')
+option: Str('version?', exclude='webui')
+option: Bytes('wrapped_session_key?', cli_name='wrapped_session_key')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_del
+args: 1,3,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent?', cli_name='parent')
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: vault_find
+args: 1,10,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Str('parent', attribute=False, autofill=False, cli_name='parent', multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('vault_id', attribute=False, autofill=False, cli_name='vault_id', multivalue=False, query=True, required=False)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: vault_mod
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Str('parent', attribute=False, autofill=False, cli_name='parent', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('vault_id', attribute=False, autofill=False, cli_name='vault_id', multivalue=False, required=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_retrieve
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('out?', cli_name='out')
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Flag('stdout?', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+option: Bytes('wrapped_session_key?', cli_name='wrapped_session_key')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vault_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='vault_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vaultcontainer_add
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('container_id', attribute=False, cli_name='container_id', multivalue=False, required=False)
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Str('parent', attribute=False, cli_name='parent', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vaultcontainer_del
+args: 1,3,3
+arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=True, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent?', cli_name='parent')
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: vaultcontainer_find
+args: 1,10,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=False)
+option: Str('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Str('parent', attribute=False, autofill=False, cli_name='parent', multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: vaultcontainer_mod
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('container_id', attribute=False, autofill=False, cli_name='container_id', multivalue=False, required=False)
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Str('parent', attribute=False, autofill=False, cli_name='parent', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: vaultcontainer_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='container_name', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('parent?', cli_name='parent')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 capability: messages 2.52
 capability: optional_uid_params 2.54
 capability: permissions2 2.69
diff --git a/VERSION b/VERSION
index b0d41e5e1ec59ddefbdcccf588b97bac2ff798ee..fe23eae5f349f4a2d40c3d3e55f6168a82b961b2 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=108
-# Last change: pvoborni - manage authorization of keytab operations
+IPA_API_VERSION_MINOR=109
+# Last change: edewata - initial vault implementation
diff --git a/install/share/60basev4.ldif b/install/share/60basev4.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..97553d53938093c1b0ecba0826fc469d0d758c62
--- /dev/null
+++ b/install/share/60basev4.ldif
@@ -0,0 +1,3 @@
+dn: cn=schema
+objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' SUP nsContainer STRUCTURAL MAY ( description ) X-ORIGIN 'IPA v4.1' )
+objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' SUP nsContainer STRUCTURAL MAY ( description ) X-ORIGIN 'IPA v4.1' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 878d8868bbbb4f774d378b1d2e886841e2b4b7e4..ec417b634aeb80be3a39f2b8e3410e68a1131ee0 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -14,6 +14,7 @@ app_DATA =				\
 	60ipaconfig.ldif		\
 	60basev2.ldif			\
 	60basev3.ldif			\
+	60basev4.ldif			\
 	60ipadns.ldif			\
 	60ipapk11.ldif			\
 	61kerberos-ipav3.ldif		\
diff --git a/install/share/copy-schema-to-ca.py b/install/share/copy-schema-to-ca.py
index fc53fe4cb52486cc618bec77aabe8283ad5eadbc..fb938d212f0f4ddd9a8250a362b89c29d3078efd 100755
--- a/install/share/copy-schema-to-ca.py
+++ b/install/share/copy-schema-to-ca.py
@@ -29,6 +29,7 @@ SCHEMA_FILENAMES = (
     "60ipaconfig.ldif",
     "60basev2.ldif",
     "60basev3.ldif",
+    "60basev4.ldif",
     "60ipadns.ldif",
     "61kerberos-ipav3.ldif",
     "65ipacertstore.ldif",
diff --git a/install/updates/40-vault.update b/install/updates/40-vault.update
new file mode 100644
index 0000000000000000000000000000000000000000..59e5b629ce4e6c5acac06df78f02106afa6c859e
--- /dev/null
+++ b/install/updates/40-vault.update
@@ -0,0 +1,27 @@
+dn: cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: objectClass: ipaVaultContainer
+default: cn: vaults
+default: description: Root vault container
+
+dn: cn=services,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: objectClass: ipaVaultContainer
+default: cn: services
+default: description: Services vault container
+
+dn: cn=shared,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: objectClass: ipaVaultContainer
+default: cn: shared
+default: description: Shared vault container
+
+dn: cn=users,cn=vaults,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: objectClass: ipaVaultContainer
+default: cn: users
+default: description: Users vault container
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 2bd877a0d89525a69ea2d09647499ab2311bb358..a127f91cbe03aa13ec90bd628eaa29b7a898c3b9 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -32,6 +32,7 @@ app_DATA =				\
 	40-dns.update			\
 	40-automember.update		\
 	40-otp.update			\
+	40-vault.update			\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipa-client/man/default.conf.5 b/ipa-client/man/default.conf.5
index dbc8a5b4647439de4de7c01152d098eb0561e236..0973f1a07179ad64daa326a02803cdc9ba1870aa 100644
--- a/ipa-client/man/default.conf.5
+++ b/ipa-client/man/default.conf.5
@@ -221,6 +221,7 @@ The following define the containers for the IPA server. Containers define where
   container_sudocmdgroup: cn=sudocmdgroups,cn=sudo
   container_sudorule: cn=sudorules,cn=sudo
   container_user: cn=users,cn=accounts
+  container_vault: cn=vaults
   container_virtual: cn=virtual operations,cn=etc
 
 .SH "FILES"
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 325414b64fdacd4d8df261588cfc9b7481923be1..f64e02b5cf2a949a3c0ea7c1702132a3a09c1c19 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -97,6 +97,7 @@ DEFAULT_CONFIG = (
     ('container_hbacservice', DN(('cn', 'hbacservices'), ('cn', 'hbac'))),
     ('container_hbacservicegroup', DN(('cn', 'hbacservicegroups'), ('cn', 'hbac'))),
     ('container_dns', DN(('cn', 'dns'))),
+    ('container_vault', DN(('cn', 'vaults'))),
     ('container_virtual', DN(('cn', 'virtual operations'), ('cn', 'etc'))),
     ('container_sudorule', DN(('cn', 'sudorules'), ('cn', 'sudo'))),
     ('container_sudocmd', DN(('cn', 'sudocmds'), ('cn', 'sudo'))),
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index e206289248dfe9ae79bd87271ff2c7672fb98b4f..f639d9f6e8ab709e43e53be354912e9ae097db56 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -22,6 +22,7 @@ from time import gmtime, strftime
 import string
 import posixpath
 import os
+import traceback
 
 from ipalib import api, errors
 from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
@@ -889,6 +890,14 @@ class user_del(LDAPDelete):
             else:
                 self.api.Command.otptoken_del(token)
 
+        # Delete user's private vault container.
+        try:
+            vaultcontainer_id = self.api.Object.vaultcontainer.get_private_id(owner)
+            (vaultcontainer_name, vaultcontainer_parent_id) = self.api.Object.vaultcontainer.split_id(vaultcontainer_id)
+            self.api.Command.vaultcontainer_del(vaultcontainer_name, parent=vaultcontainer_parent_id)
+        except errors.NotFound:
+            pass
+
         return dn
 
 
diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py
new file mode 100644
index 0000000000000000000000000000000000000000..711fd779b79f2fcf4df27ceb9e5c6be186cca11e
--- /dev/null
+++ b/ipalib/plugins/vault.py
@@ -0,0 +1,865 @@
+# Authors:
+#   Endi S. Dewata <edewata at redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import random
+import shutil
+import string
+import tempfile
+
+import pki
+import pki.account
+import pki.crypto
+import pki.key
+
+from ipalib import api, errors
+from ipalib import Str, Bytes
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import *
+from ipalib.plugins import baseldap
+from ipalib.request import context
+from ipalib.plugins.user import split_principal
+from ipalib import _, ngettext
+from ipaplatform.paths import paths
+import ipapython.nsslib
+
+__doc__ = _("""
+Vaults
+
+Manage vaults and vault containers.
+
+EXAMPLES:
+
+ List private vaults:
+   ipa vault-find
+
+ List shared vaults:
+   ipa vault-find --parent /shared
+
+ Add a standard vault:
+   ipa vault-add MyVault
+
+ Show a vault:
+   ipa vault-show MyVault
+
+ Archive data into standard vault:
+   ipa vault-archive MyVault --in data.bin
+
+ Retrieve data from standard vault:
+   ipa vault-retrieve MyVault --out data.bin
+
+ Delete a vault:
+   ipa vault-del MyVault
+
+ List private vault containers:
+   ipa vaultcontainer-find
+
+ List top-level vault containers:
+   ipa vaultcontainer-find --parent /
+
+ Add a vault container:
+   ipa vaultcontainer-add MyContainer
+
+ Show a vault container:
+   ipa vaultcontainer-show MyContainer
+
+ Delete a vault container:
+   ipa vaultcontainer-del MyContainer
+""")
+
+register = Registry()
+transport_cert_nickname = "KRA Transport Certificate"
+
+ at register()
+class vaultcontainer(LDAPObject):
+    """
+    Vault container object.
+    """
+    base_dn = DN(api.env.container_vault, api.env.basedn)
+    object_name = _('vault container')
+    object_name_plural = _('vault containers')
+
+    object_class = ['ipaVaultContainer']
+    default_attributes = [
+        'container_id', 'cn', 'description',
+    ]
+    search_display_attributes = [
+        'container_id', 'cn', 'description',
+    ]
+
+    label = _('Vault Containers')
+    label_singular = _('Vault Container')
+
+    takes_params = (
+        Str('cn',
+            pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$',
+            pattern_errmsg='may only include letters, numbers, _, -, . and $',
+            maxlength=255,
+            cli_name='container_name',
+            label=_('Container name'),
+            primary_key=True,
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Container description'),
+        ),
+        Str('parent?',
+            cli_name='parent',
+            label=_('Parent'),
+            doc=_('Parent container'),
+            flags=('virtual_attribute'),
+        ),
+        Str('container_id?',
+            cli_name='container_id',
+            label=_('Container ID'),
+            doc=_('Container ID'),
+            flags=('virtual_attribute'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        """
+        Generates vault container DN from container ID.
+        """
+
+        # get container ID from parameters
+        name = keys[0] if keys else None
+        parent_id = api.Object.vaultcontainer.normalize_id(options.get('parent'))
+        container_id = parent_id + name + u'/' if name else parent_id
+
+        dn = self.base_dn
+
+        # for each name in the ID, prepend the base DN
+        for name in container_id.split(u'/'):
+            if name:
+                dn = DN(('cn', name), dn)
+
+        return dn
+
+    def get_id(self, dn):
+        """
+        Generates container ID from container DN.
+        """
+
+        if len(dn) < len(self.base_dn):
+            raise errors.ValidationError(name='dn',
+                error=_("Invalid container DN."))
+
+        if dn == self.base_dn:
+            return u'/'
+
+        parent_dn = DN(dn[1:])
+        parent_id = self.get_id(parent_dn)
+
+        rdn = dn[0]
+        name = rdn['cn']
+        id = parent_id + name + u'/'
+
+        return id
+
+    def get_private_id(self, username=None):
+        """
+        Returns user's private container ID (i.e. /users/<username>/).
+        """
+
+        if not username:
+            principal = getattr(context, 'principal')
+            (username, realm) = split_principal(principal)
+
+        return u'/users/' + username + u'/'
+
+    def normalize_id(self, id):
+        """
+        Normalizes container ID.
+        """
+
+        # if ID is empty, return user's private container ID
+        if not id:
+            return self.get_private_id()
+
+        # make sure ID ends with slash
+        if not id.endswith(u'/'):
+            id += u'/'
+
+        # if it's an absolute ID, do nothing
+        if id.startswith(u'/'):
+            return id
+
+        # otherwise, prepend with user's private container ID
+        return self.get_private_id() + id
+
+    def split_id(self, id):
+        """
+        Splits a normalized container ID into (container name, parent ID) tuple.
+        """
+
+        # handle root ID
+        if id == u'/':
+            return (None, None)
+
+        # split ID into parent ID, container name, and empty string
+        parts = id.rsplit(u'/', 2)
+
+        # return container name and parent ID
+        return (parts[1], parts[0] + u'/')
+
+
+ at register()
+class vaultcontainer_add(LDAPCreate):
+    __doc__ = _('Create a new vault container.')
+
+    msg_summary = _('Added vault container "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+
+        # add parent container if it doesn't exist
+        try:
+            parent_dn = DN(dn[1:])
+            parent_id = self.obj.get_id(parent_dn)
+            (parent_name, grandparent_id) = self.obj.split_id(parent_id)
+            if parent_name:
+                api.Command.vaultcontainer_add(parent_name, parent=grandparent_id)
+        except errors.DuplicateEntry:
+            pass
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vaultcontainer_del(LDAPDelete):
+    __doc__ = _('Delete a vault container.')
+
+    msg_summary = _('Deleted vault container "%(value)s"')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+
+ at register()
+class vaultcontainer_find(LDAPSearch):
+    __doc__ = _('Search for vault containers.')
+
+    msg_summary = ngettext(
+        '%(count)d vault container matched', '%(count)d vault containers matched', 0
+    )
+
+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options):
+        assert isinstance(base_dn, DN)
+
+        parent_id = self.obj.normalize_id(options.get('parent'))
+        base_dn = self.obj.get_dn(parent=parent_id)
+
+        return (filter, base_dn, scope)
+
+    def post_callback(self, ldap, entries, truncated, *args, **options):
+
+        for entry in entries:
+            entry['container_id'] = self.obj.get_id(entry.dn)
+
+        return truncated
+
+
+ at register()
+class vaultcontainer_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault container.')
+
+    msg_summary = _('Modified vault container "%(value)s"')
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vaultcontainer_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault container.')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['container_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault(LDAPObject):
+    """
+    Vault object.
+    """
+    object_name = _('vault')
+    object_name_plural = _('vaults')
+
+    object_class = ['ipaVault']
+    default_attributes = [
+        'vault_id', 'cn', 'description',
+    ]
+    search_display_attributes = [
+        'vault_id', 'cn', 'description',
+    ]
+
+    label = _('Vaults')
+    label_singular = _('Vault')
+
+    takes_params = (
+        Str('cn',
+            pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$',
+            pattern_errmsg='may only include letters, numbers, _, -, . and $',
+            maxlength=255,
+            cli_name='vault_name',
+            label=_('Vault name'),
+            primary_key=True,
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Vault description'),
+        ),
+        Str('parent?',
+            cli_name='parent',
+            label=_('Parent'),
+            doc=_('Parent container'),
+            flags=('virtual_attribute'),
+        ),
+        Str('vault_id?',
+            cli_name='vault_id',
+            label=_('Vault ID'),
+            doc=_('Vault ID'),
+            flags=('virtual_attribute'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        """
+        Generates vault DN from vault ID.
+        """
+
+        # get vault ID from parameters
+        name = keys[0] if keys else None
+        parent_id = api.Object.vaultcontainer.normalize_id(options.get('parent'))
+        vault_id = parent_id + name if name else parent_id
+
+        dn = api.Object.vaultcontainer.base_dn
+
+        # for each name in the ID, prepend the base DN
+        for name in vault_id.split(u'/'):
+            if name:
+                dn = DN(('cn', name), dn)
+
+        return dn
+
+    def get_id(self, dn):
+        """
+        Generates vault ID from vault DN.
+        """
+
+        if len(dn) <= len(api.Object.vaultcontainer.base_dn):
+            raise errors.ValidationError(name='dn',
+                error=_("Invalid vault DN."))
+
+        parent_dn = DN(dn[1:])
+        parent_id = api.Object.vaultcontainer.get_id(parent_dn)
+
+        rdn = dn[0]
+        name = rdn['cn']
+        id = parent_id + name
+
+        return id
+
+    def split_id(self, id):
+        """
+        Splits a vault ID into (vault name, parent ID) tuple.
+        """
+
+        # split ID into parent ID and vault name
+        parts = id.rsplit(u'/', 1)
+
+        # return vault name and parent ID
+        return (parts[1], parts[0] + u'/')
+
+
+ at register()
+class vault_add(LDAPCreate):
+    __doc__ = _('Create a new vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Bytes('data?',
+            cli_name='data',
+            doc=_('Base-64 encoded binary data to archive'),
+        ),
+        Str('text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str('in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+    )
+
+    msg_summary = _('Added vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        data = options.get('data')
+        text = options.get('text')
+        input_file = options.get('in')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'text' in options:
+            del options['text']
+        if 'in' in options:
+            del options['in']
+
+        # get data
+        if data:
+            pass
+
+        elif text:
+            data = text.encode('utf-8')
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        # create the vault
+        response = super(vault_add, self).forward(*args, **options)
+
+        # archive initial data
+        api.Command.vault_archive(
+            vault_name,
+            parent=parent_id,
+            data=data)
+
+        return response
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+
+        # add parent container if it doesn't exist
+        try:
+            parent_dn = DN(dn[1:])
+            parent_id = api.Object.vaultcontainer.get_id(parent_dn)
+            (parent_name, grandparent_id) = api.Object.vaultcontainer.split_id(parent_id)
+            if parent_name:
+                api.Command.vaultcontainer_add(parent_name, parent=grandparent_id)
+        except errors.DuplicateEntry:
+            pass
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault_del(LDAPDelete):
+    __doc__ = _('Delete a vault.')
+
+    msg_summary = _('Deleted vault "%(value)s"')
+
+    takes_options = LDAPDelete.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def pre_callback(self, ldap, dn, *keys, **options):
+
+        vault_id = self.obj.get_id(dn)
+
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        try:
+            client_key_id = 'ipa' + vault_id.replace(u'/', u'-')
+            key_info = kra_client.keys.get_active_key_info(client_key_id)
+
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        except pki.ResourceNotFoundException, e:
+            pass
+
+        return dn
+
+
+ at register()
+class vault_find(LDAPSearch):
+    __doc__ = _('Search for vaults.')
+
+    msg_summary = ngettext(
+        '%(count)d vault matched', '%(count)d vaults matched', 0
+    )
+
+    def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options):
+        assert isinstance(base_dn, DN)
+
+        parent_id = self.Object.vaultcontainer.normalize_id(options.get('parent'))
+        base_dn = self.Object.vaultcontainer.get_dn(parent=parent_id)
+
+        return (filter, base_dn, scope)
+
+
+    def post_callback(self, ldap, entries, truncated, *args, **options):
+
+        for entry in entries:
+            entry['vault_id'] = self.obj.get_id(entry.dn)
+
+        return truncated
+
+
+ at register()
+class vault_mod(LDAPUpdate):
+    __doc__ = _('Modify a vault.')
+
+    msg_summary = _('Modified vault "%(value)s"')
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault_show(LDAPRetrieve):
+    __doc__ = _('Display information about a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+    )
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        entry_attrs['vault_id'] = self.obj.get_id(dn)
+
+        return dn
+
+
+ at register()
+class vault_archive(LDAPRetrieve):
+    __doc__ = _('Archive data into a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+        Bytes('data?',
+            cli_name='data',
+            doc=_('Base-64 encoded binary data to archive'),
+        ),
+        Str('text?',
+            cli_name='text',
+            doc=_('Text data to archive'),
+        ),
+        Str('in?',
+            cli_name='in',
+            doc=_('File containing data to archive'),
+        ),
+        Bytes('wrapped_session_key?',
+            cli_name='wrapped_session_key',
+            doc=_('Session key wrapped with transport certificate and encoded in base-64'),
+        ),
+        Bytes('encrypted_data?',
+            cli_name='encrypted_data',
+            doc=_('Data encrypted with session key and encoded in base-64'),
+        ),
+        Bytes('nonce?',
+            cli_name='nonce',
+            doc=_('Nonce encrypted encoded in base-64'),
+        ),
+    )
+
+    msg_summary = _('Archived data into vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        data = options.get('data')
+        text = options.get('text')
+        input_file = options.get('in')
+
+        # don't send these parameters to server
+        if 'data' in options:
+            del options['data']
+        if 'text' in options:
+            del options['text']
+        if 'in' in options:
+            del options['in']
+
+        # get data
+        if data:
+            pass
+
+        elif text:
+            data = text.encode('utf-8')
+
+        elif input_file:
+            with open(input_file, 'rb') as f:
+                data = f.read()
+
+        else:
+            data = ''
+
+        # initialize NSS database
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        ipapython.nsslib.current_dbdir = paths.IPA_NSSDB_DIR
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # retrieve transport certificate
+        nss_transport_cert = crypto.get_cert(transport_cert_nickname)
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # encrypt data with session key
+        nonce = crypto.generate_nonce_iv()
+        encrypted_data = crypto.symmetric_wrap(
+            data,
+            session_key,
+            nonce_iv=nonce
+        )
+
+        # send archival request to server
+        options['wrapped_session_key'] = base64.b64encode(wrapped_session_key)
+        options['encrypted_data'] = base64.b64encode(encrypted_data)
+        options['nonce'] = base64.b64encode(nonce)
+
+        return super(vault_archive, self).forward(*args, **options)
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+        entry_attrs['vault_id'] = vault_id
+
+        # connect to KRA
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = 'ipa' + vault_id.replace(u'/', u'-')
+
+        # deactivate any existing KRA record for the vault
+        try:
+            key_info = kra_client.keys.get_active_key_info(client_key_id)
+
+            kra_client.keys.modify_key_status(
+                key_info.get_key_id(),
+                pki.key.KeyClient.KEY_STATUS_INACTIVE)
+
+        except pki.ResourceNotFoundException, e:
+            pass
+
+        wrapped_session_key = base64.b64decode(options['wrapped_session_key'])
+        encrypted_data = base64.b64decode(options['encrypted_data'])
+        nonce = base64.b64decode(options['nonce'])
+
+        # forward encrypted data to KRA
+        kra_client.keys.archive_encrypted_data(
+            client_key_id,
+            pki.key.KeyClient.PASS_PHRASE_TYPE,
+            encrypted_data,
+            wrapped_session_key,
+            "{1 2 840 113549 3 7}",
+            base64.encodestring(nonce),
+        )
+
+        kra_account.logout()
+
+        return dn
+
+
+ at register()
+class vault_retrieve(LDAPRetrieve):
+    __doc__ = _('Retrieve a data from a vault.')
+
+    takes_options = LDAPRetrieve.takes_options + (
+        Str('parent?',
+            cli_name='parent',
+            doc=_('Parent container'),
+        ),
+        Flag('stdout?',
+            doc=_('Show data on standard output'),
+            autofill=False,
+        ),
+        Str('out?',
+            cli_name='out',
+            doc=_('File to store retrieved data'),
+        ),
+        Bytes('wrapped_session_key?',
+            cli_name='wrapped_session_key',
+            doc=_('Session key wrapped with transport certificate and encoded in base-64'),
+        ),
+    )
+
+    has_output_params = (
+        Bytes('data',
+            label=_('Data'),
+        ),
+    )
+
+    msg_summary = _('Retrieved data from vault "%(value)s"')
+
+    def forward(self, *args, **options):
+
+        vault_name = args[0]
+        parent_id = api.Object.vaultcontainer.normalize_id(options.get('parent'))
+
+        stdout = options.get('stdout')
+        output_file = options.get('out')
+
+        # don't send these parameters to server
+        if 'stdout' in options:
+            del options['stdout']
+        if 'out' in options:
+            del options['out']
+
+        # initialize NSS database
+        crypto = pki.crypto.NSSCryptoProvider(paths.IPA_NSSDB_DIR)
+        crypto.initialize()
+        ipapython.nsslib.current_dbdir = paths.IPA_NSSDB_DIR
+
+        # generate session key
+        session_key = crypto.generate_session_key()
+
+        # retrieve transport certificate
+        nss_transport_cert = crypto.get_cert(transport_cert_nickname)
+
+        # wrap session key with transport certificate
+        wrapped_session_key = crypto.asymmetric_wrap(
+            session_key,
+            nss_transport_cert
+        )
+
+        # send retrieval request to server
+        options['wrapped_session_key'] = base64.b64encode(wrapped_session_key)
+
+        response = super(vault_retrieve, self).forward(*args, **options)
+
+        encrypted_data = base64.b64decode(response['result']['encrypted_data'])
+        nonce = base64.b64decode(response['result']['nonce'])
+
+        # decrypt encrypted data with session key
+        data = crypto.symmetric_unwrap(
+            encrypted_data,
+            session_key,
+            nonce_iv=nonce)
+
+        if stdout:
+            print data
+            response['result'] = {}
+            response['summary'] = None
+
+        elif output_file:
+            with open(output_file, 'w') as f:
+                f.write(data)
+
+        else:
+            response['result']['data'] = data
+
+        return response
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+
+        vault_id = self.obj.get_id(dn)
+        entry_attrs['vault_id'] = vault_id
+
+        wrapped_session_key = base64.b64decode(options['wrapped_session_key'])
+
+        # connect to KRA
+        kra_client = api.Backend.kra.get_client()
+
+        kra_account = pki.account.AccountClient(kra_client.connection)
+        kra_account.login()
+
+        client_key_id = 'ipa' + vault_id.replace(u'/', u'-')
+
+        # find KRA record for the vault
+        try:
+            key_info = kra_client.keys.get_active_key_info(client_key_id)
+
+        except pki.ResourceNotFoundException:
+            raise errors.NotFound(reason=_("Data not found."))
+
+        # retrieve encrypted data from KRA
+        key = kra_client.keys.retrieve_key(
+            key_info.get_key_id(),
+            wrapped_session_key)
+
+        entry_attrs['encrypted_data'] = base64.b64encode(key.encrypted_data)
+        entry_attrs['nonce'] = base64.b64encode(key.nonce_data)
+
+        kra_account.logout()
+
+        return dn
diff --git a/ipapython/dn.py b/ipapython/dn.py
index 834291fbe8696622162efa5193622d74f11f25ca..cdc84052b29047e27b1a0349cbcb11873ec48e56 100644
--- a/ipapython/dn.py
+++ b/ipapython/dn.py
@@ -1247,6 +1247,8 @@ class DN(object):
                 return rdns[0]
             else:
                 return rdns
+        elif isinstance(value, list):
+            return value # create DN from a list of RDNs
         elif isinstance(value, (tuple, list)):
             if len(value) != 2:
                 raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (value))
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index d1569697cba7d7fb9f44a3b85afb643a42624f20..9fa736dcf635b286035b4438a6c342e64f09d1d6 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -56,6 +56,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
                     "60ipaconfig.ldif",
                     "60basev2.ldif",
                     "60basev3.ldif",
+                    "60basev4.ldif",
                     "60ipapk11.ldif",
                     "60ipadns.ldif",
                     "61kerberos-ipav3.ldif",
-- 
1.9.0



More information about the Freeipa-devel mailing list