diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 86a94d05b1282a904c64b34bd6b4a8d0f23c3e6d..fd4eff3f5e8ee879590f868637879927b9941fca 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -21,6 +21,14 @@ python3.6:
     - tox -e py36
     - tox -e pep8
 
+python2.7:
+    image: python:2.7
+    stage: test
+    tags: [qlf-ci.inria.fr]
+    script:
+    - pip install tox
+    - tox -e py27 pep8
+
 #### Entering th release zone
 pages:
     stage: deploy
diff --git a/README.org b/README.org
index 48c4b90e31642e843bb0ab37fa4a850b58b2ee56..2241f14150c1a67b49611224f4c74cda3ed47b96 100644
--- a/README.org
+++ b/README.org
@@ -688,6 +688,56 @@ In [2]: # gk is your entry point
    #+END_SRC
 
 
+*** Using Grid'5000 client certificates
+ 
+~python-grid5000~ can also be used as a trusted client with Grid'5000
+internal certificate. In this mode users can pass the ~g5k_user~ argument 
+to most calls to specify which user the API call should be made as. In 
+cases where ~g5k_user~ is not specified API calls will be made as the 
+~anonymous~ user whose access is limited to the Grid'5000 reference API.
+In this mode ~python-grid5000~ does not store any login information, so
+~g5k_user~ most be provided explicitly provided on every call that requires
+one. 
+
+   #+BEGIN_SRC python :exports code :tangle examples/certificate.py
+   import logging
+
+   from grid5000 import Grid5000
+
+   logging.basicConfig(level=logging.DEBUG)
+
+   gk = Grid5000(
+      uri="https://api-ext.grid5000.fr/stable/",
+      sslcert="/path/to/ssl/certfile.cert",
+      sslkey="/path/to/ssl/keyfile.key"
+      )
+
+   gk.sites.list()
+
+   job = site.jobs.create({"name": "pyg5k",
+                           "command": "sleep 3600"},
+                           g5k_user = "auser1")
+
+   
+   # Since the 'anonymous' user can not inspect jobs the following call will raise exception
+   # python-grid5000.exceptions.Grid5000AuthenticationError: 401 Unauthorized
+   job.refresh()
+
+   # Both following call work since any user can request info on any jobs.
+   job.refresh(g5k_user='auser1')
+   job.refresh(g5k_user='auser2')
+
+   # Some operations can only be performed by the jobs creator.
+   # The following call will raise exception 
+   # pyg5k.exceptions.Grid5000DeleteError: 403 Unauthorized
+   job.delete(g5k_user='auser2')
+
+   # This call works as expected
+   job.delete(g5k_user='auser1')
+
+   #+END_SRC
+
+ 
 * Appendix                                                         :noexport:
 ** How to export this file
 
@@ -700,4 +750,3 @@ In [2]: # gk is your entry point
   Do ~C-c C-v t~ or ~M-x
   org-babel-tangle~. The scripts are available under available under ~examples~.
 
-  
diff --git a/README.rst b/README.rst
index 4cedc021a1ea6811db06ceebc1fde15c187fff39..3136caab65cf5b8934fea3d7575ac09c807c4aa9 100644
--- a/README.rst
+++ b/README.rst
@@ -253,7 +253,7 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
     site = gk.sites["rennes"]
 
     job = site.jobs.create({"name": "pyg5k",
-                            "command": "sleep 3600"})
+    			"command": "sleep 3600"})
 
     while job.state != "running":
         job.refresh()
@@ -287,8 +287,8 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
     site = gk.sites["rennes"]
 
     job = site.jobs.create({"name": "pyg5k",
-                            "command": "sleep 3600",
-                            "types": ["deploy"]})
+    			"command": "sleep 3600",
+    			"types": ["deploy"]})
 
     while job.state != "running":
         job.refresh()
@@ -298,7 +298,7 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
     print("Assigned nodes : %s" % job.assigned_nodes)
 
     deployment = site.deployments.create({"nodes": job.assigned_nodes,
-                                          "environment": "debian9-x64-min"})
+    				      "environment": "debian9-x64-min"})
     # To get SSH access to your nodes you can pass your public key
     #
     # from pathlib import Path
@@ -358,8 +358,8 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
     site = gk.sites["rennes"]
 
     job = site.jobs.create({"name": "pyg5k",
-                            "command": "sleep 3600",
-                            "resources": "slash_22=1+nodes=1"})
+    			"command": "sleep 3600",
+    			"resources": "slash_22=1+nodes=1"})
 
     while job.state != "running":
         job.refresh()
@@ -371,8 +371,8 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
 
     # create acces for all ips in the subnet
     access = site.storage["msimonin"].access.create({"ipv4": ip_network,
-                                                      "termination": {"job": job.uid,
-                                                                      "site": site.uid}})
+    						  "termination": {"job": job.uid,
+    								  "site": site.uid}})
 
 4.7 Vlan API
 ~~~~~~~~~~~~
