diff --git a/trac/admin/console.py b/trac/admin/console.py
--- a/trac/admin/console.py
+++ b/trac/admin/console.py
@@ -32,7 +32,7 @@
 from trac.env import Environment
 from trac.perm import PermissionSystem
 from trac.ticket.model import *
-from trac.util import getuser
+from trac.util import WindowsError, getuser
 from trac.util.datefmt import parse_date, format_date, format_datetime, utc
 from trac.util.html import html
 from trac.util.text import to_unicode, wrap, unicode_quote, unicode_unquote, \
diff --git a/trac/admin/tests/console.py b/trac/admin/tests/console.py
--- a/trac/admin/tests/console.py
+++ b/trac/admin/tests/console.py
@@ -54,6 +54,12 @@
     return expected
 
 
+class InMemoryConfiguration(Configuration):
+    """A subclass of Configuration that doesn't save to disk."""
+    def save(self):
+        pass
+
+
 class InMemoryEnvironment(Environment):
     """
     A subclass of Environment that keeps its' DB in memory.
@@ -79,10 +85,7 @@
                cls.__module__.find('.tests.') == -1
 
     def setup_config(self, load_defaults=None):
-        self.config = Configuration(None)
-
-    def save_config(self):
-        pass
+        self.config = InMemoryConfiguration(None)
 
 
 class TracadminTestCase(unittest.TestCase):
diff --git a/trac/config.py b/trac/config.py
--- a/trac/config.py
+++ b/trac/config.py
@@ -14,9 +14,11 @@
 
 from ConfigParser import ConfigParser
 from copy import deepcopy
-import os
+import os.path
+import tempfile
 
 from trac.core import ExtensionPoint, TracError
+from trac.util import rename
 from trac.util.compat import set, sorted
 from trac.util.text import to_unicode, CRLF
 from trac.util.translation import _
@@ -208,7 +210,9 @@
 
         # At this point, all the strings in `sections` are UTF-8 encoded `str`
         try:
-            fileobj = open(self.filename, 'w')
+            (fd, path) = tempfile.mkstemp(prefix='trac.ini.',
+                                          dir=os.path.dirname(self.filename))
+            fileobj = os.fdopen(fd, "w")
             try:
                 fileobj.write('# -*- coding: utf-8 -*-\n\n')
                 for section, options in sections:
@@ -223,6 +227,21 @@
                     fileobj.write('\n')
             finally:
                 fileobj.close()
+            
+            # Try to preserve group ownership and permissions, but failure
+            # should not be fatal
+            try:
+                st = os.stat(self.filename)
+                if hasattr(os, 'chmod'):
+                    os.chmod(path, st.st_mode)
+                if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
+                    os.chflags(dst, st.st_flags)
+                if hasattr(os, 'chown'):
+                    os.chown(path, -1, st.st_gid)
+            except OSError:
+                pass
+            
+            rename(path, self.filename, duration=2.0)
             self._old_sections = deepcopy(self.parser._sections)
         except Exception:
             # Revert all changes to avoid inconsistencies
diff --git a/trac/env.py b/trac/env.py
--- a/trac/env.py
+++ b/trac/env.py
@@ -28,7 +28,7 @@
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
 from trac.db import DatabaseManager
-from trac.util import get_pkginfo
+from trac.util import create_file, get_pkginfo
 from trac.util.text import exception_to_unicode
 from trac.util.translation import _
 from trac.versioncontrol import RepositoryManager
@@ -299,12 +299,6 @@
         If options contains ('inherit', 'file'), default values will not be
         loaded; they are expected to be provided by that file or other options.
         """
-        def _create_file(fname, data=None):
-            fd = open(fname, 'w')
-            if data:
-                fd.write(data)
-            fd.close()
-
         # Create the directory structure
         if not os.path.exists(self.path):
             os.mkdir(self.path)
@@ -313,15 +307,16 @@
         os.mkdir(os.path.join(self.path, 'plugins'))
 
         # Create a few files
-        _create_file(os.path.join(self.path, 'VERSION'),
-                     'Trac Environment Version 1\n')
-        _create_file(os.path.join(self.path, 'README'),
-                     'This directory contains a Trac environment.\n'
-                     'Visit http://trac.edgewall.org/ for more information.\n')
+        create_file(os.path.join(self.path, 'VERSION'),
+                    'Trac Environment Version 1\n')
+        create_file(os.path.join(self.path, 'README'),
+                    'This directory contains a Trac environment.\n'
+                    'Visit http://trac.edgewall.org/ for more information.\n')
 
         # Setup the default configuration
         os.mkdir(os.path.join(self.path, 'conf'))
-        _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
+        create_file(os.path.join(self.path, 'conf', 'trac.ini'))
+        create_file(os.path.join(self.path, 'conf', 'trac.ini.sample'))
         skip_defaults = options and ('inherit', 'file') in [(section, option) \
                 for (section, option, value) in options]
         self.setup_config(load_defaults=not skip_defaults)
@@ -526,6 +521,8 @@
 
     def _update_sample_config(self):
         filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
+        if not os.path.isfile(filename):
+            return
         config = Configuration(filename)
         for section, default_options in config.defaults().iteritems():
             for name, value in default_options.iteritems():
diff --git a/trac/util/__init__.py b/trac/util/__init__.py
--- a/trac/util/__init__.py
+++ b/trac/util/__init__.py
@@ -77,6 +77,40 @@
 
 # -- os utilities
 
+try:
+    WindowsError = WindowsError
+except NameError:
+    class WindowsError(OSError):
+        """Dummy exception replacing WindowsError on non-Windows platforms"""
+
+
+def rename(src, dst, duration=1.0, retry_delay=0.1):
+    """Rename `src` to `dst`.
+    
+    Windows raises a WindowsError if the destination of a rename is open (even
+    if only for reading). Retry for the given duration if this is the case.
+    """
+    end = time.time() + duration
+    while True:
+        try:
+            os.rename(src, dst)
+            break
+        except WindowsError:
+            if time.time() >= end:
+                raise
+            time.sleep(retry_delay)
+
+
+def create_file(path, data=""):
+    """Create a new file with the given data."""
+    f = open(path, 'w')
+    try:
+        if data:
+            f.write(data)
+    finally:
+        f.close()
+
+
 def create_unique_file(path):
     """Create a new file. An index is added if the path exists"""
     parts = os.path.splitext(path)