@@ -434,9 +434,9 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
       site = gk.sites["rennes"]
 
       job = site.jobs.create({"name": "pyg5k",
-                              "command": "sleep 3600",
-                              "resources": "{type='kavlan'}/vlan=1+nodes=1",
-                              "types": ["deploy"]})
+      			"command": "sleep 3600",
+      			"resources": "{type='kavlan'}/vlan=1+nodes=1",
+      			"types": ["deploy"]})
 
       while job.state != "running":
           job.refresh()
@@ -444,8 +444,8 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
           time.sleep(5)
 
       deployment = site.deployments.create({"nodes": job.assigned_nodes,
-                                            "environment": "debian9-x64-min",
-                                            "vlan": job.resources_by_type["vlans"][0]})
+      				      "environment": "debian9-x64-min",
+      				      "vlan": job.resources_by_type["vlans"][0]})
 
       while deployment.status != "terminated":
           deployment.refresh()
@@ -485,9 +485,9 @@ Before starting, the file ``$HOME/.python-grid5000.yaml`` will be loaded.
       site = gk.sites["rennes"]
 
       job = site.jobs.create({"name": "pyg5k",
-                              "command": "sleep 3600",
-                              "resources": "{type='kavlan'}/vlan=1+{cluster='paranoia'}nodes=1",
-                              "types": ["deploy"]
+      			"command": "sleep 3600",
+      			"resources": "{type='kavlan'}/vlan=1+{cluster='paranoia'}nodes=1",
+      			"types": ["deploy"]
       })
 
       while job.state != "running":
@@ -555,16 +555,16 @@ For this example you’ll need ``matplotlib``, ``seaborn`` and ``pandas``.
         value = timeserie.values
         measurement = timeserie.uid
         df = pd.concat([df, pd.DataFrame({
-            "timestamp": timestamp,
-            "value": value,
-            "measurement": [measurement]*len(timestamp)
+    	"timestamp": timestamp,
+    	"value": value,
+    	"measurement": [measurement]*len(timestamp)
         })])
 
     sns.relplot(data=df,
-                x="timestamp",
-                y="value",
-                hue="measurement",
-                kind="line")
+    	    x="timestamp",
+    	    y="value",
+    	    hue="measurement",
+    	    kind="line")
     plt.show()
 
 4.9 More snippets
@@ -594,8 +594,8 @@ For this example you’ll need ``matplotlib``, ``seaborn`` and ``pandas``.
         candidates = site.clusters.list()
         matching = [c.uid for c in candidates if c.uid in clusters]
         if len(matching) == 1:
-            matches.append((site, matching[0]))
-            clusters.remove(matching[0])
+    	matches.append((site, matching[0]))
+    	clusters.remove(matching[0])
     print("We found the following matches %s" % matches)
 
 4.9.2 Get all job with a given name on all the sites
@@ -625,13 +625,13 @@ For this example you’ll need ``matplotlib``, ``seaborn`` and ``pandas``.
     jobs = []
     for site in sites:
         job = site.jobs.create({"name": "pyg5k",
-                                "command": "sleep 3600"})
+    			    "command": "sleep 3600"})
         jobs.append(job)
 
     _jobs = []
     for site in sites:
         _jobs.append((site.uid, site.jobs.list(name=NAME,
-                                               state="waiting,launching,running")))
+    					   state="waiting,launching,running")))
 
     print("We found %s" % _jobs)
 
@@ -674,13 +674,13 @@ cross-processes cache) and give you control on the cached object. Enough talking
     def get_api_client():
         """Gets the reference to the API cient (singleton)."""
         with _api_lock:
-            global _api_client
-            if not _api_client:
-                conf_file = os.path.join(os.environ.get("HOME"),
-                                        ".python-grid5000.yaml")
-                _api_client = Grid5000.from_yaml(conf_file)
+    	global _api_client
+    	if not _api_client:
+    	    conf_file = os.path.join(os.environ.get("HOME"),
+    				    ".python-grid5000.yaml")
+    	    _api_client = Grid5000.from_yaml(conf_file)
 
-            return _api_client
+    	return _api_client
 
 
     @ring.disk(storage)
@@ -696,8 +696,8 @@ cross-processes cache) and give you control on the cached object. Enough talking
         sites = get_sites_obj()
         clusters = []
         for site in sites:
-            # should we cache the list aswell ?
-            clusters.extend(site.clusters.list())
+    	# should we cache the list aswell ?
+    	clusters.extend(site.clusters.list())
         return clusters
 
 
@@ -710,3 +710,52 @@ cross-processes cache) and give you control on the cached object. Enough talking
         print("Calling again the function is now faster")
         clusters = get_all_clusters_obj()
         print(clusters)
+
+4.9.4 Using Grid’5000 client certificates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``python-grid5000`` can also be used as a trusted client with Grid’5000
+internal certificate. In this mode users can pass the ``g5k_user`` argument 
+to most calls to specify which user the API call should be made as. In 
+cases where ``g5k_user`` is not specified API calls will be made as the 
+``anonymous`` user whose access is limited to the Grid’5000 reference API.
+In this mode ``python-grid5000`` does not store any login information, so
+``g5k_user`` most be provided explicitly provided on every call that requires
+one. 
+
+.. code:: python
+
+    import logging
+
+    from grid5000 import Grid5000
+
+    logging.basicConfig(level=logging.DEBUG)
+
+    gk = Grid5000(
+       uri="https://api-ext.grid5000.fr/stable/",
+       sslcert="/path/to/ssl/certfile.cert",
+       sslkey="/path/to/ssl/keyfile.key"
+       )
+
+    gk.sites.list()
+
+    job = site.jobs.create({"name": "pyg5k",
+    			"command": "sleep 3600"},
+    			g5k_user = "auser1")
+
+
+    # Since the 'anonymous' user can not inspect jobs the following call will raise exception
+    # python-grid5000.exceptions.Grid5000AuthenticationError: 401 Unauthorized
+    job.refresh()
+
+    # Both following call work since any user can request info on any jobs.
+    job.refresh(g5k_user='auser1')
+    job.refresh(g5k_user='auser2')
+
+    # Some operations can only be performed by the jobs creator.
+    # The following call will raise exception 
+    # pyg5k.exceptions.Grid5000DeleteError: 403 Unauthorized
+    job.delete(g5k_user='auser2')
+
+    # This call works as expected
+    job.delete(g5k_user='auser1')
diff --git a/examples/certificate.py b/examples/certificate.py
new file mode 100644
index 0000000000000000000000000000000000000000..8977ccec8f964f8f509cbd2de732a4124f8a356c
--- /dev/null
+++ b/examples/certificate.py
@@ -0,0 +1,34 @@
+import logging
+
+from grid5000 import Grid5000
+
+logging.basicConfig(level=logging.DEBUG)
+
+gk = Grid5000(
+   uri="https://api-ext.grid5000.fr/stable/",
+   sslcert="/path/to/ssl/certfile.cert",
+   sslkey="/path/to/ssl/keyfile.key"
+   )
+
+gk.sites.list()
+
+job = site.jobs.create({"name": "pyg5k",
+                        "command": "sleep 3600"},
+                        g5k_user = "auser1")
+
+
+# Since the 'anonymous' user can not inspect jobs the following call will raise exception
+# python-grid5000.exceptions.Grid5000AuthenticationError: 401 Unauthorized
+job.refresh()
+
+# Both following call work since any user can request info on any jobs.
+job.refresh(g5k_user='auser1')
+job.refresh(g5k_user='auser2')
+
+# Some operations can only be performed by the jobs creator.
+# The following call will raise exception 
+# pyg5k.exceptions.Grid5000DeleteError: 403 Unauthorized
+job.delete(g5k_user='auser2')
+
+# This call works as expected
+job.delete(g5k_user='auser1')
diff --git a/grid5000/__init__.py b/grid5000/__init__.py
index 168a7610c8f23e993cd63a274e1a0b8739a99ebb..db84e6ff4d8d454e024f63e9c4da1831544ad521 100644
--- a/grid5000/__init__.py
+++ b/grid5000/__init__.py
@@ -49,17 +49,20 @@ class Grid5000(object):
         verify_ssl (bool); Whether SSL certificates should be validated.
         timeout (float): Timeout to use for requests to the Grid5000 API.
         session (requests.Session): session to use
+        ssl_cert (str): path to the client certificate file for Grid5000 API
+        ssl_key (str): path to the client key file for Grid5000 API
     """
 
     def __init__(
         self,
-        *,
         uri=DEFAULT_BASE_URL,
         username=None,
         password=None,
         verify_ssl=True,
         timeout=None,
         session=None,
+        sslcert=None,
+        sslkey=None,
         **kwargs
     ):
         self._uri = uri
@@ -68,12 +71,21 @@ class Grid5000(object):
         self.password = password
         self.verify_ssl = verify_ssl
 
+        self.client_ssl = False
+        self.client_cert = None
+        if sslcert is not None:
+            self.client_ssl = True
+            if sslkey is not None:
+                self.client_cert = (sslcert, sslkey)
+            else:
+                self.client_cert = sslcert
+
         self.headers = {"user-agent": USER_AGENT}
         self.session = _create_session()
 
         # manage auth
         self._http_auth = None
-        if self.username:
+        if self.username and not self.client_ssl:
             self._http_auth = requests.auth.HTTPBasicAuth(self.username, self.password)
 
         self.root = RootManager(self)
@@ -97,16 +109,14 @@ class Grid5000(object):
                 return cls(**conf)
         except Exception as e:
             logging.warn(e)
-            logging.info(
-                "...Falling back to anonymous connection"
-            )
+            logging.info("...Falling back to anonymous connection")
             return cls()
 
     def __enter__(self):
         return self
 
     def __exit__(self, *args):
-        return self.session.cloase()
+        return self.session.close()
 
     def _build_url(self, path):
         """Returns the full url from path.
@@ -122,26 +132,52 @@ class Grid5000(object):
         else:
             return "%s%s" % (self._uri, path)
 
-    def _get_session_opts(self, content_type=None, accept=None):
+    def _get_session_opts(
+        self, content_type=None, accept=None, user_id=None, other_headers=None
+    ):
+        """Returns list of option and headers to use of an http transaction
+
+        Args:
+            content_type (str): value of the Content-type http headers
+            accept (str) : value of the Accept http header
+            user_id (str) : Grid5000 user id to use in certificate mode
+            other_headers (dict) : other http headers to include
+        """
         request_headers = self.headers.copy()
         if content_type is not None:
             request_headers["Content-type"] = content_type
         if accept is not None:
             request_headers["Accept"] = accept
 
-        return {
+        res = {
             "headers": request_headers,
-            "auth": self._http_auth,
             "timeout": self.timeout,
             "verify": self.verify_ssl,
+            "cert": self.client_cert,
         }
 
+        if self.client_ssl:
+            if user_id is not None:
+                request_headers["X-Api-User-CN"] = user_id
+                request_headers["X-Remote-Ident"] = user_id
+            else:
+                request_headers["X-Api-User-CN"] = "anonymous"
+                request_headers["X-Remote-Ident"] = "anonymous"
+        else:
+            res["auth"] = self._http_auth
+
+        if other_headers is not None:
+            request_headers.update(other_headers)
+
+        return res
+
     def http_request(
         self,
         verb,
         path,
         query_data=None,
         post_data=None,
+        header_data=None,
         streamed=False,
         content_type="application/json",
         accept="application/json",
@@ -157,6 +193,7 @@ class Grid5000(object):
             query_data (dict): Data to send as query parameters
             post_data (dict): Data to send in the body (will be converted to
                               json)
+            header_data (dict): Data to send as http headers
             streamed (bool): Whether the data should be streamed
             **kwargs: Extra options to send to the server (e.g. sudo)
 
@@ -169,10 +206,20 @@ class Grid5000(object):
         query_data = {} if query_data is None else query_data
         url = self._build_url(path)
 
-        opts = self._get_session_opts(content_type=content_type, accept=accept)
+        g5k_user = None
+        if self.client_ssl:
+            g5k_user = kwargs.pop("g5k_user", None)
+
+        opts = self._get_session_opts(
+            content_type=content_type,
+            accept=accept,
+            user_id=g5k_user,
+            other_headers=header_data,
+        )
 
         verify = opts.pop("verify")
         timeout = opts.pop("timeout")
+        cert = opts.pop("cert")
 
         json = post_data
         data = None
@@ -186,12 +233,11 @@ class Grid5000(object):
         prepped = req.prepare()
 
         settings = self.session.merge_environment_settings(
-            prepped.url, {}, streamed, verify, None
+            prepped.url, {}, streamed, verify, cert
         )
 
         while True:
             result = self.session.send(prepped, timeout=timeout, **settings)
-
             # TODO:
             # https://www.grid5000.fr/mediawiki/index.php/API#Status_Codes
             if 200 <= result.status_code < 300:
diff --git a/grid5000/mixins.py b/grid5000/mixins.py
index f08580f732f362d869d99b9f09feb137c6485e08..e56438a1f4c65dcfa90af1bb73b929962e10a0e1 100644
--- a/grid5000/mixins.py
+++ b/grid5000/mixins.py
@@ -198,7 +198,7 @@ class ObjectDeleteMixin(object):
             Grid5000AuthenticationError: If authentication is not correct
             Grid5000DeleteError: If the server cannot perform the request
         """
-        self.manager.delete(self.get_id())
+        self.manager.delete(self.get_id(), **kwargs)
 
 
 # Composite Mixins
diff --git a/setup.cfg b/setup.cfg
index 454cbd581fe76c7b6bfeb8a491dd2fdd951f3ece..de5c8349041db92cdc9b8f4a6b1ef513f9e30973 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,7 +24,7 @@ setup_requires =
 install_requires =
     requests>=2.21
     pyyaml>=5.1
-    ipython>=7.3.0
+    ipython
 
 [options.packages.find]
 exclude =